latticesql 3.3.1 → 3.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +375 -276
- package/dist/index.cjs +374 -276
- package/dist/index.js +374 -276
- package/package.json +3 -1
package/dist/index.cjs
CHANGED
|
@@ -48545,6 +48545,10 @@ function isJunctionTable(table) {
|
|
|
48545
48545
|
const fkCols = new Set(belongsTo.map((r6) => r6.foreignKey));
|
|
48546
48546
|
return table.columns.every((c6) => fkCols.has(c6) || JUNCTION_ALLOWED_NONFK.has(c6));
|
|
48547
48547
|
}
|
|
48548
|
+
function isJunctionByColumns(columns) {
|
|
48549
|
+
const payload = columns.filter((c6) => !JUNCTION_ALLOWED_NONFK.has(c6));
|
|
48550
|
+
return payload.length === 2 && payload.every((c6) => c6.endsWith("_id"));
|
|
48551
|
+
}
|
|
48548
48552
|
function fileJunctions(configPath, outputDir) {
|
|
48549
48553
|
const out = [];
|
|
48550
48554
|
for (const t8 of getGuiEntities(configPath, outputDir).tables) {
|
|
@@ -51884,6 +51888,16 @@ var init_ingest_url = __esm({
|
|
|
51884
51888
|
});
|
|
51885
51889
|
|
|
51886
51890
|
// src/gui/ai/dispatch.ts
|
|
51891
|
+
function visibilityDenialReason(opts) {
|
|
51892
|
+
if (opts.kind === "table") {
|
|
51893
|
+
return opts.canManageTableDefault ? null : "Only the workspace owner can change a table's default sharing.";
|
|
51894
|
+
}
|
|
51895
|
+
if (!opts.rowAccess) return "That record was not found, or is not visible to you.";
|
|
51896
|
+
if (!opts.rowAccess.ownedByMe) {
|
|
51897
|
+
return "You do not own this record, so you cannot change its sharing \u2014 only its owner can.";
|
|
51898
|
+
}
|
|
51899
|
+
return null;
|
|
51900
|
+
}
|
|
51887
51901
|
async function secretColumnsFor(db, table) {
|
|
51888
51902
|
try {
|
|
51889
51903
|
const rows = await db.query("_lattice_gui_column_meta", {
|
|
@@ -52109,6 +52123,14 @@ async function executeFunction(ctx, name, args) {
|
|
|
52109
52123
|
};
|
|
52110
52124
|
}
|
|
52111
52125
|
const id = typeof args.id === "string" && args.id ? args.id : void 0;
|
|
52126
|
+
const denial = id ? visibilityDenialReason({
|
|
52127
|
+
kind: "row",
|
|
52128
|
+
rowAccess: (await rowAccessSummaries(ctx.db, table, [id])).get(id)
|
|
52129
|
+
}) : visibilityDenialReason({
|
|
52130
|
+
kind: "table",
|
|
52131
|
+
canManageTableDefault: await canManageRoles(ctx.db)
|
|
52132
|
+
});
|
|
52133
|
+
if (denial) return { ok: false, error: denial };
|
|
52112
52134
|
try {
|
|
52113
52135
|
if (id) {
|
|
52114
52136
|
await setRowVisibility(ctx.db, table, id, visibility);
|
|
@@ -52272,6 +52294,7 @@ var init_dispatch = __esm({
|
|
|
52272
52294
|
init_column_descriptions();
|
|
52273
52295
|
init_members();
|
|
52274
52296
|
init_table_policy();
|
|
52297
|
+
init_cloud_connect();
|
|
52275
52298
|
init_dedup_service();
|
|
52276
52299
|
DISPATCHABLE = /* @__PURE__ */ new Set([
|
|
52277
52300
|
"list_entities",
|
|
@@ -53993,6 +54016,44 @@ async function reconcileCloudMemberAccess(db) {
|
|
|
53993
54016
|
);
|
|
53994
54017
|
}
|
|
53995
54018
|
}
|
|
54019
|
+
const memberSystemGrants = [
|
|
54020
|
+
["_lattice_gui_meta", "SELECT, INSERT, UPDATE"],
|
|
54021
|
+
["_lattice_gui_column_meta", "SELECT, INSERT, UPDATE"],
|
|
54022
|
+
["_lattice_gui_audit", "SELECT, INSERT"],
|
|
54023
|
+
["__lattice_user_identity", "SELECT, INSERT, UPDATE"]
|
|
54024
|
+
];
|
|
54025
|
+
for (const [tbl, privs] of memberSystemGrants) {
|
|
54026
|
+
await runAsyncOrSync(
|
|
54027
|
+
db.adapter,
|
|
54028
|
+
`DO $LATTICE$ BEGIN
|
|
54029
|
+
IF to_regclass('${tbl}') IS NOT NULL THEN
|
|
54030
|
+
EXECUTE 'GRANT ${privs} ON "${tbl}" TO ${MEMBER_GROUP}';
|
|
54031
|
+
END IF;
|
|
54032
|
+
END $LATTICE$`
|
|
54033
|
+
);
|
|
54034
|
+
}
|
|
54035
|
+
try {
|
|
54036
|
+
await runAsyncOrSync(
|
|
54037
|
+
db.adapter,
|
|
54038
|
+
`GRANT EXECUTE ON FUNCTION json_extract(text, text), strftime(text, text) TO ${MEMBER_GROUP}`
|
|
54039
|
+
);
|
|
54040
|
+
} catch (err) {
|
|
54041
|
+
console.warn(
|
|
54042
|
+
"[reconcileCloudMemberAccess] could not grant EXECUTE on polyfills (will retry next open):",
|
|
54043
|
+
err instanceof Error ? err.message : String(err)
|
|
54044
|
+
);
|
|
54045
|
+
}
|
|
54046
|
+
for (const table of registered) {
|
|
54047
|
+
if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) continue;
|
|
54048
|
+
const cols = db.getRegisteredColumns(table);
|
|
54049
|
+
if (cols && !("deleted_at" in cols)) {
|
|
54050
|
+
const q3 = `"${table.replace(/"/g, '""')}"`;
|
|
54051
|
+
await runAsyncOrSync(
|
|
54052
|
+
db.adapter,
|
|
54053
|
+
`ALTER TABLE ${q3} ADD COLUMN IF NOT EXISTS "deleted_at" TEXT`
|
|
54054
|
+
);
|
|
54055
|
+
}
|
|
54056
|
+
}
|
|
53996
54057
|
}
|
|
53997
54058
|
async function secureNewCloudTable(db, table, pk) {
|
|
53998
54059
|
if (db.getDialect() !== "postgres") return;
|
|
@@ -54018,14 +54079,6 @@ async function secureCloud(db) {
|
|
|
54018
54079
|
await secureNewCloudTable(db, table, db.getPrimaryKey(table));
|
|
54019
54080
|
}
|
|
54020
54081
|
await reconcileCloudMemberAccess(db);
|
|
54021
|
-
await runAsyncOrSync(
|
|
54022
|
-
db.adapter,
|
|
54023
|
-
`DO $LATTICE$ BEGIN
|
|
54024
|
-
IF to_regclass('__lattice_user_identity') IS NOT NULL THEN
|
|
54025
|
-
EXECUTE 'GRANT SELECT, INSERT, UPDATE ON "__lattice_user_identity" TO ${MEMBER_GROUP}';
|
|
54026
|
-
END IF;
|
|
54027
|
-
END $LATTICE$`
|
|
54028
|
-
);
|
|
54029
54082
|
}
|
|
54030
54083
|
|
|
54031
54084
|
// src/index.ts
|
|
@@ -54318,6 +54371,7 @@ init_summarize();
|
|
|
54318
54371
|
// src/gui/server.ts
|
|
54319
54372
|
var import_node_http3 = require("http");
|
|
54320
54373
|
var import_node_child_process4 = require("child_process");
|
|
54374
|
+
var import_ws = require("ws");
|
|
54321
54375
|
var import_node_fs31 = require("fs");
|
|
54322
54376
|
var import_node_path35 = require("path");
|
|
54323
54377
|
var import_yaml5 = require("yaml");
|
|
@@ -56184,6 +56238,31 @@ var appJs = `
|
|
|
56184
56238
|
// Boot analytics with the resolved consent (no network contact when off),
|
|
56185
56239
|
// then record the session open. advanced_mode is a boolean \u2014 safe to send.
|
|
56186
56240
|
if (window.LatticeGA) window.LatticeGA.init(state.analyticsEffective);
|
|
56241
|
+
// Deduplicate unique users in GA: set the GA user_id to a SHA-256 hash of
|
|
56242
|
+
// the operator's email. Anonymized \u2014 the plaintext is hashed in-browser and
|
|
56243
|
+
// never sent (analytics.ts only accepts a hex digest). Without a user_id,
|
|
56244
|
+
// GA counts each session/device as a new user (active-users \u2248 events).
|
|
56245
|
+
// Best-effort + only when analytics consent is on.
|
|
56246
|
+
if (window.LatticeGA && state.analyticsEffective && window.crypto && window.crypto.subtle) {
|
|
56247
|
+
fetchJson('/api/userconfig/identity')
|
|
56248
|
+
.then(function (id) {
|
|
56249
|
+
var email = id && id.email ? String(id.email).trim().toLowerCase() : '';
|
|
56250
|
+
if (!email) return undefined;
|
|
56251
|
+
return window.crypto.subtle
|
|
56252
|
+
.digest('SHA-256', new TextEncoder().encode(email))
|
|
56253
|
+
.then(function (buf) {
|
|
56254
|
+
var hex = Array.prototype.map
|
|
56255
|
+
.call(new Uint8Array(buf), function (b) {
|
|
56256
|
+
return ('0' + b.toString(16)).slice(-2);
|
|
56257
|
+
})
|
|
56258
|
+
.join('');
|
|
56259
|
+
window.LatticeGA.setUser(hex);
|
|
56260
|
+
});
|
|
56261
|
+
})
|
|
56262
|
+
.catch(function () {
|
|
56263
|
+
/* best-effort \u2014 GA still functions without a user_id */
|
|
56264
|
+
});
|
|
56265
|
+
}
|
|
56187
56266
|
gaTrack('app_open', { advanced_mode: advancedMode() });
|
|
56188
56267
|
document.body.classList.toggle('advanced-mode', advancedMode());
|
|
56189
56268
|
wireSettingsDrawer();
|
|
@@ -56195,15 +56274,13 @@ var appJs = `
|
|
|
56195
56274
|
wireHistoryControls();
|
|
56196
56275
|
refreshHistoryState();
|
|
56197
56276
|
renderRoute();
|
|
56198
|
-
|
|
56199
|
-
startRenderProgress();
|
|
56277
|
+
startEventStream();
|
|
56200
56278
|
initSearch();
|
|
56201
56279
|
initLastEdited();
|
|
56202
56280
|
initOffline();
|
|
56203
56281
|
initRailResize();
|
|
56204
56282
|
initRailDrawer();
|
|
56205
56283
|
initRailDragDrop();
|
|
56206
|
-
startFeed();
|
|
56207
56284
|
renderComposer();
|
|
56208
56285
|
initThreadControls();
|
|
56209
56286
|
checkNativeSetup();
|
|
@@ -56219,12 +56296,14 @@ var appJs = `
|
|
|
56219
56296
|
}
|
|
56220
56297
|
|
|
56221
56298
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
56222
|
-
// Realtime
|
|
56223
|
-
//
|
|
56224
|
-
//
|
|
56225
|
-
//
|
|
56299
|
+
// Realtime / feed / render-progress all arrive over ONE multiplexed
|
|
56300
|
+
// WebSocket (/api/stream) \u2014 see startEventStream() below. A single
|
|
56301
|
+
// connection per tab (instead of three SSE streams) keeps the browser's
|
|
56302
|
+
// tiny per-host HTTP connection budget free for data requests, so clicking
|
|
56303
|
+
// objects and switching workspaces stay responsive no matter how many tabs
|
|
56304
|
+
// are open. 'change' events mark the current view dirty and refetch via
|
|
56305
|
+
// afterMutation() (debounced); 'state' events drive the topbar pill.
|
|
56226
56306
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
56227
|
-
var realtimeSource = null;
|
|
56228
56307
|
var realtimePending = null;
|
|
56229
56308
|
// Team-cloud collaboration state. usersById resolves "last edited by"
|
|
56230
56309
|
// names; lastEditedByPk maps "<table>|<pk>" \u2192 { userId, at } from realtime
|
|
@@ -56562,42 +56641,87 @@ var appJs = `
|
|
|
56562
56641
|
afterMutation().catch(function () { /* swallow */ });
|
|
56563
56642
|
}, 200);
|
|
56564
56643
|
}
|
|
56565
|
-
|
|
56566
|
-
|
|
56567
|
-
|
|
56568
|
-
|
|
56569
|
-
|
|
56570
|
-
|
|
56571
|
-
|
|
56572
|
-
|
|
56573
|
-
|
|
56574
|
-
|
|
56575
|
-
|
|
56576
|
-
|
|
56577
|
-
|
|
56578
|
-
|
|
56579
|
-
|
|
56580
|
-
|
|
56581
|
-
if (
|
|
56644
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
56645
|
+
// Multiplexed event stream \u2014 ONE WebSocket carries realtime state/change,
|
|
56646
|
+
// the activity feed, and background-render progress (previously three
|
|
56647
|
+
// separate SSE streams). Holding one connection per tab instead of three
|
|
56648
|
+
// keeps the browser's per-host HTTP budget free for data requests, so the
|
|
56649
|
+
// GUI never freezes when several tabs are open. Each server message is
|
|
56650
|
+
// { type, data }; we demux to the same handlers the SSE listeners used.
|
|
56651
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
56652
|
+
var eventStream = null; // the active WebSocket (or null)
|
|
56653
|
+
var eventStreamReconnect = null; // pending reconnect timer
|
|
56654
|
+
var eventStreamBackoff = 1000; // reconnect delay, grows to a cap
|
|
56655
|
+
var eventStreamClosed = false; // true \u21D2 closed on purpose (switch/teardown), don't reconnect
|
|
56656
|
+
function dispatchStreamMessage(type, data) {
|
|
56657
|
+
if (type === 'realtime-state') {
|
|
56658
|
+
setStatusPill((data && data.mode) || 'local', (data && data.state) || 'local');
|
|
56659
|
+
} else if (type === 'realtime-change') {
|
|
56660
|
+
if (data) onRealtimeChange(data);
|
|
56582
56661
|
scheduleRealtimeRefresh();
|
|
56583
|
-
})
|
|
56584
|
-
|
|
56585
|
-
|
|
56586
|
-
|
|
56662
|
+
} else if (type === 'feed') {
|
|
56663
|
+
try { renderFeedItem(data); } catch (_) { /* render best-effort */ }
|
|
56664
|
+
if (data && (data.table || data.op === 'schema')) scheduleRealtimeRefresh();
|
|
56665
|
+
} else if (type === 'render-snapshot') {
|
|
56666
|
+
if (data) applyRenderSnapshot(data);
|
|
56667
|
+
} else if (type === 'render-progress') {
|
|
56668
|
+
if (data) onRenderEvent(data);
|
|
56669
|
+
}
|
|
56670
|
+
}
|
|
56671
|
+
function scheduleEventStreamReconnect() {
|
|
56672
|
+
if (eventStreamClosed || eventStreamReconnect) return;
|
|
56673
|
+
var delay = eventStreamBackoff;
|
|
56674
|
+
eventStreamBackoff = Math.min(eventStreamBackoff * 2, 15000);
|
|
56675
|
+
eventStreamReconnect = setTimeout(function () {
|
|
56676
|
+
eventStreamReconnect = null;
|
|
56677
|
+
startEventStream();
|
|
56678
|
+
}, delay);
|
|
56679
|
+
}
|
|
56680
|
+
function stopEventStream() {
|
|
56681
|
+
eventStreamClosed = true;
|
|
56682
|
+
if (eventStreamReconnect) { clearTimeout(eventStreamReconnect); eventStreamReconnect = null; }
|
|
56683
|
+
if (eventStream) {
|
|
56684
|
+
// Drop the onclose handler first so an intentional close doesn't trip the
|
|
56685
|
+
// reconnect/disconnect path.
|
|
56686
|
+
try { eventStream.onclose = null; eventStream.close(); } catch (_) { /* ignore */ }
|
|
56687
|
+
eventStream = null;
|
|
56688
|
+
}
|
|
56689
|
+
}
|
|
56690
|
+
function startEventStream() {
|
|
56691
|
+
stopEventStream();
|
|
56692
|
+
eventStreamClosed = false;
|
|
56693
|
+
if (typeof WebSocket === 'undefined') return;
|
|
56694
|
+
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
56695
|
+
var ws;
|
|
56696
|
+
try { ws = new WebSocket(proto + '//' + location.host + '/api/stream'); }
|
|
56697
|
+
catch (_) { scheduleEventStreamReconnect(); return; }
|
|
56698
|
+
eventStream = ws;
|
|
56699
|
+
ws.onopen = function () { eventStreamBackoff = 1000; };
|
|
56700
|
+
ws.onmessage = function (ev) {
|
|
56701
|
+
var msg = null;
|
|
56702
|
+
try { msg = JSON.parse(ev.data); } catch (_) { return; /* ignore malformed */ }
|
|
56703
|
+
if (msg && msg.type) dispatchStreamMessage(msg.type, msg.data);
|
|
56704
|
+
};
|
|
56705
|
+
ws.onerror = function () { /* surfaced via onclose \u2192 reconnect */ };
|
|
56706
|
+
ws.onclose = function () {
|
|
56707
|
+
if (eventStream === ws) eventStream = null;
|
|
56708
|
+
if (eventStreamClosed) return;
|
|
56709
|
+
// Unexpected drop: show the disconnect on the pill and auto-reconnect with
|
|
56710
|
+
// backoff (the server replays state + render snapshot on reconnect).
|
|
56587
56711
|
setStatusPill('cloud', 'disconnected');
|
|
56712
|
+
scheduleEventStreamReconnect();
|
|
56588
56713
|
};
|
|
56589
56714
|
}
|
|
56590
56715
|
|
|
56591
56716
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
56592
|
-
// Background-render progress \u2014
|
|
56593
|
-
// /api/render
|
|
56594
|
-
// context tree in the background;
|
|
56595
|
-
// dashboard cards (bottom-edge bar +
|
|
56596
|
-
// each table completes. Row COUNTS come
|
|
56597
|
-
//
|
|
56598
|
-
//
|
|
56717
|
+
// Background-render progress \u2014 render events arrive over the multiplexed
|
|
56718
|
+
// /api/stream WebSocket (render-snapshot + render-progress). A workspace
|
|
56719
|
+
// opens/switches instantly and renders its context tree in the background;
|
|
56720
|
+
// this paints a per-table % overlay on the dashboard cards (bottom-edge bar +
|
|
56721
|
+
// \u27F3 pill) and dims the row count until each table completes. Row COUNTS come
|
|
56722
|
+
// only from /api/entities \u2014 the render events drive only the transient overlay
|
|
56723
|
+
// and one reconciling refetch on completion.
|
|
56599
56724
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
56600
|
-
var renderSource = null;
|
|
56601
56725
|
// { [table]: { pct, rendered, total, done, error } } \u2014 the live render state,
|
|
56602
56726
|
// re-applied to cards after every dashboard rebuild (drawDashboard wipes the
|
|
56603
56727
|
// DOM overlays but not this map).
|
|
@@ -56710,37 +56834,17 @@ var appJs = `
|
|
|
56710
56834
|
}
|
|
56711
56835
|
}
|
|
56712
56836
|
}
|
|
56713
|
-
|
|
56714
|
-
|
|
56715
|
-
|
|
56716
|
-
|
|
56717
|
-
}
|
|
56718
|
-
if (typeof EventSource === 'undefined') return;
|
|
56719
|
-
// On (re)connect, fetch the single-shot snapshot so a tab that connects
|
|
56720
|
-
// mid- or post-render paints correctly even before the next event. The SSE
|
|
56721
|
-
// endpoint ALSO replays a 'snapshot' event on connect, so both paths agree.
|
|
56722
|
-
fetchJson('/api/render/status').then(applyRenderSnapshot).catch(function () { /* ignore */ });
|
|
56723
|
-
renderSource = new EventSource('/api/render/progress');
|
|
56724
|
-
renderSource.addEventListener('snapshot', function (ev) {
|
|
56725
|
-
var snap = null;
|
|
56726
|
-
try { snap = JSON.parse(ev.data); } catch (_) { /* ignore malformed */ }
|
|
56727
|
-
if (snap) applyRenderSnapshot(snap);
|
|
56728
|
-
});
|
|
56729
|
-
renderSource.addEventListener('progress', function (ev) {
|
|
56730
|
-
var e = null;
|
|
56731
|
-
try { e = JSON.parse(ev.data); } catch (_) { /* ignore malformed */ }
|
|
56732
|
-
if (e) onRenderEvent(e);
|
|
56733
|
-
});
|
|
56734
|
-
// EventSource auto-reconnects on error; the status refetch on the next
|
|
56735
|
-
// open repaints from the authoritative snapshot.
|
|
56736
|
-
}
|
|
56837
|
+
// Render snapshot + progress are applied from the multiplexed /api/stream
|
|
56838
|
+
// WebSocket: the server replays a render-snapshot on connect (so a tab that
|
|
56839
|
+
// connects mid- or post-render paints correctly) and streams render-progress
|
|
56840
|
+
// events thereafter. See dispatchStreamMessage / startEventStream.
|
|
56737
56841
|
|
|
56738
56842
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
56739
56843
|
// Shared activity helpers \u2014 the operation-icon map and relative-time
|
|
56740
56844
|
// formatter, used by Version History and the dashboard activity list. The
|
|
56741
56845
|
// standalone Activity rail was removed in 1.16.1 (redundant with Version
|
|
56742
|
-
// History); multiplayer realtime convergence runs on the
|
|
56743
|
-
//
|
|
56846
|
+
// History); multiplayer realtime convergence runs on the realtime-change
|
|
56847
|
+
// messages of the multiplexed event stream (startEventStream), not on this.
|
|
56744
56848
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
56745
56849
|
var FEED_ICONS = {
|
|
56746
56850
|
insert: '\u2795', update: '\u270F\uFE0F', delete: '\u{1F5D1}',
|
|
@@ -56978,12 +57082,11 @@ var appJs = `
|
|
|
56978
57082
|
if (location.hash !== '#/') location.hash = '#/';
|
|
56979
57083
|
else renderRoute();
|
|
56980
57084
|
loadedTables = {};
|
|
56981
|
-
|
|
56982
|
-
//
|
|
56983
|
-
//
|
|
56984
|
-
// onto this workspace's cards.
|
|
57085
|
+
// A switch swaps the server-side buses to the new workspace; drop the old
|
|
57086
|
+
// workspace's render overlay state and reconnect the multiplexed event
|
|
57087
|
+
// stream so realtime/feed/render all rebind to this workspace.
|
|
56985
57088
|
renderProgress = {};
|
|
56986
|
-
|
|
57089
|
+
startEventStream();
|
|
56987
57090
|
});
|
|
56988
57091
|
}
|
|
56989
57092
|
|
|
@@ -57150,7 +57253,6 @@ var appJs = `
|
|
|
57150
57253
|
// to THIS workspace, and reload its thread list (+ latest convo).
|
|
57151
57254
|
newChat();
|
|
57152
57255
|
clearActivityFeed();
|
|
57153
|
-
startFeed();
|
|
57154
57256
|
refreshThreadList(true);
|
|
57155
57257
|
showToast('Switched workspace', {});
|
|
57156
57258
|
}).catch(function (err) { menu.hidden = true; endWsSwitching(true); showToast('Switch failed: ' + err.message, {}); });
|
|
@@ -57611,7 +57713,7 @@ var appJs = `
|
|
|
57611
57713
|
function rowVisMarkup(tbl, r) {
|
|
57612
57714
|
var a = r._access;
|
|
57613
57715
|
if (!a) return '';
|
|
57614
|
-
var vis = a
|
|
57716
|
+
var vis = effectiveVisibility(a);
|
|
57615
57717
|
var glyph = vis === 'custom' ? '\u25CE' : '\u25C9';
|
|
57616
57718
|
if (!a.ownedByMe) {
|
|
57617
57719
|
var seen = vis === 'custom' ? 'Shared with you' : 'Visible to everyone';
|
|
@@ -57961,7 +58063,7 @@ var appJs = `
|
|
|
57961
58063
|
function detailVisLineEl(row) {
|
|
57962
58064
|
var a = row._access;
|
|
57963
58065
|
if (!a) return '';
|
|
57964
|
-
var vis = a
|
|
58066
|
+
var vis = effectiveVisibility(a);
|
|
57965
58067
|
var labelMap = { everyone: 'Visible to everyone', private: 'Private to you', custom: 'Shared with specific people' };
|
|
57966
58068
|
// Clear visual indicator: a lock when private, an eye when shared (everyone
|
|
57967
58069
|
// or specific people), with a hover tooltip. The shared helper keeps the
|
|
@@ -57972,8 +58074,7 @@ var appJs = `
|
|
|
57972
58074
|
return '<div class="detail-vis muted" style="display:flex;align-items:center;gap:6px;margin:6px 0;font-size:13px">' +
|
|
57973
58075
|
visIcon + '<span>' + escapeHtml(seen) + '</span></div>';
|
|
57974
58076
|
}
|
|
57975
|
-
var info =
|
|
57976
|
-
if (vis === 'custom' && a.grantees) info += ' (' + a.grantees.length + ')';
|
|
58077
|
+
var info = visInfoLabel(a);
|
|
57977
58078
|
var buttons;
|
|
57978
58079
|
if (vis === 'custom') {
|
|
57979
58080
|
// Leaving custom stops the grant list from applying \u2014 the toggle
|
|
@@ -58028,9 +58129,12 @@ var appJs = `
|
|
|
58028
58129
|
if (!panel) return;
|
|
58029
58130
|
if (!panel.hidden) { panel.hidden = true; return; }
|
|
58030
58131
|
var access = row._access || {};
|
|
58031
|
-
|
|
58032
|
-
|
|
58033
|
-
|
|
58132
|
+
// Opening the panel must NOT pre-flip the row to 'custom' \u2014 that left a
|
|
58133
|
+
// row the user never actually shared stuck at "custom (0)". The first
|
|
58134
|
+
// grant flips it to custom server-side (lattice_grant_row); revoking the
|
|
58135
|
+
// last leaves it custom-with-0-grantees, which now reads as private. So
|
|
58136
|
+
// just load the member checklist.
|
|
58137
|
+
var ensure = Promise.resolve();
|
|
58034
58138
|
withBusy(detailVisManage, function () {
|
|
58035
58139
|
return ensure.then(function () {
|
|
58036
58140
|
return fetchJson('/api/cloud/members');
|
|
@@ -58064,8 +58168,13 @@ var appJs = `
|
|
|
58064
58168
|
var at = list.indexOf(role);
|
|
58065
58169
|
if (cb.checked && at === -1) list.push(role);
|
|
58066
58170
|
if (!cb.checked && at !== -1) list.splice(at, 1);
|
|
58171
|
+
// The first grant flips the row to custom server-side; mirror
|
|
58172
|
+
// that locally so the indicator updates. Revoking the last leaves
|
|
58173
|
+
// visibility 'custom' but effectiveVisibility renders custom-0 as
|
|
58174
|
+
// private, so the label flips back to "Private to you".
|
|
58175
|
+
if (list.length > 0) access.visibility = 'custom';
|
|
58067
58176
|
var infoEl = content.querySelector('#detail-vis-info');
|
|
58068
|
-
if (infoEl) infoEl.textContent =
|
|
58177
|
+
if (infoEl) infoEl.textContent = visInfoLabel(access);
|
|
58069
58178
|
invalidate(tableName);
|
|
58070
58179
|
}).catch(function (e) {
|
|
58071
58180
|
cb.checked = !cb.checked; // revert the failed change
|
|
@@ -58073,10 +58182,8 @@ var appJs = `
|
|
|
58073
58182
|
}).then(function () { cb.disabled = false; });
|
|
58074
58183
|
});
|
|
58075
58184
|
});
|
|
58076
|
-
|
|
58077
|
-
|
|
58078
|
-
if (infoEl) infoEl.textContent = 'Shared with specific people (' + (access.grantees || []).length + ')';
|
|
58079
|
-
}
|
|
58185
|
+
var infoEl = content.querySelector('#detail-vis-info');
|
|
58186
|
+
if (infoEl) infoEl.textContent = visInfoLabel(access);
|
|
58080
58187
|
}).catch(function (e) { showToast('Could not load members: ' + e.message, {}); });
|
|
58081
58188
|
});
|
|
58082
58189
|
});
|
|
@@ -60187,6 +60294,29 @@ var appJs = `
|
|
|
60187
60294
|
var EYE_SVG =
|
|
60188
60295
|
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
|
|
60189
60296
|
'<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>';
|
|
60297
|
+
// A custom row with zero grantees is effectively PRIVATE (RLS shows it only to
|
|
60298
|
+
// the owner), so it must read as private, not "Shared with specific people
|
|
60299
|
+
// (0)". Only collapse for the OWNER's own view, where the grantees list is
|
|
60300
|
+
// authoritative \u2014 a member viewing a row shared WITH them gets custom with no
|
|
60301
|
+
// grantees list and must still read as "Shared with you".
|
|
60302
|
+
function effectiveVisibility(access) {
|
|
60303
|
+
if (!access || !access.visibility) return 'private';
|
|
60304
|
+
if (access.visibility === 'custom' && access.ownedByMe &&
|
|
60305
|
+
(!access.grantees || access.grantees.length === 0)) {
|
|
60306
|
+
return 'private';
|
|
60307
|
+
}
|
|
60308
|
+
return access.visibility;
|
|
60309
|
+
}
|
|
60310
|
+
// Owner-facing status label for a row's sharing, honoring effectiveVisibility
|
|
60311
|
+
// (so custom-0 reads as "Private to you") and appending the grantee count only
|
|
60312
|
+
// for a genuine specific-people share.
|
|
60313
|
+
function visInfoLabel(access) {
|
|
60314
|
+
var v = effectiveVisibility(access);
|
|
60315
|
+
var map = { everyone: 'Visible to everyone', private: 'Private to you', custom: 'Shared with specific people' };
|
|
60316
|
+
var s = map[v] || '';
|
|
60317
|
+
if (v === 'custom' && access && access.grantees) s += ' (' + access.grantees.length + ')';
|
|
60318
|
+
return s;
|
|
60319
|
+
}
|
|
60190
60320
|
// Shared per-row lock/eye indicator, reused on the entity-detail header and the
|
|
60191
60321
|
// fs card tiles so the meaning is consistent. The access arg is the server-
|
|
60192
60322
|
// attached row._access (visibility + ownedByMe); returns empty when absent (a
|
|
@@ -60194,7 +60324,7 @@ var appJs = `
|
|
|
60194
60324
|
// A hover tooltip spells out what the lock/eye means (state + ownership aware).
|
|
60195
60325
|
function visIndicator(access, extraClass) {
|
|
60196
60326
|
if (!access || !access.visibility) return '';
|
|
60197
|
-
var vis = access
|
|
60327
|
+
var vis = effectiveVisibility(access);
|
|
60198
60328
|
var tip = vis === 'private'
|
|
60199
60329
|
? 'Private \u2014 only you can see this'
|
|
60200
60330
|
: vis === 'custom'
|
|
@@ -61983,7 +62113,6 @@ var appJs = `
|
|
|
61983
62113
|
|
|
61984
62114
|
|
|
61985
62115
|
// ============ AI assistant rail (2.0) ============
|
|
61986
|
-
var feedSource = null;
|
|
61987
62116
|
var FEED_ICONS = {
|
|
61988
62117
|
insert: '\u2795', update: '\u270F\uFE0F', delete: '\u{1F5D1}',
|
|
61989
62118
|
link: '\u{1F517}', unlink: '\u26D3', undo: '\u21B6', redo: '\u21B7', schema: '\u{1F6E0}',
|
|
@@ -62236,32 +62365,13 @@ var appJs = `
|
|
|
62236
62365
|
else { card.timeEl.textContent = formatElapsed(Math.max(0, evMs - startMs)); }
|
|
62237
62366
|
}
|
|
62238
62367
|
}
|
|
62239
|
-
|
|
62240
|
-
|
|
62241
|
-
|
|
62242
|
-
|
|
62243
|
-
|
|
62244
|
-
|
|
62245
|
-
|
|
62246
|
-
feedSource.addEventListener('feed', function (ev) {
|
|
62247
|
-
var data;
|
|
62248
|
-
try { data = JSON.parse(ev.data); } catch (_) { return; /* ignore malformed */ }
|
|
62249
|
-
try { renderFeedItem(data); } catch (_) { /* render best-effort */ }
|
|
62250
|
-
// Refresh on ANY data mutation, not just schema/new-table events. The
|
|
62251
|
-
// local feed bus delivers every insert/update/delete/link even when
|
|
62252
|
-
// there's no realtime broker (SQLite/local), so this is what makes the
|
|
62253
|
-
// home dashboard counts AND the open entity view live-update without a
|
|
62254
|
-
// manual reload (previously only schema ops or brand-new tables did).
|
|
62255
|
-
// scheduleRealtimeRefresh is debounced (200ms) so a burst from one
|
|
62256
|
-
// ingest still coalesces into a single refetch \u2014 and on Postgres/cloud
|
|
62257
|
-
// it shares that debounce with the realtime 'change' handler (no double
|
|
62258
|
-
// fetch). /api/entities batches its row counts into one query, not N.
|
|
62259
|
-
if (data && (data.table || data.op === 'schema')) {
|
|
62260
|
-
scheduleRealtimeRefresh();
|
|
62261
|
-
}
|
|
62262
|
-
});
|
|
62263
|
-
// EventSource auto-reconnects on error; no extra handling needed.
|
|
62264
|
-
}
|
|
62368
|
+
// Feed events arrive over the multiplexed /api/stream WebSocket and are
|
|
62369
|
+
// handled in dispatchStreamMessage('feed', \u2026): renderFeedItem() paints the
|
|
62370
|
+
// card, then scheduleRealtimeRefresh() refetches on ANY data mutation (the
|
|
62371
|
+
// local feed bus delivers every insert/update/delete/link even with no
|
|
62372
|
+
// realtime broker, so this is what live-updates the dashboard counts and the
|
|
62373
|
+
// open entity view without a manual reload). The 200ms debounce coalesces a
|
|
62374
|
+
// burst into a single refetch and is shared with the realtime 'change' path.
|
|
62265
62375
|
|
|
62266
62376
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
62267
62377
|
// Assistant rail resize \u2014 drag the left edge, clamp, persist.
|
|
@@ -62957,6 +63067,16 @@ var analyticsJs = `
|
|
|
62957
63067
|
page_title: t,
|
|
62958
63068
|
});
|
|
62959
63069
|
},
|
|
63070
|
+
// Set the GA user_id to an OPAQUE per-user hash so active users are
|
|
63071
|
+
// deduplicated across sessions/devices (without it, unique users \u2248 events).
|
|
63072
|
+
// Accepts ONLY a 64-char hex digest \u2014 a raw email or any other PII is
|
|
63073
|
+
// rejected (never sent), preserving the privacy contract above. The caller
|
|
63074
|
+
// hashes the email; this module never sees the plaintext.
|
|
63075
|
+
setUser: function (idHash) {
|
|
63076
|
+
if (!consent || !loaded) return;
|
|
63077
|
+
if (typeof idHash !== 'string' || !/^[a-f0-9]{64}$/.test(idHash)) return;
|
|
63078
|
+
gtag('config', MEASUREMENT_ID, { user_id: idHash });
|
|
63079
|
+
},
|
|
62960
63080
|
};
|
|
62961
63081
|
})();
|
|
62962
63082
|
`;
|
|
@@ -66366,19 +66486,36 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
|
|
|
66366
66486
|
}
|
|
66367
66487
|
let memberOpen = false;
|
|
66368
66488
|
const maskedReadViews = /* @__PURE__ */ new Map();
|
|
66489
|
+
const discoveredJunctions = /* @__PURE__ */ new Set();
|
|
66369
66490
|
if (db.getDialect() === "postgres") {
|
|
66370
66491
|
const peek = new Lattice({ config: configPath }, { encryptionKey });
|
|
66371
66492
|
try {
|
|
66372
66493
|
await peek.init({ introspectOnly: true });
|
|
66373
|
-
|
|
66374
|
-
|
|
66494
|
+
const [rlsInstalled, canManage] = await Promise.all([
|
|
66495
|
+
cloudRlsInstalled(peek),
|
|
66496
|
+
canManageRoles(peek)
|
|
66497
|
+
]);
|
|
66498
|
+
if (rlsInstalled) {
|
|
66499
|
+
memberOpen = !canManage;
|
|
66375
66500
|
if (memberOpen) {
|
|
66376
66501
|
const declared = new Set(db.getRegisteredTableNames());
|
|
66377
|
-
const discovered = await
|
|
66502
|
+
const [discovered, viewsRaw] = await Promise.all([
|
|
66503
|
+
discoverCloudTables(peek),
|
|
66504
|
+
allAsyncOrSync(
|
|
66505
|
+
peek.adapter,
|
|
66506
|
+
`SELECT table_name AS name FROM information_schema.views
|
|
66507
|
+
WHERE table_schema = current_schema() AND table_name LIKE '%\\_v' ESCAPE '\\'`
|
|
66508
|
+
)
|
|
66509
|
+
]);
|
|
66510
|
+
const views = viewsRaw;
|
|
66378
66511
|
const knownTables = /* @__PURE__ */ new Set([...declared, ...discovered.map((t8) => t8.name)]);
|
|
66379
66512
|
for (const t8 of discovered) {
|
|
66380
66513
|
if (declared.has(t8.name)) continue;
|
|
66381
66514
|
if (t8.columns.length === 0) continue;
|
|
66515
|
+
if (isJunctionByColumns(t8.columns)) {
|
|
66516
|
+
discoveredJunctions.add(t8.name);
|
|
66517
|
+
continue;
|
|
66518
|
+
}
|
|
66382
66519
|
db.define(t8.name, {
|
|
66383
66520
|
columns: Object.fromEntries(t8.columns.map((c6) => [c6, "TEXT"])),
|
|
66384
66521
|
...t8.pk.length > 0 ? { primaryKey: t8.pk.length === 1 ? t8.pk[0] : t8.pk } : {},
|
|
@@ -66386,11 +66523,6 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
|
|
|
66386
66523
|
outputFile: `${t8.name}/.lattice/${t8.name}.md`
|
|
66387
66524
|
});
|
|
66388
66525
|
}
|
|
66389
|
-
const views = await allAsyncOrSync(
|
|
66390
|
-
peek.adapter,
|
|
66391
|
-
`SELECT table_name AS name FROM information_schema.views
|
|
66392
|
-
WHERE table_schema = current_schema() AND table_name LIKE '%\\_v' ESCAPE '\\'`
|
|
66393
|
-
);
|
|
66394
66526
|
for (const { name } of views) {
|
|
66395
66527
|
const base = name.slice(0, -2);
|
|
66396
66528
|
if (knownTables.has(base)) maskedReadViews.set(base, name);
|
|
@@ -66427,9 +66559,12 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
|
|
|
66427
66559
|
if (name.startsWith("__lattice_") || name.startsWith("_lattice_")) continue;
|
|
66428
66560
|
validTables.add(name);
|
|
66429
66561
|
}
|
|
66430
|
-
const junctionTables = new Set(
|
|
66431
|
-
getGuiEntities(configPath, outputDir).tables.filter(isJunctionTable).map((t8) => t8.name)
|
|
66432
|
-
|
|
66562
|
+
const junctionTables = /* @__PURE__ */ new Set([
|
|
66563
|
+
...getGuiEntities(configPath, outputDir).tables.filter(isJunctionTable).map((t8) => t8.name),
|
|
66564
|
+
// Member-discovered junctions (classified from the physical shape above);
|
|
66565
|
+
// empty for an owner/local open.
|
|
66566
|
+
...discoveredJunctions
|
|
66567
|
+
]);
|
|
66433
66568
|
const entityContextByTable = db.entityContexts();
|
|
66434
66569
|
const manifest = readManifest(outputDir);
|
|
66435
66570
|
const softDeletable = /* @__PURE__ */ new Set();
|
|
@@ -66630,6 +66765,9 @@ async function changeVisibleToActiveRole(db, payload) {
|
|
|
66630
66765
|
function isDeleteOp(op) {
|
|
66631
66766
|
return op === "delete" || op === "DELETE";
|
|
66632
66767
|
}
|
|
66768
|
+
function isFeedHiddenTable(t8) {
|
|
66769
|
+
return t8.startsWith("_lattice") || t8.startsWith("__lattice") || isInternalNativeEntity(t8);
|
|
66770
|
+
}
|
|
66633
66771
|
function readRelationFor(active, table) {
|
|
66634
66772
|
return active.maskedReadViews.get(table) ?? table;
|
|
66635
66773
|
}
|
|
@@ -67011,158 +67149,10 @@ async function startGuiServer(options) {
|
|
|
67011
67149
|
sendJson(res, { mode, state: active.realtime?.state() ?? "local", connected });
|
|
67012
67150
|
return;
|
|
67013
67151
|
}
|
|
67014
|
-
if (method === "GET" && pathname === "/api/realtime/stream") {
|
|
67015
|
-
res.writeHead(200, {
|
|
67016
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
67017
|
-
"cache-control": "no-store, no-transform",
|
|
67018
|
-
connection: "keep-alive",
|
|
67019
|
-
"x-accel-buffering": "no"
|
|
67020
|
-
});
|
|
67021
|
-
const broker = active.realtime;
|
|
67022
|
-
const initialMode = broker ? "cloud" : "local";
|
|
67023
|
-
const writeEvent = (event, data) => {
|
|
67024
|
-
try {
|
|
67025
|
-
res.write(`event: ${event}
|
|
67026
|
-
data: ${JSON.stringify(data)}
|
|
67027
|
-
|
|
67028
|
-
`);
|
|
67029
|
-
} catch {
|
|
67030
|
-
}
|
|
67031
|
-
};
|
|
67032
|
-
writeEvent("state", {
|
|
67033
|
-
mode: initialMode,
|
|
67034
|
-
state: broker?.state() ?? "local"
|
|
67035
|
-
});
|
|
67036
|
-
const keepalive = setInterval(() => {
|
|
67037
|
-
try {
|
|
67038
|
-
res.write(`: keepalive
|
|
67039
|
-
|
|
67040
|
-
`);
|
|
67041
|
-
} catch {
|
|
67042
|
-
}
|
|
67043
|
-
}, 25e3);
|
|
67044
|
-
const offState = broker?.subscribeState((state2) => {
|
|
67045
|
-
writeEvent("state", { mode: "cloud", state: state2 });
|
|
67046
|
-
});
|
|
67047
|
-
const offPayload = broker?.subscribePayload((payload) => {
|
|
67048
|
-
if (activeRef !== active) return;
|
|
67049
|
-
void changeVisibleToActiveRole(active.db, payload).then((visible) => {
|
|
67050
|
-
if (!visible) return;
|
|
67051
|
-
const out = isDeleteOp(payload.op) ? { ...payload, owner_role: null } : payload;
|
|
67052
|
-
writeEvent("change", out);
|
|
67053
|
-
});
|
|
67054
|
-
});
|
|
67055
|
-
const cleanup = () => {
|
|
67056
|
-
clearInterval(keepalive);
|
|
67057
|
-
if (offState) offState();
|
|
67058
|
-
if (offPayload) offPayload();
|
|
67059
|
-
};
|
|
67060
|
-
req.on("close", cleanup);
|
|
67061
|
-
req.on("error", cleanup);
|
|
67062
|
-
return;
|
|
67063
|
-
}
|
|
67064
|
-
if (method === "GET" && pathname === "/api/feed/stream") {
|
|
67065
|
-
res.writeHead(200, {
|
|
67066
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
67067
|
-
"cache-control": "no-store, no-transform",
|
|
67068
|
-
connection: "keep-alive",
|
|
67069
|
-
"x-accel-buffering": "no"
|
|
67070
|
-
});
|
|
67071
|
-
const writeFeed = (data) => {
|
|
67072
|
-
try {
|
|
67073
|
-
res.write(`event: feed
|
|
67074
|
-
data: ${JSON.stringify(data)}
|
|
67075
|
-
|
|
67076
|
-
`);
|
|
67077
|
-
} catch {
|
|
67078
|
-
}
|
|
67079
|
-
};
|
|
67080
|
-
const keepalive = setInterval(() => {
|
|
67081
|
-
try {
|
|
67082
|
-
res.write(`: keepalive
|
|
67083
|
-
|
|
67084
|
-
`);
|
|
67085
|
-
} catch {
|
|
67086
|
-
}
|
|
67087
|
-
}, 25e3);
|
|
67088
|
-
const recentSelf = /* @__PURE__ */ new Map();
|
|
67089
|
-
const isFeedHiddenTable = (t8) => t8.startsWith("_lattice") || t8.startsWith("__lattice") || isInternalNativeEntity(t8);
|
|
67090
|
-
const offFeed = active.feed.subscribe((e6) => {
|
|
67091
|
-
if (e6.table && isFeedHiddenTable(e6.table)) return;
|
|
67092
|
-
recentSelf.set(`${e6.table ?? ""}:${e6.rowId ?? ""}:${e6.op}`, Date.now());
|
|
67093
|
-
writeFeed(e6);
|
|
67094
|
-
});
|
|
67095
|
-
const offBroker = active.realtime?.subscribePayload((p3) => {
|
|
67096
|
-
const op = feedOpForChange(p3.op);
|
|
67097
|
-
if (!op || !p3.table_name || isFeedHiddenTable(p3.table_name)) return;
|
|
67098
|
-
const tableName = p3.table_name;
|
|
67099
|
-
const key = `${tableName}:${p3.pk ?? ""}:${op}`;
|
|
67100
|
-
const seen = recentSelf.get(key);
|
|
67101
|
-
if (seen && Date.now() - seen < 5e3) return;
|
|
67102
|
-
if (activeRef !== active) return;
|
|
67103
|
-
void changeVisibleToActiveRole(active.db, p3).then((visible) => {
|
|
67104
|
-
if (!visible) return;
|
|
67105
|
-
writeFeed({
|
|
67106
|
-
seq: p3.seq,
|
|
67107
|
-
table: tableName,
|
|
67108
|
-
op,
|
|
67109
|
-
rowId: p3.pk,
|
|
67110
|
-
source: "cli",
|
|
67111
|
-
actor: isDeleteOp(p3.op) ? void 0 : p3.owner_role ?? void 0,
|
|
67112
|
-
ts: p3.created_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
67113
|
-
summary: `${op} on ${tableName} (another client)`
|
|
67114
|
-
});
|
|
67115
|
-
});
|
|
67116
|
-
});
|
|
67117
|
-
const cleanup = () => {
|
|
67118
|
-
clearInterval(keepalive);
|
|
67119
|
-
offFeed();
|
|
67120
|
-
if (offBroker) offBroker();
|
|
67121
|
-
};
|
|
67122
|
-
req.on("close", cleanup);
|
|
67123
|
-
req.on("error", cleanup);
|
|
67124
|
-
return;
|
|
67125
|
-
}
|
|
67126
67152
|
if (method === "GET" && pathname === "/api/render/status") {
|
|
67127
67153
|
sendJson(res, active.renderState);
|
|
67128
67154
|
return;
|
|
67129
67155
|
}
|
|
67130
|
-
if (method === "GET" && pathname === "/api/render/progress") {
|
|
67131
|
-
res.writeHead(200, {
|
|
67132
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
67133
|
-
"cache-control": "no-store, no-transform",
|
|
67134
|
-
connection: "keep-alive",
|
|
67135
|
-
"x-accel-buffering": "no"
|
|
67136
|
-
});
|
|
67137
|
-
const writeEvent = (event, data) => {
|
|
67138
|
-
try {
|
|
67139
|
-
res.write(`event: ${event}
|
|
67140
|
-
data: ${JSON.stringify(data)}
|
|
67141
|
-
|
|
67142
|
-
`);
|
|
67143
|
-
} catch {
|
|
67144
|
-
}
|
|
67145
|
-
};
|
|
67146
|
-
writeEvent("snapshot", active.renderState);
|
|
67147
|
-
const keepalive = setInterval(() => {
|
|
67148
|
-
try {
|
|
67149
|
-
res.write(`: keepalive
|
|
67150
|
-
|
|
67151
|
-
`);
|
|
67152
|
-
} catch {
|
|
67153
|
-
}
|
|
67154
|
-
}, 25e3);
|
|
67155
|
-
const offProgress = active.renderProgress.subscribe((e6) => {
|
|
67156
|
-
writeEvent("progress", e6);
|
|
67157
|
-
});
|
|
67158
|
-
const cleanup = () => {
|
|
67159
|
-
clearInterval(keepalive);
|
|
67160
|
-
offProgress();
|
|
67161
|
-
};
|
|
67162
|
-
req.on("close", cleanup);
|
|
67163
|
-
req.on("error", cleanup);
|
|
67164
|
-
return;
|
|
67165
|
-
}
|
|
67166
67156
|
if (method === "GET" && pathname === "/api/project") {
|
|
67167
67157
|
sendJson(res, getGuiProject(active.configPath, active.outputDir));
|
|
67168
67158
|
return;
|
|
@@ -68680,6 +68670,107 @@ ${e6.stack ?? ""}`
|
|
|
68680
68670
|
}
|
|
68681
68671
|
})();
|
|
68682
68672
|
});
|
|
68673
|
+
const wss = new import_ws.WebSocketServer({ noServer: true });
|
|
68674
|
+
const handleEventStream = (ws) => {
|
|
68675
|
+
const bound = activeRef;
|
|
68676
|
+
const send = (type, data) => {
|
|
68677
|
+
if (ws.readyState !== import_ws.WebSocket.OPEN) return;
|
|
68678
|
+
try {
|
|
68679
|
+
ws.send(JSON.stringify({ type, data }));
|
|
68680
|
+
} catch {
|
|
68681
|
+
}
|
|
68682
|
+
};
|
|
68683
|
+
const broker = bound?.realtime ?? null;
|
|
68684
|
+
send("realtime-state", { mode: broker ? "cloud" : "local", state: broker?.state() ?? "local" });
|
|
68685
|
+
if (bound) send("render-snapshot", bound.renderState);
|
|
68686
|
+
const offs = [];
|
|
68687
|
+
if (bound) {
|
|
68688
|
+
if (broker) {
|
|
68689
|
+
offs.push(
|
|
68690
|
+
broker.subscribeState((state2) => {
|
|
68691
|
+
send("realtime-state", { mode: "cloud", state: state2 });
|
|
68692
|
+
})
|
|
68693
|
+
);
|
|
68694
|
+
offs.push(
|
|
68695
|
+
broker.subscribePayload((payload) => {
|
|
68696
|
+
if (activeRef !== bound) return;
|
|
68697
|
+
void changeVisibleToActiveRole(bound.db, payload).then((visible) => {
|
|
68698
|
+
if (!visible) return;
|
|
68699
|
+
const out = isDeleteOp(payload.op) ? { ...payload, owner_role: null } : payload;
|
|
68700
|
+
send("realtime-change", out);
|
|
68701
|
+
});
|
|
68702
|
+
})
|
|
68703
|
+
);
|
|
68704
|
+
}
|
|
68705
|
+
const recentSelf = /* @__PURE__ */ new Map();
|
|
68706
|
+
offs.push(
|
|
68707
|
+
bound.feed.subscribe((e6) => {
|
|
68708
|
+
if (e6.table && isFeedHiddenTable(e6.table)) return;
|
|
68709
|
+
recentSelf.set(`${e6.table ?? ""}:${e6.rowId ?? ""}:${e6.op}`, Date.now());
|
|
68710
|
+
send("feed", e6);
|
|
68711
|
+
})
|
|
68712
|
+
);
|
|
68713
|
+
if (broker) {
|
|
68714
|
+
offs.push(
|
|
68715
|
+
broker.subscribePayload((p3) => {
|
|
68716
|
+
const op = feedOpForChange(p3.op);
|
|
68717
|
+
if (!op || !p3.table_name || isFeedHiddenTable(p3.table_name)) return;
|
|
68718
|
+
const tableName = p3.table_name;
|
|
68719
|
+
const key = `${tableName}:${p3.pk ?? ""}:${op}`;
|
|
68720
|
+
const seen = recentSelf.get(key);
|
|
68721
|
+
if (seen && Date.now() - seen < 5e3) return;
|
|
68722
|
+
if (activeRef !== bound) return;
|
|
68723
|
+
void changeVisibleToActiveRole(bound.db, p3).then((visible) => {
|
|
68724
|
+
if (!visible) return;
|
|
68725
|
+
send("feed", {
|
|
68726
|
+
seq: p3.seq,
|
|
68727
|
+
table: tableName,
|
|
68728
|
+
op,
|
|
68729
|
+
rowId: p3.pk,
|
|
68730
|
+
source: "cli",
|
|
68731
|
+
actor: isDeleteOp(p3.op) ? void 0 : p3.owner_role ?? void 0,
|
|
68732
|
+
ts: p3.created_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
68733
|
+
summary: `${op} on ${tableName} (another client)`
|
|
68734
|
+
});
|
|
68735
|
+
});
|
|
68736
|
+
})
|
|
68737
|
+
);
|
|
68738
|
+
}
|
|
68739
|
+
offs.push(
|
|
68740
|
+
bound.renderProgress.subscribe((e6) => {
|
|
68741
|
+
send("render-progress", e6);
|
|
68742
|
+
})
|
|
68743
|
+
);
|
|
68744
|
+
}
|
|
68745
|
+
const keepalive = setInterval(() => {
|
|
68746
|
+
if (ws.readyState !== import_ws.WebSocket.OPEN) return;
|
|
68747
|
+
try {
|
|
68748
|
+
ws.ping();
|
|
68749
|
+
} catch {
|
|
68750
|
+
}
|
|
68751
|
+
}, 25e3);
|
|
68752
|
+
const cleanup = () => {
|
|
68753
|
+
clearInterval(keepalive);
|
|
68754
|
+
for (const off of offs) {
|
|
68755
|
+
try {
|
|
68756
|
+
off();
|
|
68757
|
+
} catch {
|
|
68758
|
+
}
|
|
68759
|
+
}
|
|
68760
|
+
};
|
|
68761
|
+
ws.on("close", cleanup);
|
|
68762
|
+
ws.on("error", cleanup);
|
|
68763
|
+
};
|
|
68764
|
+
server.on("upgrade", (req, socket, head) => {
|
|
68765
|
+
const { pathname } = new URL(req.url ?? "/", `http://${host}`);
|
|
68766
|
+
if (pathname !== "/api/stream") {
|
|
68767
|
+
socket.destroy();
|
|
68768
|
+
return;
|
|
68769
|
+
}
|
|
68770
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
68771
|
+
handleEventStream(ws);
|
|
68772
|
+
});
|
|
68773
|
+
});
|
|
68683
68774
|
const port = await listenWithPortFallback(server, startPort, host);
|
|
68684
68775
|
if (activeRef) startBackgroundRender(activeRef);
|
|
68685
68776
|
const displayHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
|
|
@@ -68690,6 +68781,13 @@ ${e6.stack ?? ""}`
|
|
|
68690
68781
|
port,
|
|
68691
68782
|
url,
|
|
68692
68783
|
close: () => new Promise((resolveClose, reject) => {
|
|
68784
|
+
for (const client of wss.clients) {
|
|
68785
|
+
try {
|
|
68786
|
+
client.terminate();
|
|
68787
|
+
} catch {
|
|
68788
|
+
}
|
|
68789
|
+
}
|
|
68790
|
+
wss.close();
|
|
68693
68791
|
server.close((err) => {
|
|
68694
68792
|
if (err) {
|
|
68695
68793
|
reject(err);
|