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.js
CHANGED
|
@@ -48535,6 +48535,10 @@ function isJunctionTable(table) {
|
|
|
48535
48535
|
const fkCols = new Set(belongsTo.map((r6) => r6.foreignKey));
|
|
48536
48536
|
return table.columns.every((c6) => fkCols.has(c6) || JUNCTION_ALLOWED_NONFK.has(c6));
|
|
48537
48537
|
}
|
|
48538
|
+
function isJunctionByColumns(columns) {
|
|
48539
|
+
const payload = columns.filter((c6) => !JUNCTION_ALLOWED_NONFK.has(c6));
|
|
48540
|
+
return payload.length === 2 && payload.every((c6) => c6.endsWith("_id"));
|
|
48541
|
+
}
|
|
48538
48542
|
function fileJunctions(configPath, outputDir) {
|
|
48539
48543
|
const out = [];
|
|
48540
48544
|
for (const t8 of getGuiEntities(configPath, outputDir).tables) {
|
|
@@ -51871,6 +51875,16 @@ var init_ingest_url = __esm({
|
|
|
51871
51875
|
});
|
|
51872
51876
|
|
|
51873
51877
|
// src/gui/ai/dispatch.ts
|
|
51878
|
+
function visibilityDenialReason(opts) {
|
|
51879
|
+
if (opts.kind === "table") {
|
|
51880
|
+
return opts.canManageTableDefault ? null : "Only the workspace owner can change a table's default sharing.";
|
|
51881
|
+
}
|
|
51882
|
+
if (!opts.rowAccess) return "That record was not found, or is not visible to you.";
|
|
51883
|
+
if (!opts.rowAccess.ownedByMe) {
|
|
51884
|
+
return "You do not own this record, so you cannot change its sharing \u2014 only its owner can.";
|
|
51885
|
+
}
|
|
51886
|
+
return null;
|
|
51887
|
+
}
|
|
51874
51888
|
async function secretColumnsFor(db, table) {
|
|
51875
51889
|
try {
|
|
51876
51890
|
const rows = await db.query("_lattice_gui_column_meta", {
|
|
@@ -52096,6 +52110,14 @@ async function executeFunction(ctx, name, args) {
|
|
|
52096
52110
|
};
|
|
52097
52111
|
}
|
|
52098
52112
|
const id = typeof args.id === "string" && args.id ? args.id : void 0;
|
|
52113
|
+
const denial = id ? visibilityDenialReason({
|
|
52114
|
+
kind: "row",
|
|
52115
|
+
rowAccess: (await rowAccessSummaries(ctx.db, table, [id])).get(id)
|
|
52116
|
+
}) : visibilityDenialReason({
|
|
52117
|
+
kind: "table",
|
|
52118
|
+
canManageTableDefault: await canManageRoles(ctx.db)
|
|
52119
|
+
});
|
|
52120
|
+
if (denial) return { ok: false, error: denial };
|
|
52099
52121
|
try {
|
|
52100
52122
|
if (id) {
|
|
52101
52123
|
await setRowVisibility(ctx.db, table, id, visibility);
|
|
@@ -52259,6 +52281,7 @@ var init_dispatch = __esm({
|
|
|
52259
52281
|
init_column_descriptions();
|
|
52260
52282
|
init_members();
|
|
52261
52283
|
init_table_policy();
|
|
52284
|
+
init_cloud_connect();
|
|
52262
52285
|
init_dedup_service();
|
|
52263
52286
|
DISPATCHABLE = /* @__PURE__ */ new Set([
|
|
52264
52287
|
"list_entities",
|
|
@@ -53806,6 +53829,44 @@ async function reconcileCloudMemberAccess(db) {
|
|
|
53806
53829
|
);
|
|
53807
53830
|
}
|
|
53808
53831
|
}
|
|
53832
|
+
const memberSystemGrants = [
|
|
53833
|
+
["_lattice_gui_meta", "SELECT, INSERT, UPDATE"],
|
|
53834
|
+
["_lattice_gui_column_meta", "SELECT, INSERT, UPDATE"],
|
|
53835
|
+
["_lattice_gui_audit", "SELECT, INSERT"],
|
|
53836
|
+
["__lattice_user_identity", "SELECT, INSERT, UPDATE"]
|
|
53837
|
+
];
|
|
53838
|
+
for (const [tbl, privs] of memberSystemGrants) {
|
|
53839
|
+
await runAsyncOrSync(
|
|
53840
|
+
db.adapter,
|
|
53841
|
+
`DO $LATTICE$ BEGIN
|
|
53842
|
+
IF to_regclass('${tbl}') IS NOT NULL THEN
|
|
53843
|
+
EXECUTE 'GRANT ${privs} ON "${tbl}" TO ${MEMBER_GROUP}';
|
|
53844
|
+
END IF;
|
|
53845
|
+
END $LATTICE$`
|
|
53846
|
+
);
|
|
53847
|
+
}
|
|
53848
|
+
try {
|
|
53849
|
+
await runAsyncOrSync(
|
|
53850
|
+
db.adapter,
|
|
53851
|
+
`GRANT EXECUTE ON FUNCTION json_extract(text, text), strftime(text, text) TO ${MEMBER_GROUP}`
|
|
53852
|
+
);
|
|
53853
|
+
} catch (err) {
|
|
53854
|
+
console.warn(
|
|
53855
|
+
"[reconcileCloudMemberAccess] could not grant EXECUTE on polyfills (will retry next open):",
|
|
53856
|
+
err instanceof Error ? err.message : String(err)
|
|
53857
|
+
);
|
|
53858
|
+
}
|
|
53859
|
+
for (const table of registered) {
|
|
53860
|
+
if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) continue;
|
|
53861
|
+
const cols = db.getRegisteredColumns(table);
|
|
53862
|
+
if (cols && !("deleted_at" in cols)) {
|
|
53863
|
+
const q3 = `"${table.replace(/"/g, '""')}"`;
|
|
53864
|
+
await runAsyncOrSync(
|
|
53865
|
+
db.adapter,
|
|
53866
|
+
`ALTER TABLE ${q3} ADD COLUMN IF NOT EXISTS "deleted_at" TEXT`
|
|
53867
|
+
);
|
|
53868
|
+
}
|
|
53869
|
+
}
|
|
53809
53870
|
}
|
|
53810
53871
|
async function secureNewCloudTable(db, table, pk) {
|
|
53811
53872
|
if (db.getDialect() !== "postgres") return;
|
|
@@ -53831,14 +53892,6 @@ async function secureCloud(db) {
|
|
|
53831
53892
|
await secureNewCloudTable(db, table, db.getPrimaryKey(table));
|
|
53832
53893
|
}
|
|
53833
53894
|
await reconcileCloudMemberAccess(db);
|
|
53834
|
-
await runAsyncOrSync(
|
|
53835
|
-
db.adapter,
|
|
53836
|
-
`DO $LATTICE$ BEGIN
|
|
53837
|
-
IF to_regclass('__lattice_user_identity') IS NOT NULL THEN
|
|
53838
|
-
EXECUTE 'GRANT SELECT, INSERT, UPDATE ON "__lattice_user_identity" TO ${MEMBER_GROUP}';
|
|
53839
|
-
END IF;
|
|
53840
|
-
END $LATTICE$`
|
|
53841
|
-
);
|
|
53842
53895
|
}
|
|
53843
53896
|
|
|
53844
53897
|
// src/index.ts
|
|
@@ -54130,6 +54183,7 @@ init_summarize();
|
|
|
54130
54183
|
// src/gui/server.ts
|
|
54131
54184
|
import { createServer } from "http";
|
|
54132
54185
|
import { spawn as spawn2 } from "child_process";
|
|
54186
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
54133
54187
|
import {
|
|
54134
54188
|
existsSync as existsSync21,
|
|
54135
54189
|
mkdirSync as mkdirSync9,
|
|
@@ -56004,6 +56058,31 @@ var appJs = `
|
|
|
56004
56058
|
// Boot analytics with the resolved consent (no network contact when off),
|
|
56005
56059
|
// then record the session open. advanced_mode is a boolean \u2014 safe to send.
|
|
56006
56060
|
if (window.LatticeGA) window.LatticeGA.init(state.analyticsEffective);
|
|
56061
|
+
// Deduplicate unique users in GA: set the GA user_id to a SHA-256 hash of
|
|
56062
|
+
// the operator's email. Anonymized \u2014 the plaintext is hashed in-browser and
|
|
56063
|
+
// never sent (analytics.ts only accepts a hex digest). Without a user_id,
|
|
56064
|
+
// GA counts each session/device as a new user (active-users \u2248 events).
|
|
56065
|
+
// Best-effort + only when analytics consent is on.
|
|
56066
|
+
if (window.LatticeGA && state.analyticsEffective && window.crypto && window.crypto.subtle) {
|
|
56067
|
+
fetchJson('/api/userconfig/identity')
|
|
56068
|
+
.then(function (id) {
|
|
56069
|
+
var email = id && id.email ? String(id.email).trim().toLowerCase() : '';
|
|
56070
|
+
if (!email) return undefined;
|
|
56071
|
+
return window.crypto.subtle
|
|
56072
|
+
.digest('SHA-256', new TextEncoder().encode(email))
|
|
56073
|
+
.then(function (buf) {
|
|
56074
|
+
var hex = Array.prototype.map
|
|
56075
|
+
.call(new Uint8Array(buf), function (b) {
|
|
56076
|
+
return ('0' + b.toString(16)).slice(-2);
|
|
56077
|
+
})
|
|
56078
|
+
.join('');
|
|
56079
|
+
window.LatticeGA.setUser(hex);
|
|
56080
|
+
});
|
|
56081
|
+
})
|
|
56082
|
+
.catch(function () {
|
|
56083
|
+
/* best-effort \u2014 GA still functions without a user_id */
|
|
56084
|
+
});
|
|
56085
|
+
}
|
|
56007
56086
|
gaTrack('app_open', { advanced_mode: advancedMode() });
|
|
56008
56087
|
document.body.classList.toggle('advanced-mode', advancedMode());
|
|
56009
56088
|
wireSettingsDrawer();
|
|
@@ -56015,15 +56094,13 @@ var appJs = `
|
|
|
56015
56094
|
wireHistoryControls();
|
|
56016
56095
|
refreshHistoryState();
|
|
56017
56096
|
renderRoute();
|
|
56018
|
-
|
|
56019
|
-
startRenderProgress();
|
|
56097
|
+
startEventStream();
|
|
56020
56098
|
initSearch();
|
|
56021
56099
|
initLastEdited();
|
|
56022
56100
|
initOffline();
|
|
56023
56101
|
initRailResize();
|
|
56024
56102
|
initRailDrawer();
|
|
56025
56103
|
initRailDragDrop();
|
|
56026
|
-
startFeed();
|
|
56027
56104
|
renderComposer();
|
|
56028
56105
|
initThreadControls();
|
|
56029
56106
|
checkNativeSetup();
|
|
@@ -56039,12 +56116,14 @@ var appJs = `
|
|
|
56039
56116
|
}
|
|
56040
56117
|
|
|
56041
56118
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
56042
|
-
// Realtime
|
|
56043
|
-
//
|
|
56044
|
-
//
|
|
56045
|
-
//
|
|
56119
|
+
// Realtime / feed / render-progress all arrive over ONE multiplexed
|
|
56120
|
+
// WebSocket (/api/stream) \u2014 see startEventStream() below. A single
|
|
56121
|
+
// connection per tab (instead of three SSE streams) keeps the browser's
|
|
56122
|
+
// tiny per-host HTTP connection budget free for data requests, so clicking
|
|
56123
|
+
// objects and switching workspaces stay responsive no matter how many tabs
|
|
56124
|
+
// are open. 'change' events mark the current view dirty and refetch via
|
|
56125
|
+
// afterMutation() (debounced); 'state' events drive the topbar pill.
|
|
56046
56126
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
56047
|
-
var realtimeSource = null;
|
|
56048
56127
|
var realtimePending = null;
|
|
56049
56128
|
// Team-cloud collaboration state. usersById resolves "last edited by"
|
|
56050
56129
|
// names; lastEditedByPk maps "<table>|<pk>" \u2192 { userId, at } from realtime
|
|
@@ -56382,42 +56461,87 @@ var appJs = `
|
|
|
56382
56461
|
afterMutation().catch(function () { /* swallow */ });
|
|
56383
56462
|
}, 200);
|
|
56384
56463
|
}
|
|
56385
|
-
|
|
56386
|
-
|
|
56387
|
-
|
|
56388
|
-
|
|
56389
|
-
|
|
56390
|
-
|
|
56391
|
-
|
|
56392
|
-
|
|
56393
|
-
|
|
56394
|
-
|
|
56395
|
-
|
|
56396
|
-
|
|
56397
|
-
|
|
56398
|
-
|
|
56399
|
-
|
|
56400
|
-
|
|
56401
|
-
if (
|
|
56464
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
56465
|
+
// Multiplexed event stream \u2014 ONE WebSocket carries realtime state/change,
|
|
56466
|
+
// the activity feed, and background-render progress (previously three
|
|
56467
|
+
// separate SSE streams). Holding one connection per tab instead of three
|
|
56468
|
+
// keeps the browser's per-host HTTP budget free for data requests, so the
|
|
56469
|
+
// GUI never freezes when several tabs are open. Each server message is
|
|
56470
|
+
// { type, data }; we demux to the same handlers the SSE listeners used.
|
|
56471
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
56472
|
+
var eventStream = null; // the active WebSocket (or null)
|
|
56473
|
+
var eventStreamReconnect = null; // pending reconnect timer
|
|
56474
|
+
var eventStreamBackoff = 1000; // reconnect delay, grows to a cap
|
|
56475
|
+
var eventStreamClosed = false; // true \u21D2 closed on purpose (switch/teardown), don't reconnect
|
|
56476
|
+
function dispatchStreamMessage(type, data) {
|
|
56477
|
+
if (type === 'realtime-state') {
|
|
56478
|
+
setStatusPill((data && data.mode) || 'local', (data && data.state) || 'local');
|
|
56479
|
+
} else if (type === 'realtime-change') {
|
|
56480
|
+
if (data) onRealtimeChange(data);
|
|
56402
56481
|
scheduleRealtimeRefresh();
|
|
56403
|
-
})
|
|
56404
|
-
|
|
56405
|
-
|
|
56406
|
-
|
|
56482
|
+
} else if (type === 'feed') {
|
|
56483
|
+
try { renderFeedItem(data); } catch (_) { /* render best-effort */ }
|
|
56484
|
+
if (data && (data.table || data.op === 'schema')) scheduleRealtimeRefresh();
|
|
56485
|
+
} else if (type === 'render-snapshot') {
|
|
56486
|
+
if (data) applyRenderSnapshot(data);
|
|
56487
|
+
} else if (type === 'render-progress') {
|
|
56488
|
+
if (data) onRenderEvent(data);
|
|
56489
|
+
}
|
|
56490
|
+
}
|
|
56491
|
+
function scheduleEventStreamReconnect() {
|
|
56492
|
+
if (eventStreamClosed || eventStreamReconnect) return;
|
|
56493
|
+
var delay = eventStreamBackoff;
|
|
56494
|
+
eventStreamBackoff = Math.min(eventStreamBackoff * 2, 15000);
|
|
56495
|
+
eventStreamReconnect = setTimeout(function () {
|
|
56496
|
+
eventStreamReconnect = null;
|
|
56497
|
+
startEventStream();
|
|
56498
|
+
}, delay);
|
|
56499
|
+
}
|
|
56500
|
+
function stopEventStream() {
|
|
56501
|
+
eventStreamClosed = true;
|
|
56502
|
+
if (eventStreamReconnect) { clearTimeout(eventStreamReconnect); eventStreamReconnect = null; }
|
|
56503
|
+
if (eventStream) {
|
|
56504
|
+
// Drop the onclose handler first so an intentional close doesn't trip the
|
|
56505
|
+
// reconnect/disconnect path.
|
|
56506
|
+
try { eventStream.onclose = null; eventStream.close(); } catch (_) { /* ignore */ }
|
|
56507
|
+
eventStream = null;
|
|
56508
|
+
}
|
|
56509
|
+
}
|
|
56510
|
+
function startEventStream() {
|
|
56511
|
+
stopEventStream();
|
|
56512
|
+
eventStreamClosed = false;
|
|
56513
|
+
if (typeof WebSocket === 'undefined') return;
|
|
56514
|
+
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
56515
|
+
var ws;
|
|
56516
|
+
try { ws = new WebSocket(proto + '//' + location.host + '/api/stream'); }
|
|
56517
|
+
catch (_) { scheduleEventStreamReconnect(); return; }
|
|
56518
|
+
eventStream = ws;
|
|
56519
|
+
ws.onopen = function () { eventStreamBackoff = 1000; };
|
|
56520
|
+
ws.onmessage = function (ev) {
|
|
56521
|
+
var msg = null;
|
|
56522
|
+
try { msg = JSON.parse(ev.data); } catch (_) { return; /* ignore malformed */ }
|
|
56523
|
+
if (msg && msg.type) dispatchStreamMessage(msg.type, msg.data);
|
|
56524
|
+
};
|
|
56525
|
+
ws.onerror = function () { /* surfaced via onclose \u2192 reconnect */ };
|
|
56526
|
+
ws.onclose = function () {
|
|
56527
|
+
if (eventStream === ws) eventStream = null;
|
|
56528
|
+
if (eventStreamClosed) return;
|
|
56529
|
+
// Unexpected drop: show the disconnect on the pill and auto-reconnect with
|
|
56530
|
+
// backoff (the server replays state + render snapshot on reconnect).
|
|
56407
56531
|
setStatusPill('cloud', 'disconnected');
|
|
56532
|
+
scheduleEventStreamReconnect();
|
|
56408
56533
|
};
|
|
56409
56534
|
}
|
|
56410
56535
|
|
|
56411
56536
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
56412
|
-
// Background-render progress \u2014
|
|
56413
|
-
// /api/render
|
|
56414
|
-
// context tree in the background;
|
|
56415
|
-
// dashboard cards (bottom-edge bar +
|
|
56416
|
-
// each table completes. Row COUNTS come
|
|
56417
|
-
//
|
|
56418
|
-
//
|
|
56537
|
+
// Background-render progress \u2014 render events arrive over the multiplexed
|
|
56538
|
+
// /api/stream WebSocket (render-snapshot + render-progress). A workspace
|
|
56539
|
+
// opens/switches instantly and renders its context tree in the background;
|
|
56540
|
+
// this paints a per-table % overlay on the dashboard cards (bottom-edge bar +
|
|
56541
|
+
// \u27F3 pill) and dims the row count until each table completes. Row COUNTS come
|
|
56542
|
+
// only from /api/entities \u2014 the render events drive only the transient overlay
|
|
56543
|
+
// and one reconciling refetch on completion.
|
|
56419
56544
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
56420
|
-
var renderSource = null;
|
|
56421
56545
|
// { [table]: { pct, rendered, total, done, error } } \u2014 the live render state,
|
|
56422
56546
|
// re-applied to cards after every dashboard rebuild (drawDashboard wipes the
|
|
56423
56547
|
// DOM overlays but not this map).
|
|
@@ -56530,37 +56654,17 @@ var appJs = `
|
|
|
56530
56654
|
}
|
|
56531
56655
|
}
|
|
56532
56656
|
}
|
|
56533
|
-
|
|
56534
|
-
|
|
56535
|
-
|
|
56536
|
-
|
|
56537
|
-
}
|
|
56538
|
-
if (typeof EventSource === 'undefined') return;
|
|
56539
|
-
// On (re)connect, fetch the single-shot snapshot so a tab that connects
|
|
56540
|
-
// mid- or post-render paints correctly even before the next event. The SSE
|
|
56541
|
-
// endpoint ALSO replays a 'snapshot' event on connect, so both paths agree.
|
|
56542
|
-
fetchJson('/api/render/status').then(applyRenderSnapshot).catch(function () { /* ignore */ });
|
|
56543
|
-
renderSource = new EventSource('/api/render/progress');
|
|
56544
|
-
renderSource.addEventListener('snapshot', function (ev) {
|
|
56545
|
-
var snap = null;
|
|
56546
|
-
try { snap = JSON.parse(ev.data); } catch (_) { /* ignore malformed */ }
|
|
56547
|
-
if (snap) applyRenderSnapshot(snap);
|
|
56548
|
-
});
|
|
56549
|
-
renderSource.addEventListener('progress', function (ev) {
|
|
56550
|
-
var e = null;
|
|
56551
|
-
try { e = JSON.parse(ev.data); } catch (_) { /* ignore malformed */ }
|
|
56552
|
-
if (e) onRenderEvent(e);
|
|
56553
|
-
});
|
|
56554
|
-
// EventSource auto-reconnects on error; the status refetch on the next
|
|
56555
|
-
// open repaints from the authoritative snapshot.
|
|
56556
|
-
}
|
|
56657
|
+
// Render snapshot + progress are applied from the multiplexed /api/stream
|
|
56658
|
+
// WebSocket: the server replays a render-snapshot on connect (so a tab that
|
|
56659
|
+
// connects mid- or post-render paints correctly) and streams render-progress
|
|
56660
|
+
// events thereafter. See dispatchStreamMessage / startEventStream.
|
|
56557
56661
|
|
|
56558
56662
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
56559
56663
|
// Shared activity helpers \u2014 the operation-icon map and relative-time
|
|
56560
56664
|
// formatter, used by Version History and the dashboard activity list. The
|
|
56561
56665
|
// standalone Activity rail was removed in 1.16.1 (redundant with Version
|
|
56562
|
-
// History); multiplayer realtime convergence runs on the
|
|
56563
|
-
//
|
|
56666
|
+
// History); multiplayer realtime convergence runs on the realtime-change
|
|
56667
|
+
// messages of the multiplexed event stream (startEventStream), not on this.
|
|
56564
56668
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
56565
56669
|
var FEED_ICONS = {
|
|
56566
56670
|
insert: '\u2795', update: '\u270F\uFE0F', delete: '\u{1F5D1}',
|
|
@@ -56798,12 +56902,11 @@ var appJs = `
|
|
|
56798
56902
|
if (location.hash !== '#/') location.hash = '#/';
|
|
56799
56903
|
else renderRoute();
|
|
56800
56904
|
loadedTables = {};
|
|
56801
|
-
|
|
56802
|
-
//
|
|
56803
|
-
//
|
|
56804
|
-
// onto this workspace's cards.
|
|
56905
|
+
// A switch swaps the server-side buses to the new workspace; drop the old
|
|
56906
|
+
// workspace's render overlay state and reconnect the multiplexed event
|
|
56907
|
+
// stream so realtime/feed/render all rebind to this workspace.
|
|
56805
56908
|
renderProgress = {};
|
|
56806
|
-
|
|
56909
|
+
startEventStream();
|
|
56807
56910
|
});
|
|
56808
56911
|
}
|
|
56809
56912
|
|
|
@@ -56970,7 +57073,6 @@ var appJs = `
|
|
|
56970
57073
|
// to THIS workspace, and reload its thread list (+ latest convo).
|
|
56971
57074
|
newChat();
|
|
56972
57075
|
clearActivityFeed();
|
|
56973
|
-
startFeed();
|
|
56974
57076
|
refreshThreadList(true);
|
|
56975
57077
|
showToast('Switched workspace', {});
|
|
56976
57078
|
}).catch(function (err) { menu.hidden = true; endWsSwitching(true); showToast('Switch failed: ' + err.message, {}); });
|
|
@@ -57431,7 +57533,7 @@ var appJs = `
|
|
|
57431
57533
|
function rowVisMarkup(tbl, r) {
|
|
57432
57534
|
var a = r._access;
|
|
57433
57535
|
if (!a) return '';
|
|
57434
|
-
var vis = a
|
|
57536
|
+
var vis = effectiveVisibility(a);
|
|
57435
57537
|
var glyph = vis === 'custom' ? '\u25CE' : '\u25C9';
|
|
57436
57538
|
if (!a.ownedByMe) {
|
|
57437
57539
|
var seen = vis === 'custom' ? 'Shared with you' : 'Visible to everyone';
|
|
@@ -57781,7 +57883,7 @@ var appJs = `
|
|
|
57781
57883
|
function detailVisLineEl(row) {
|
|
57782
57884
|
var a = row._access;
|
|
57783
57885
|
if (!a) return '';
|
|
57784
|
-
var vis = a
|
|
57886
|
+
var vis = effectiveVisibility(a);
|
|
57785
57887
|
var labelMap = { everyone: 'Visible to everyone', private: 'Private to you', custom: 'Shared with specific people' };
|
|
57786
57888
|
// Clear visual indicator: a lock when private, an eye when shared (everyone
|
|
57787
57889
|
// or specific people), with a hover tooltip. The shared helper keeps the
|
|
@@ -57792,8 +57894,7 @@ var appJs = `
|
|
|
57792
57894
|
return '<div class="detail-vis muted" style="display:flex;align-items:center;gap:6px;margin:6px 0;font-size:13px">' +
|
|
57793
57895
|
visIcon + '<span>' + escapeHtml(seen) + '</span></div>';
|
|
57794
57896
|
}
|
|
57795
|
-
var info =
|
|
57796
|
-
if (vis === 'custom' && a.grantees) info += ' (' + a.grantees.length + ')';
|
|
57897
|
+
var info = visInfoLabel(a);
|
|
57797
57898
|
var buttons;
|
|
57798
57899
|
if (vis === 'custom') {
|
|
57799
57900
|
// Leaving custom stops the grant list from applying \u2014 the toggle
|
|
@@ -57848,9 +57949,12 @@ var appJs = `
|
|
|
57848
57949
|
if (!panel) return;
|
|
57849
57950
|
if (!panel.hidden) { panel.hidden = true; return; }
|
|
57850
57951
|
var access = row._access || {};
|
|
57851
|
-
|
|
57852
|
-
|
|
57853
|
-
|
|
57952
|
+
// Opening the panel must NOT pre-flip the row to 'custom' \u2014 that left a
|
|
57953
|
+
// row the user never actually shared stuck at "custom (0)". The first
|
|
57954
|
+
// grant flips it to custom server-side (lattice_grant_row); revoking the
|
|
57955
|
+
// last leaves it custom-with-0-grantees, which now reads as private. So
|
|
57956
|
+
// just load the member checklist.
|
|
57957
|
+
var ensure = Promise.resolve();
|
|
57854
57958
|
withBusy(detailVisManage, function () {
|
|
57855
57959
|
return ensure.then(function () {
|
|
57856
57960
|
return fetchJson('/api/cloud/members');
|
|
@@ -57884,8 +57988,13 @@ var appJs = `
|
|
|
57884
57988
|
var at = list.indexOf(role);
|
|
57885
57989
|
if (cb.checked && at === -1) list.push(role);
|
|
57886
57990
|
if (!cb.checked && at !== -1) list.splice(at, 1);
|
|
57991
|
+
// The first grant flips the row to custom server-side; mirror
|
|
57992
|
+
// that locally so the indicator updates. Revoking the last leaves
|
|
57993
|
+
// visibility 'custom' but effectiveVisibility renders custom-0 as
|
|
57994
|
+
// private, so the label flips back to "Private to you".
|
|
57995
|
+
if (list.length > 0) access.visibility = 'custom';
|
|
57887
57996
|
var infoEl = content.querySelector('#detail-vis-info');
|
|
57888
|
-
if (infoEl) infoEl.textContent =
|
|
57997
|
+
if (infoEl) infoEl.textContent = visInfoLabel(access);
|
|
57889
57998
|
invalidate(tableName);
|
|
57890
57999
|
}).catch(function (e) {
|
|
57891
58000
|
cb.checked = !cb.checked; // revert the failed change
|
|
@@ -57893,10 +58002,8 @@ var appJs = `
|
|
|
57893
58002
|
}).then(function () { cb.disabled = false; });
|
|
57894
58003
|
});
|
|
57895
58004
|
});
|
|
57896
|
-
|
|
57897
|
-
|
|
57898
|
-
if (infoEl) infoEl.textContent = 'Shared with specific people (' + (access.grantees || []).length + ')';
|
|
57899
|
-
}
|
|
58005
|
+
var infoEl = content.querySelector('#detail-vis-info');
|
|
58006
|
+
if (infoEl) infoEl.textContent = visInfoLabel(access);
|
|
57900
58007
|
}).catch(function (e) { showToast('Could not load members: ' + e.message, {}); });
|
|
57901
58008
|
});
|
|
57902
58009
|
});
|
|
@@ -60007,6 +60114,29 @@ var appJs = `
|
|
|
60007
60114
|
var EYE_SVG =
|
|
60008
60115
|
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
|
|
60009
60116
|
'<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>';
|
|
60117
|
+
// A custom row with zero grantees is effectively PRIVATE (RLS shows it only to
|
|
60118
|
+
// the owner), so it must read as private, not "Shared with specific people
|
|
60119
|
+
// (0)". Only collapse for the OWNER's own view, where the grantees list is
|
|
60120
|
+
// authoritative \u2014 a member viewing a row shared WITH them gets custom with no
|
|
60121
|
+
// grantees list and must still read as "Shared with you".
|
|
60122
|
+
function effectiveVisibility(access) {
|
|
60123
|
+
if (!access || !access.visibility) return 'private';
|
|
60124
|
+
if (access.visibility === 'custom' && access.ownedByMe &&
|
|
60125
|
+
(!access.grantees || access.grantees.length === 0)) {
|
|
60126
|
+
return 'private';
|
|
60127
|
+
}
|
|
60128
|
+
return access.visibility;
|
|
60129
|
+
}
|
|
60130
|
+
// Owner-facing status label for a row's sharing, honoring effectiveVisibility
|
|
60131
|
+
// (so custom-0 reads as "Private to you") and appending the grantee count only
|
|
60132
|
+
// for a genuine specific-people share.
|
|
60133
|
+
function visInfoLabel(access) {
|
|
60134
|
+
var v = effectiveVisibility(access);
|
|
60135
|
+
var map = { everyone: 'Visible to everyone', private: 'Private to you', custom: 'Shared with specific people' };
|
|
60136
|
+
var s = map[v] || '';
|
|
60137
|
+
if (v === 'custom' && access && access.grantees) s += ' (' + access.grantees.length + ')';
|
|
60138
|
+
return s;
|
|
60139
|
+
}
|
|
60010
60140
|
// Shared per-row lock/eye indicator, reused on the entity-detail header and the
|
|
60011
60141
|
// fs card tiles so the meaning is consistent. The access arg is the server-
|
|
60012
60142
|
// attached row._access (visibility + ownedByMe); returns empty when absent (a
|
|
@@ -60014,7 +60144,7 @@ var appJs = `
|
|
|
60014
60144
|
// A hover tooltip spells out what the lock/eye means (state + ownership aware).
|
|
60015
60145
|
function visIndicator(access, extraClass) {
|
|
60016
60146
|
if (!access || !access.visibility) return '';
|
|
60017
|
-
var vis = access
|
|
60147
|
+
var vis = effectiveVisibility(access);
|
|
60018
60148
|
var tip = vis === 'private'
|
|
60019
60149
|
? 'Private \u2014 only you can see this'
|
|
60020
60150
|
: vis === 'custom'
|
|
@@ -61803,7 +61933,6 @@ var appJs = `
|
|
|
61803
61933
|
|
|
61804
61934
|
|
|
61805
61935
|
// ============ AI assistant rail (2.0) ============
|
|
61806
|
-
var feedSource = null;
|
|
61807
61936
|
var FEED_ICONS = {
|
|
61808
61937
|
insert: '\u2795', update: '\u270F\uFE0F', delete: '\u{1F5D1}',
|
|
61809
61938
|
link: '\u{1F517}', unlink: '\u26D3', undo: '\u21B6', redo: '\u21B7', schema: '\u{1F6E0}',
|
|
@@ -62056,32 +62185,13 @@ var appJs = `
|
|
|
62056
62185
|
else { card.timeEl.textContent = formatElapsed(Math.max(0, evMs - startMs)); }
|
|
62057
62186
|
}
|
|
62058
62187
|
}
|
|
62059
|
-
|
|
62060
|
-
|
|
62061
|
-
|
|
62062
|
-
|
|
62063
|
-
|
|
62064
|
-
|
|
62065
|
-
|
|
62066
|
-
feedSource.addEventListener('feed', function (ev) {
|
|
62067
|
-
var data;
|
|
62068
|
-
try { data = JSON.parse(ev.data); } catch (_) { return; /* ignore malformed */ }
|
|
62069
|
-
try { renderFeedItem(data); } catch (_) { /* render best-effort */ }
|
|
62070
|
-
// Refresh on ANY data mutation, not just schema/new-table events. The
|
|
62071
|
-
// local feed bus delivers every insert/update/delete/link even when
|
|
62072
|
-
// there's no realtime broker (SQLite/local), so this is what makes the
|
|
62073
|
-
// home dashboard counts AND the open entity view live-update without a
|
|
62074
|
-
// manual reload (previously only schema ops or brand-new tables did).
|
|
62075
|
-
// scheduleRealtimeRefresh is debounced (200ms) so a burst from one
|
|
62076
|
-
// ingest still coalesces into a single refetch \u2014 and on Postgres/cloud
|
|
62077
|
-
// it shares that debounce with the realtime 'change' handler (no double
|
|
62078
|
-
// fetch). /api/entities batches its row counts into one query, not N.
|
|
62079
|
-
if (data && (data.table || data.op === 'schema')) {
|
|
62080
|
-
scheduleRealtimeRefresh();
|
|
62081
|
-
}
|
|
62082
|
-
});
|
|
62083
|
-
// EventSource auto-reconnects on error; no extra handling needed.
|
|
62084
|
-
}
|
|
62188
|
+
// Feed events arrive over the multiplexed /api/stream WebSocket and are
|
|
62189
|
+
// handled in dispatchStreamMessage('feed', \u2026): renderFeedItem() paints the
|
|
62190
|
+
// card, then scheduleRealtimeRefresh() refetches on ANY data mutation (the
|
|
62191
|
+
// local feed bus delivers every insert/update/delete/link even with no
|
|
62192
|
+
// realtime broker, so this is what live-updates the dashboard counts and the
|
|
62193
|
+
// open entity view without a manual reload). The 200ms debounce coalesces a
|
|
62194
|
+
// burst into a single refetch and is shared with the realtime 'change' path.
|
|
62085
62195
|
|
|
62086
62196
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
62087
62197
|
// Assistant rail resize \u2014 drag the left edge, clamp, persist.
|
|
@@ -62777,6 +62887,16 @@ var analyticsJs = `
|
|
|
62777
62887
|
page_title: t,
|
|
62778
62888
|
});
|
|
62779
62889
|
},
|
|
62890
|
+
// Set the GA user_id to an OPAQUE per-user hash so active users are
|
|
62891
|
+
// deduplicated across sessions/devices (without it, unique users \u2248 events).
|
|
62892
|
+
// Accepts ONLY a 64-char hex digest \u2014 a raw email or any other PII is
|
|
62893
|
+
// rejected (never sent), preserving the privacy contract above. The caller
|
|
62894
|
+
// hashes the email; this module never sees the plaintext.
|
|
62895
|
+
setUser: function (idHash) {
|
|
62896
|
+
if (!consent || !loaded) return;
|
|
62897
|
+
if (typeof idHash !== 'string' || !/^[a-f0-9]{64}$/.test(idHash)) return;
|
|
62898
|
+
gtag('config', MEASUREMENT_ID, { user_id: idHash });
|
|
62899
|
+
},
|
|
62780
62900
|
};
|
|
62781
62901
|
})();
|
|
62782
62902
|
`;
|
|
@@ -66185,19 +66305,36 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
|
|
|
66185
66305
|
}
|
|
66186
66306
|
let memberOpen = false;
|
|
66187
66307
|
const maskedReadViews = /* @__PURE__ */ new Map();
|
|
66308
|
+
const discoveredJunctions = /* @__PURE__ */ new Set();
|
|
66188
66309
|
if (db.getDialect() === "postgres") {
|
|
66189
66310
|
const peek = new Lattice({ config: configPath }, { encryptionKey });
|
|
66190
66311
|
try {
|
|
66191
66312
|
await peek.init({ introspectOnly: true });
|
|
66192
|
-
|
|
66193
|
-
|
|
66313
|
+
const [rlsInstalled, canManage] = await Promise.all([
|
|
66314
|
+
cloudRlsInstalled(peek),
|
|
66315
|
+
canManageRoles(peek)
|
|
66316
|
+
]);
|
|
66317
|
+
if (rlsInstalled) {
|
|
66318
|
+
memberOpen = !canManage;
|
|
66194
66319
|
if (memberOpen) {
|
|
66195
66320
|
const declared = new Set(db.getRegisteredTableNames());
|
|
66196
|
-
const discovered = await
|
|
66321
|
+
const [discovered, viewsRaw] = await Promise.all([
|
|
66322
|
+
discoverCloudTables(peek),
|
|
66323
|
+
allAsyncOrSync(
|
|
66324
|
+
peek.adapter,
|
|
66325
|
+
`SELECT table_name AS name FROM information_schema.views
|
|
66326
|
+
WHERE table_schema = current_schema() AND table_name LIKE '%\\_v' ESCAPE '\\'`
|
|
66327
|
+
)
|
|
66328
|
+
]);
|
|
66329
|
+
const views = viewsRaw;
|
|
66197
66330
|
const knownTables = /* @__PURE__ */ new Set([...declared, ...discovered.map((t8) => t8.name)]);
|
|
66198
66331
|
for (const t8 of discovered) {
|
|
66199
66332
|
if (declared.has(t8.name)) continue;
|
|
66200
66333
|
if (t8.columns.length === 0) continue;
|
|
66334
|
+
if (isJunctionByColumns(t8.columns)) {
|
|
66335
|
+
discoveredJunctions.add(t8.name);
|
|
66336
|
+
continue;
|
|
66337
|
+
}
|
|
66201
66338
|
db.define(t8.name, {
|
|
66202
66339
|
columns: Object.fromEntries(t8.columns.map((c6) => [c6, "TEXT"])),
|
|
66203
66340
|
...t8.pk.length > 0 ? { primaryKey: t8.pk.length === 1 ? t8.pk[0] : t8.pk } : {},
|
|
@@ -66205,11 +66342,6 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
|
|
|
66205
66342
|
outputFile: `${t8.name}/.lattice/${t8.name}.md`
|
|
66206
66343
|
});
|
|
66207
66344
|
}
|
|
66208
|
-
const views = await allAsyncOrSync(
|
|
66209
|
-
peek.adapter,
|
|
66210
|
-
`SELECT table_name AS name FROM information_schema.views
|
|
66211
|
-
WHERE table_schema = current_schema() AND table_name LIKE '%\\_v' ESCAPE '\\'`
|
|
66212
|
-
);
|
|
66213
66345
|
for (const { name } of views) {
|
|
66214
66346
|
const base = name.slice(0, -2);
|
|
66215
66347
|
if (knownTables.has(base)) maskedReadViews.set(base, name);
|
|
@@ -66246,9 +66378,12 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
|
|
|
66246
66378
|
if (name.startsWith("__lattice_") || name.startsWith("_lattice_")) continue;
|
|
66247
66379
|
validTables.add(name);
|
|
66248
66380
|
}
|
|
66249
|
-
const junctionTables = new Set(
|
|
66250
|
-
getGuiEntities(configPath, outputDir).tables.filter(isJunctionTable).map((t8) => t8.name)
|
|
66251
|
-
|
|
66381
|
+
const junctionTables = /* @__PURE__ */ new Set([
|
|
66382
|
+
...getGuiEntities(configPath, outputDir).tables.filter(isJunctionTable).map((t8) => t8.name),
|
|
66383
|
+
// Member-discovered junctions (classified from the physical shape above);
|
|
66384
|
+
// empty for an owner/local open.
|
|
66385
|
+
...discoveredJunctions
|
|
66386
|
+
]);
|
|
66252
66387
|
const entityContextByTable = db.entityContexts();
|
|
66253
66388
|
const manifest = readManifest(outputDir);
|
|
66254
66389
|
const softDeletable = /* @__PURE__ */ new Set();
|
|
@@ -66449,6 +66584,9 @@ async function changeVisibleToActiveRole(db, payload) {
|
|
|
66449
66584
|
function isDeleteOp(op) {
|
|
66450
66585
|
return op === "delete" || op === "DELETE";
|
|
66451
66586
|
}
|
|
66587
|
+
function isFeedHiddenTable(t8) {
|
|
66588
|
+
return t8.startsWith("_lattice") || t8.startsWith("__lattice") || isInternalNativeEntity(t8);
|
|
66589
|
+
}
|
|
66452
66590
|
function readRelationFor(active, table) {
|
|
66453
66591
|
return active.maskedReadViews.get(table) ?? table;
|
|
66454
66592
|
}
|
|
@@ -66830,158 +66968,10 @@ async function startGuiServer(options) {
|
|
|
66830
66968
|
sendJson(res, { mode, state: active.realtime?.state() ?? "local", connected });
|
|
66831
66969
|
return;
|
|
66832
66970
|
}
|
|
66833
|
-
if (method === "GET" && pathname === "/api/realtime/stream") {
|
|
66834
|
-
res.writeHead(200, {
|
|
66835
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
66836
|
-
"cache-control": "no-store, no-transform",
|
|
66837
|
-
connection: "keep-alive",
|
|
66838
|
-
"x-accel-buffering": "no"
|
|
66839
|
-
});
|
|
66840
|
-
const broker = active.realtime;
|
|
66841
|
-
const initialMode = broker ? "cloud" : "local";
|
|
66842
|
-
const writeEvent = (event, data) => {
|
|
66843
|
-
try {
|
|
66844
|
-
res.write(`event: ${event}
|
|
66845
|
-
data: ${JSON.stringify(data)}
|
|
66846
|
-
|
|
66847
|
-
`);
|
|
66848
|
-
} catch {
|
|
66849
|
-
}
|
|
66850
|
-
};
|
|
66851
|
-
writeEvent("state", {
|
|
66852
|
-
mode: initialMode,
|
|
66853
|
-
state: broker?.state() ?? "local"
|
|
66854
|
-
});
|
|
66855
|
-
const keepalive = setInterval(() => {
|
|
66856
|
-
try {
|
|
66857
|
-
res.write(`: keepalive
|
|
66858
|
-
|
|
66859
|
-
`);
|
|
66860
|
-
} catch {
|
|
66861
|
-
}
|
|
66862
|
-
}, 25e3);
|
|
66863
|
-
const offState = broker?.subscribeState((state2) => {
|
|
66864
|
-
writeEvent("state", { mode: "cloud", state: state2 });
|
|
66865
|
-
});
|
|
66866
|
-
const offPayload = broker?.subscribePayload((payload) => {
|
|
66867
|
-
if (activeRef !== active) return;
|
|
66868
|
-
void changeVisibleToActiveRole(active.db, payload).then((visible) => {
|
|
66869
|
-
if (!visible) return;
|
|
66870
|
-
const out = isDeleteOp(payload.op) ? { ...payload, owner_role: null } : payload;
|
|
66871
|
-
writeEvent("change", out);
|
|
66872
|
-
});
|
|
66873
|
-
});
|
|
66874
|
-
const cleanup = () => {
|
|
66875
|
-
clearInterval(keepalive);
|
|
66876
|
-
if (offState) offState();
|
|
66877
|
-
if (offPayload) offPayload();
|
|
66878
|
-
};
|
|
66879
|
-
req.on("close", cleanup);
|
|
66880
|
-
req.on("error", cleanup);
|
|
66881
|
-
return;
|
|
66882
|
-
}
|
|
66883
|
-
if (method === "GET" && pathname === "/api/feed/stream") {
|
|
66884
|
-
res.writeHead(200, {
|
|
66885
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
66886
|
-
"cache-control": "no-store, no-transform",
|
|
66887
|
-
connection: "keep-alive",
|
|
66888
|
-
"x-accel-buffering": "no"
|
|
66889
|
-
});
|
|
66890
|
-
const writeFeed = (data) => {
|
|
66891
|
-
try {
|
|
66892
|
-
res.write(`event: feed
|
|
66893
|
-
data: ${JSON.stringify(data)}
|
|
66894
|
-
|
|
66895
|
-
`);
|
|
66896
|
-
} catch {
|
|
66897
|
-
}
|
|
66898
|
-
};
|
|
66899
|
-
const keepalive = setInterval(() => {
|
|
66900
|
-
try {
|
|
66901
|
-
res.write(`: keepalive
|
|
66902
|
-
|
|
66903
|
-
`);
|
|
66904
|
-
} catch {
|
|
66905
|
-
}
|
|
66906
|
-
}, 25e3);
|
|
66907
|
-
const recentSelf = /* @__PURE__ */ new Map();
|
|
66908
|
-
const isFeedHiddenTable = (t8) => t8.startsWith("_lattice") || t8.startsWith("__lattice") || isInternalNativeEntity(t8);
|
|
66909
|
-
const offFeed = active.feed.subscribe((e6) => {
|
|
66910
|
-
if (e6.table && isFeedHiddenTable(e6.table)) return;
|
|
66911
|
-
recentSelf.set(`${e6.table ?? ""}:${e6.rowId ?? ""}:${e6.op}`, Date.now());
|
|
66912
|
-
writeFeed(e6);
|
|
66913
|
-
});
|
|
66914
|
-
const offBroker = active.realtime?.subscribePayload((p3) => {
|
|
66915
|
-
const op = feedOpForChange(p3.op);
|
|
66916
|
-
if (!op || !p3.table_name || isFeedHiddenTable(p3.table_name)) return;
|
|
66917
|
-
const tableName = p3.table_name;
|
|
66918
|
-
const key = `${tableName}:${p3.pk ?? ""}:${op}`;
|
|
66919
|
-
const seen = recentSelf.get(key);
|
|
66920
|
-
if (seen && Date.now() - seen < 5e3) return;
|
|
66921
|
-
if (activeRef !== active) return;
|
|
66922
|
-
void changeVisibleToActiveRole(active.db, p3).then((visible) => {
|
|
66923
|
-
if (!visible) return;
|
|
66924
|
-
writeFeed({
|
|
66925
|
-
seq: p3.seq,
|
|
66926
|
-
table: tableName,
|
|
66927
|
-
op,
|
|
66928
|
-
rowId: p3.pk,
|
|
66929
|
-
source: "cli",
|
|
66930
|
-
actor: isDeleteOp(p3.op) ? void 0 : p3.owner_role ?? void 0,
|
|
66931
|
-
ts: p3.created_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
66932
|
-
summary: `${op} on ${tableName} (another client)`
|
|
66933
|
-
});
|
|
66934
|
-
});
|
|
66935
|
-
});
|
|
66936
|
-
const cleanup = () => {
|
|
66937
|
-
clearInterval(keepalive);
|
|
66938
|
-
offFeed();
|
|
66939
|
-
if (offBroker) offBroker();
|
|
66940
|
-
};
|
|
66941
|
-
req.on("close", cleanup);
|
|
66942
|
-
req.on("error", cleanup);
|
|
66943
|
-
return;
|
|
66944
|
-
}
|
|
66945
66971
|
if (method === "GET" && pathname === "/api/render/status") {
|
|
66946
66972
|
sendJson(res, active.renderState);
|
|
66947
66973
|
return;
|
|
66948
66974
|
}
|
|
66949
|
-
if (method === "GET" && pathname === "/api/render/progress") {
|
|
66950
|
-
res.writeHead(200, {
|
|
66951
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
66952
|
-
"cache-control": "no-store, no-transform",
|
|
66953
|
-
connection: "keep-alive",
|
|
66954
|
-
"x-accel-buffering": "no"
|
|
66955
|
-
});
|
|
66956
|
-
const writeEvent = (event, data) => {
|
|
66957
|
-
try {
|
|
66958
|
-
res.write(`event: ${event}
|
|
66959
|
-
data: ${JSON.stringify(data)}
|
|
66960
|
-
|
|
66961
|
-
`);
|
|
66962
|
-
} catch {
|
|
66963
|
-
}
|
|
66964
|
-
};
|
|
66965
|
-
writeEvent("snapshot", active.renderState);
|
|
66966
|
-
const keepalive = setInterval(() => {
|
|
66967
|
-
try {
|
|
66968
|
-
res.write(`: keepalive
|
|
66969
|
-
|
|
66970
|
-
`);
|
|
66971
|
-
} catch {
|
|
66972
|
-
}
|
|
66973
|
-
}, 25e3);
|
|
66974
|
-
const offProgress = active.renderProgress.subscribe((e6) => {
|
|
66975
|
-
writeEvent("progress", e6);
|
|
66976
|
-
});
|
|
66977
|
-
const cleanup = () => {
|
|
66978
|
-
clearInterval(keepalive);
|
|
66979
|
-
offProgress();
|
|
66980
|
-
};
|
|
66981
|
-
req.on("close", cleanup);
|
|
66982
|
-
req.on("error", cleanup);
|
|
66983
|
-
return;
|
|
66984
|
-
}
|
|
66985
66975
|
if (method === "GET" && pathname === "/api/project") {
|
|
66986
66976
|
sendJson(res, getGuiProject(active.configPath, active.outputDir));
|
|
66987
66977
|
return;
|
|
@@ -68499,6 +68489,107 @@ ${e6.stack ?? ""}`
|
|
|
68499
68489
|
}
|
|
68500
68490
|
})();
|
|
68501
68491
|
});
|
|
68492
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
68493
|
+
const handleEventStream = (ws) => {
|
|
68494
|
+
const bound = activeRef;
|
|
68495
|
+
const send = (type, data) => {
|
|
68496
|
+
if (ws.readyState !== WebSocket.OPEN) return;
|
|
68497
|
+
try {
|
|
68498
|
+
ws.send(JSON.stringify({ type, data }));
|
|
68499
|
+
} catch {
|
|
68500
|
+
}
|
|
68501
|
+
};
|
|
68502
|
+
const broker = bound?.realtime ?? null;
|
|
68503
|
+
send("realtime-state", { mode: broker ? "cloud" : "local", state: broker?.state() ?? "local" });
|
|
68504
|
+
if (bound) send("render-snapshot", bound.renderState);
|
|
68505
|
+
const offs = [];
|
|
68506
|
+
if (bound) {
|
|
68507
|
+
if (broker) {
|
|
68508
|
+
offs.push(
|
|
68509
|
+
broker.subscribeState((state2) => {
|
|
68510
|
+
send("realtime-state", { mode: "cloud", state: state2 });
|
|
68511
|
+
})
|
|
68512
|
+
);
|
|
68513
|
+
offs.push(
|
|
68514
|
+
broker.subscribePayload((payload) => {
|
|
68515
|
+
if (activeRef !== bound) return;
|
|
68516
|
+
void changeVisibleToActiveRole(bound.db, payload).then((visible) => {
|
|
68517
|
+
if (!visible) return;
|
|
68518
|
+
const out = isDeleteOp(payload.op) ? { ...payload, owner_role: null } : payload;
|
|
68519
|
+
send("realtime-change", out);
|
|
68520
|
+
});
|
|
68521
|
+
})
|
|
68522
|
+
);
|
|
68523
|
+
}
|
|
68524
|
+
const recentSelf = /* @__PURE__ */ new Map();
|
|
68525
|
+
offs.push(
|
|
68526
|
+
bound.feed.subscribe((e6) => {
|
|
68527
|
+
if (e6.table && isFeedHiddenTable(e6.table)) return;
|
|
68528
|
+
recentSelf.set(`${e6.table ?? ""}:${e6.rowId ?? ""}:${e6.op}`, Date.now());
|
|
68529
|
+
send("feed", e6);
|
|
68530
|
+
})
|
|
68531
|
+
);
|
|
68532
|
+
if (broker) {
|
|
68533
|
+
offs.push(
|
|
68534
|
+
broker.subscribePayload((p3) => {
|
|
68535
|
+
const op = feedOpForChange(p3.op);
|
|
68536
|
+
if (!op || !p3.table_name || isFeedHiddenTable(p3.table_name)) return;
|
|
68537
|
+
const tableName = p3.table_name;
|
|
68538
|
+
const key = `${tableName}:${p3.pk ?? ""}:${op}`;
|
|
68539
|
+
const seen = recentSelf.get(key);
|
|
68540
|
+
if (seen && Date.now() - seen < 5e3) return;
|
|
68541
|
+
if (activeRef !== bound) return;
|
|
68542
|
+
void changeVisibleToActiveRole(bound.db, p3).then((visible) => {
|
|
68543
|
+
if (!visible) return;
|
|
68544
|
+
send("feed", {
|
|
68545
|
+
seq: p3.seq,
|
|
68546
|
+
table: tableName,
|
|
68547
|
+
op,
|
|
68548
|
+
rowId: p3.pk,
|
|
68549
|
+
source: "cli",
|
|
68550
|
+
actor: isDeleteOp(p3.op) ? void 0 : p3.owner_role ?? void 0,
|
|
68551
|
+
ts: p3.created_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
68552
|
+
summary: `${op} on ${tableName} (another client)`
|
|
68553
|
+
});
|
|
68554
|
+
});
|
|
68555
|
+
})
|
|
68556
|
+
);
|
|
68557
|
+
}
|
|
68558
|
+
offs.push(
|
|
68559
|
+
bound.renderProgress.subscribe((e6) => {
|
|
68560
|
+
send("render-progress", e6);
|
|
68561
|
+
})
|
|
68562
|
+
);
|
|
68563
|
+
}
|
|
68564
|
+
const keepalive = setInterval(() => {
|
|
68565
|
+
if (ws.readyState !== WebSocket.OPEN) return;
|
|
68566
|
+
try {
|
|
68567
|
+
ws.ping();
|
|
68568
|
+
} catch {
|
|
68569
|
+
}
|
|
68570
|
+
}, 25e3);
|
|
68571
|
+
const cleanup = () => {
|
|
68572
|
+
clearInterval(keepalive);
|
|
68573
|
+
for (const off of offs) {
|
|
68574
|
+
try {
|
|
68575
|
+
off();
|
|
68576
|
+
} catch {
|
|
68577
|
+
}
|
|
68578
|
+
}
|
|
68579
|
+
};
|
|
68580
|
+
ws.on("close", cleanup);
|
|
68581
|
+
ws.on("error", cleanup);
|
|
68582
|
+
};
|
|
68583
|
+
server.on("upgrade", (req, socket, head) => {
|
|
68584
|
+
const { pathname } = new URL(req.url ?? "/", `http://${host}`);
|
|
68585
|
+
if (pathname !== "/api/stream") {
|
|
68586
|
+
socket.destroy();
|
|
68587
|
+
return;
|
|
68588
|
+
}
|
|
68589
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
68590
|
+
handleEventStream(ws);
|
|
68591
|
+
});
|
|
68592
|
+
});
|
|
68502
68593
|
const port = await listenWithPortFallback(server, startPort, host);
|
|
68503
68594
|
if (activeRef) startBackgroundRender(activeRef);
|
|
68504
68595
|
const displayHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
|
|
@@ -68509,6 +68600,13 @@ ${e6.stack ?? ""}`
|
|
|
68509
68600
|
port,
|
|
68510
68601
|
url,
|
|
68511
68602
|
close: () => new Promise((resolveClose, reject) => {
|
|
68603
|
+
for (const client of wss.clients) {
|
|
68604
|
+
try {
|
|
68605
|
+
client.terminate();
|
|
68606
|
+
} catch {
|
|
68607
|
+
}
|
|
68608
|
+
}
|
|
68609
|
+
wss.close();
|
|
68512
68610
|
server.close((err) => {
|
|
68513
68611
|
if (err) {
|
|
68514
68612
|
reject(err);
|