latticesql 3.3.1 → 3.3.2
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 +337 -268
- package/dist/index.cjs +336 -268
- package/dist/index.js +336 -268
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -7417,6 +7417,10 @@ function isJunctionTable(table) {
|
|
|
7417
7417
|
const fkCols = new Set(belongsTo.map((r6) => r6.foreignKey));
|
|
7418
7418
|
return table.columns.every((c6) => fkCols.has(c6) || JUNCTION_ALLOWED_NONFK.has(c6));
|
|
7419
7419
|
}
|
|
7420
|
+
function isJunctionByColumns(columns) {
|
|
7421
|
+
const payload = columns.filter((c6) => !JUNCTION_ALLOWED_NONFK.has(c6));
|
|
7422
|
+
return payload.length === 2 && payload.every((c6) => c6.endsWith("_id"));
|
|
7423
|
+
}
|
|
7420
7424
|
function fileJunctions(configPath, outputDir) {
|
|
7421
7425
|
const out = [];
|
|
7422
7426
|
for (const t8 of getGuiEntities(configPath, outputDir).tables) {
|
|
@@ -12224,6 +12228,16 @@ var init_ingest_url = __esm({
|
|
|
12224
12228
|
});
|
|
12225
12229
|
|
|
12226
12230
|
// src/gui/ai/dispatch.ts
|
|
12231
|
+
function visibilityDenialReason(opts) {
|
|
12232
|
+
if (opts.kind === "table") {
|
|
12233
|
+
return opts.canManageTableDefault ? null : "Only the workspace owner can change a table's default sharing.";
|
|
12234
|
+
}
|
|
12235
|
+
if (!opts.rowAccess) return "That record was not found, or is not visible to you.";
|
|
12236
|
+
if (!opts.rowAccess.ownedByMe) {
|
|
12237
|
+
return "You do not own this record, so you cannot change its sharing \u2014 only its owner can.";
|
|
12238
|
+
}
|
|
12239
|
+
return null;
|
|
12240
|
+
}
|
|
12227
12241
|
async function secretColumnsFor(db, table) {
|
|
12228
12242
|
try {
|
|
12229
12243
|
const rows = await db.query("_lattice_gui_column_meta", {
|
|
@@ -12449,6 +12463,14 @@ async function executeFunction(ctx, name, args) {
|
|
|
12449
12463
|
};
|
|
12450
12464
|
}
|
|
12451
12465
|
const id = typeof args.id === "string" && args.id ? args.id : void 0;
|
|
12466
|
+
const denial = id ? visibilityDenialReason({
|
|
12467
|
+
kind: "row",
|
|
12468
|
+
rowAccess: (await rowAccessSummaries(ctx.db, table, [id])).get(id)
|
|
12469
|
+
}) : visibilityDenialReason({
|
|
12470
|
+
kind: "table",
|
|
12471
|
+
canManageTableDefault: await canManageRoles(ctx.db)
|
|
12472
|
+
});
|
|
12473
|
+
if (denial) return { ok: false, error: denial };
|
|
12452
12474
|
try {
|
|
12453
12475
|
if (id) {
|
|
12454
12476
|
await setRowVisibility(ctx.db, table, id, visibility);
|
|
@@ -12612,6 +12634,7 @@ var init_dispatch = __esm({
|
|
|
12612
12634
|
init_column_descriptions();
|
|
12613
12635
|
init_members();
|
|
12614
12636
|
init_table_policy();
|
|
12637
|
+
init_cloud_connect();
|
|
12615
12638
|
init_dedup_service();
|
|
12616
12639
|
DISPATCHABLE = /* @__PURE__ */ new Set([
|
|
12617
12640
|
"list_entities",
|
|
@@ -52440,6 +52463,7 @@ async function checkForUpdate(pkgName, currentVersion) {
|
|
|
52440
52463
|
// src/gui/server.ts
|
|
52441
52464
|
import { createServer } from "http";
|
|
52442
52465
|
import { spawn as spawn2 } from "child_process";
|
|
52466
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
52443
52467
|
import {
|
|
52444
52468
|
existsSync as existsSync23,
|
|
52445
52469
|
mkdirSync as mkdirSync11,
|
|
@@ -54314,6 +54338,31 @@ var appJs = `
|
|
|
54314
54338
|
// Boot analytics with the resolved consent (no network contact when off),
|
|
54315
54339
|
// then record the session open. advanced_mode is a boolean \u2014 safe to send.
|
|
54316
54340
|
if (window.LatticeGA) window.LatticeGA.init(state.analyticsEffective);
|
|
54341
|
+
// Deduplicate unique users in GA: set the GA user_id to a SHA-256 hash of
|
|
54342
|
+
// the operator's email. Anonymized \u2014 the plaintext is hashed in-browser and
|
|
54343
|
+
// never sent (analytics.ts only accepts a hex digest). Without a user_id,
|
|
54344
|
+
// GA counts each session/device as a new user (active-users \u2248 events).
|
|
54345
|
+
// Best-effort + only when analytics consent is on.
|
|
54346
|
+
if (window.LatticeGA && state.analyticsEffective && window.crypto && window.crypto.subtle) {
|
|
54347
|
+
fetchJson('/api/userconfig/identity')
|
|
54348
|
+
.then(function (id) {
|
|
54349
|
+
var email = id && id.email ? String(id.email).trim().toLowerCase() : '';
|
|
54350
|
+
if (!email) return undefined;
|
|
54351
|
+
return window.crypto.subtle
|
|
54352
|
+
.digest('SHA-256', new TextEncoder().encode(email))
|
|
54353
|
+
.then(function (buf) {
|
|
54354
|
+
var hex = Array.prototype.map
|
|
54355
|
+
.call(new Uint8Array(buf), function (b) {
|
|
54356
|
+
return ('0' + b.toString(16)).slice(-2);
|
|
54357
|
+
})
|
|
54358
|
+
.join('');
|
|
54359
|
+
window.LatticeGA.setUser(hex);
|
|
54360
|
+
});
|
|
54361
|
+
})
|
|
54362
|
+
.catch(function () {
|
|
54363
|
+
/* best-effort \u2014 GA still functions without a user_id */
|
|
54364
|
+
});
|
|
54365
|
+
}
|
|
54317
54366
|
gaTrack('app_open', { advanced_mode: advancedMode() });
|
|
54318
54367
|
document.body.classList.toggle('advanced-mode', advancedMode());
|
|
54319
54368
|
wireSettingsDrawer();
|
|
@@ -54325,15 +54374,13 @@ var appJs = `
|
|
|
54325
54374
|
wireHistoryControls();
|
|
54326
54375
|
refreshHistoryState();
|
|
54327
54376
|
renderRoute();
|
|
54328
|
-
|
|
54329
|
-
startRenderProgress();
|
|
54377
|
+
startEventStream();
|
|
54330
54378
|
initSearch();
|
|
54331
54379
|
initLastEdited();
|
|
54332
54380
|
initOffline();
|
|
54333
54381
|
initRailResize();
|
|
54334
54382
|
initRailDrawer();
|
|
54335
54383
|
initRailDragDrop();
|
|
54336
|
-
startFeed();
|
|
54337
54384
|
renderComposer();
|
|
54338
54385
|
initThreadControls();
|
|
54339
54386
|
checkNativeSetup();
|
|
@@ -54349,12 +54396,14 @@ var appJs = `
|
|
|
54349
54396
|
}
|
|
54350
54397
|
|
|
54351
54398
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
54352
|
-
// Realtime
|
|
54353
|
-
//
|
|
54354
|
-
//
|
|
54355
|
-
//
|
|
54399
|
+
// Realtime / feed / render-progress all arrive over ONE multiplexed
|
|
54400
|
+
// WebSocket (/api/stream) \u2014 see startEventStream() below. A single
|
|
54401
|
+
// connection per tab (instead of three SSE streams) keeps the browser's
|
|
54402
|
+
// tiny per-host HTTP connection budget free for data requests, so clicking
|
|
54403
|
+
// objects and switching workspaces stay responsive no matter how many tabs
|
|
54404
|
+
// are open. 'change' events mark the current view dirty and refetch via
|
|
54405
|
+
// afterMutation() (debounced); 'state' events drive the topbar pill.
|
|
54356
54406
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
54357
|
-
var realtimeSource = null;
|
|
54358
54407
|
var realtimePending = null;
|
|
54359
54408
|
// Team-cloud collaboration state. usersById resolves "last edited by"
|
|
54360
54409
|
// names; lastEditedByPk maps "<table>|<pk>" \u2192 { userId, at } from realtime
|
|
@@ -54692,42 +54741,87 @@ var appJs = `
|
|
|
54692
54741
|
afterMutation().catch(function () { /* swallow */ });
|
|
54693
54742
|
}, 200);
|
|
54694
54743
|
}
|
|
54695
|
-
|
|
54696
|
-
|
|
54697
|
-
|
|
54698
|
-
|
|
54699
|
-
|
|
54700
|
-
|
|
54701
|
-
|
|
54702
|
-
|
|
54703
|
-
|
|
54704
|
-
|
|
54705
|
-
|
|
54706
|
-
|
|
54707
|
-
|
|
54708
|
-
|
|
54709
|
-
|
|
54710
|
-
|
|
54711
|
-
if (
|
|
54744
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
54745
|
+
// Multiplexed event stream \u2014 ONE WebSocket carries realtime state/change,
|
|
54746
|
+
// the activity feed, and background-render progress (previously three
|
|
54747
|
+
// separate SSE streams). Holding one connection per tab instead of three
|
|
54748
|
+
// keeps the browser's per-host HTTP budget free for data requests, so the
|
|
54749
|
+
// GUI never freezes when several tabs are open. Each server message is
|
|
54750
|
+
// { type, data }; we demux to the same handlers the SSE listeners used.
|
|
54751
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
54752
|
+
var eventStream = null; // the active WebSocket (or null)
|
|
54753
|
+
var eventStreamReconnect = null; // pending reconnect timer
|
|
54754
|
+
var eventStreamBackoff = 1000; // reconnect delay, grows to a cap
|
|
54755
|
+
var eventStreamClosed = false; // true \u21D2 closed on purpose (switch/teardown), don't reconnect
|
|
54756
|
+
function dispatchStreamMessage(type, data) {
|
|
54757
|
+
if (type === 'realtime-state') {
|
|
54758
|
+
setStatusPill((data && data.mode) || 'local', (data && data.state) || 'local');
|
|
54759
|
+
} else if (type === 'realtime-change') {
|
|
54760
|
+
if (data) onRealtimeChange(data);
|
|
54712
54761
|
scheduleRealtimeRefresh();
|
|
54713
|
-
})
|
|
54714
|
-
|
|
54715
|
-
|
|
54716
|
-
|
|
54762
|
+
} else if (type === 'feed') {
|
|
54763
|
+
try { renderFeedItem(data); } catch (_) { /* render best-effort */ }
|
|
54764
|
+
if (data && (data.table || data.op === 'schema')) scheduleRealtimeRefresh();
|
|
54765
|
+
} else if (type === 'render-snapshot') {
|
|
54766
|
+
if (data) applyRenderSnapshot(data);
|
|
54767
|
+
} else if (type === 'render-progress') {
|
|
54768
|
+
if (data) onRenderEvent(data);
|
|
54769
|
+
}
|
|
54770
|
+
}
|
|
54771
|
+
function scheduleEventStreamReconnect() {
|
|
54772
|
+
if (eventStreamClosed || eventStreamReconnect) return;
|
|
54773
|
+
var delay = eventStreamBackoff;
|
|
54774
|
+
eventStreamBackoff = Math.min(eventStreamBackoff * 2, 15000);
|
|
54775
|
+
eventStreamReconnect = setTimeout(function () {
|
|
54776
|
+
eventStreamReconnect = null;
|
|
54777
|
+
startEventStream();
|
|
54778
|
+
}, delay);
|
|
54779
|
+
}
|
|
54780
|
+
function stopEventStream() {
|
|
54781
|
+
eventStreamClosed = true;
|
|
54782
|
+
if (eventStreamReconnect) { clearTimeout(eventStreamReconnect); eventStreamReconnect = null; }
|
|
54783
|
+
if (eventStream) {
|
|
54784
|
+
// Drop the onclose handler first so an intentional close doesn't trip the
|
|
54785
|
+
// reconnect/disconnect path.
|
|
54786
|
+
try { eventStream.onclose = null; eventStream.close(); } catch (_) { /* ignore */ }
|
|
54787
|
+
eventStream = null;
|
|
54788
|
+
}
|
|
54789
|
+
}
|
|
54790
|
+
function startEventStream() {
|
|
54791
|
+
stopEventStream();
|
|
54792
|
+
eventStreamClosed = false;
|
|
54793
|
+
if (typeof WebSocket === 'undefined') return;
|
|
54794
|
+
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
54795
|
+
var ws;
|
|
54796
|
+
try { ws = new WebSocket(proto + '//' + location.host + '/api/stream'); }
|
|
54797
|
+
catch (_) { scheduleEventStreamReconnect(); return; }
|
|
54798
|
+
eventStream = ws;
|
|
54799
|
+
ws.onopen = function () { eventStreamBackoff = 1000; };
|
|
54800
|
+
ws.onmessage = function (ev) {
|
|
54801
|
+
var msg = null;
|
|
54802
|
+
try { msg = JSON.parse(ev.data); } catch (_) { return; /* ignore malformed */ }
|
|
54803
|
+
if (msg && msg.type) dispatchStreamMessage(msg.type, msg.data);
|
|
54804
|
+
};
|
|
54805
|
+
ws.onerror = function () { /* surfaced via onclose \u2192 reconnect */ };
|
|
54806
|
+
ws.onclose = function () {
|
|
54807
|
+
if (eventStream === ws) eventStream = null;
|
|
54808
|
+
if (eventStreamClosed) return;
|
|
54809
|
+
// Unexpected drop: show the disconnect on the pill and auto-reconnect with
|
|
54810
|
+
// backoff (the server replays state + render snapshot on reconnect).
|
|
54717
54811
|
setStatusPill('cloud', 'disconnected');
|
|
54812
|
+
scheduleEventStreamReconnect();
|
|
54718
54813
|
};
|
|
54719
54814
|
}
|
|
54720
54815
|
|
|
54721
54816
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
54722
|
-
// Background-render progress \u2014
|
|
54723
|
-
// /api/render
|
|
54724
|
-
// context tree in the background;
|
|
54725
|
-
// dashboard cards (bottom-edge bar +
|
|
54726
|
-
// each table completes. Row COUNTS come
|
|
54727
|
-
//
|
|
54728
|
-
//
|
|
54817
|
+
// Background-render progress \u2014 render events arrive over the multiplexed
|
|
54818
|
+
// /api/stream WebSocket (render-snapshot + render-progress). A workspace
|
|
54819
|
+
// opens/switches instantly and renders its context tree in the background;
|
|
54820
|
+
// this paints a per-table % overlay on the dashboard cards (bottom-edge bar +
|
|
54821
|
+
// \u27F3 pill) and dims the row count until each table completes. Row COUNTS come
|
|
54822
|
+
// only from /api/entities \u2014 the render events drive only the transient overlay
|
|
54823
|
+
// and one reconciling refetch on completion.
|
|
54729
54824
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
54730
|
-
var renderSource = null;
|
|
54731
54825
|
// { [table]: { pct, rendered, total, done, error } } \u2014 the live render state,
|
|
54732
54826
|
// re-applied to cards after every dashboard rebuild (drawDashboard wipes the
|
|
54733
54827
|
// DOM overlays but not this map).
|
|
@@ -54840,37 +54934,17 @@ var appJs = `
|
|
|
54840
54934
|
}
|
|
54841
54935
|
}
|
|
54842
54936
|
}
|
|
54843
|
-
|
|
54844
|
-
|
|
54845
|
-
|
|
54846
|
-
|
|
54847
|
-
}
|
|
54848
|
-
if (typeof EventSource === 'undefined') return;
|
|
54849
|
-
// On (re)connect, fetch the single-shot snapshot so a tab that connects
|
|
54850
|
-
// mid- or post-render paints correctly even before the next event. The SSE
|
|
54851
|
-
// endpoint ALSO replays a 'snapshot' event on connect, so both paths agree.
|
|
54852
|
-
fetchJson('/api/render/status').then(applyRenderSnapshot).catch(function () { /* ignore */ });
|
|
54853
|
-
renderSource = new EventSource('/api/render/progress');
|
|
54854
|
-
renderSource.addEventListener('snapshot', function (ev) {
|
|
54855
|
-
var snap = null;
|
|
54856
|
-
try { snap = JSON.parse(ev.data); } catch (_) { /* ignore malformed */ }
|
|
54857
|
-
if (snap) applyRenderSnapshot(snap);
|
|
54858
|
-
});
|
|
54859
|
-
renderSource.addEventListener('progress', function (ev) {
|
|
54860
|
-
var e = null;
|
|
54861
|
-
try { e = JSON.parse(ev.data); } catch (_) { /* ignore malformed */ }
|
|
54862
|
-
if (e) onRenderEvent(e);
|
|
54863
|
-
});
|
|
54864
|
-
// EventSource auto-reconnects on error; the status refetch on the next
|
|
54865
|
-
// open repaints from the authoritative snapshot.
|
|
54866
|
-
}
|
|
54937
|
+
// Render snapshot + progress are applied from the multiplexed /api/stream
|
|
54938
|
+
// WebSocket: the server replays a render-snapshot on connect (so a tab that
|
|
54939
|
+
// connects mid- or post-render paints correctly) and streams render-progress
|
|
54940
|
+
// events thereafter. See dispatchStreamMessage / startEventStream.
|
|
54867
54941
|
|
|
54868
54942
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
54869
54943
|
// Shared activity helpers \u2014 the operation-icon map and relative-time
|
|
54870
54944
|
// formatter, used by Version History and the dashboard activity list. The
|
|
54871
54945
|
// standalone Activity rail was removed in 1.16.1 (redundant with Version
|
|
54872
|
-
// History); multiplayer realtime convergence runs on the
|
|
54873
|
-
//
|
|
54946
|
+
// History); multiplayer realtime convergence runs on the realtime-change
|
|
54947
|
+
// messages of the multiplexed event stream (startEventStream), not on this.
|
|
54874
54948
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
54875
54949
|
var FEED_ICONS = {
|
|
54876
54950
|
insert: '\u2795', update: '\u270F\uFE0F', delete: '\u{1F5D1}',
|
|
@@ -55108,12 +55182,11 @@ var appJs = `
|
|
|
55108
55182
|
if (location.hash !== '#/') location.hash = '#/';
|
|
55109
55183
|
else renderRoute();
|
|
55110
55184
|
loadedTables = {};
|
|
55111
|
-
|
|
55112
|
-
//
|
|
55113
|
-
//
|
|
55114
|
-
// onto this workspace's cards.
|
|
55185
|
+
// A switch swaps the server-side buses to the new workspace; drop the old
|
|
55186
|
+
// workspace's render overlay state and reconnect the multiplexed event
|
|
55187
|
+
// stream so realtime/feed/render all rebind to this workspace.
|
|
55115
55188
|
renderProgress = {};
|
|
55116
|
-
|
|
55189
|
+
startEventStream();
|
|
55117
55190
|
});
|
|
55118
55191
|
}
|
|
55119
55192
|
|
|
@@ -55280,7 +55353,6 @@ var appJs = `
|
|
|
55280
55353
|
// to THIS workspace, and reload its thread list (+ latest convo).
|
|
55281
55354
|
newChat();
|
|
55282
55355
|
clearActivityFeed();
|
|
55283
|
-
startFeed();
|
|
55284
55356
|
refreshThreadList(true);
|
|
55285
55357
|
showToast('Switched workspace', {});
|
|
55286
55358
|
}).catch(function (err) { menu.hidden = true; endWsSwitching(true); showToast('Switch failed: ' + err.message, {}); });
|
|
@@ -55741,7 +55813,7 @@ var appJs = `
|
|
|
55741
55813
|
function rowVisMarkup(tbl, r) {
|
|
55742
55814
|
var a = r._access;
|
|
55743
55815
|
if (!a) return '';
|
|
55744
|
-
var vis = a
|
|
55816
|
+
var vis = effectiveVisibility(a);
|
|
55745
55817
|
var glyph = vis === 'custom' ? '\u25CE' : '\u25C9';
|
|
55746
55818
|
if (!a.ownedByMe) {
|
|
55747
55819
|
var seen = vis === 'custom' ? 'Shared with you' : 'Visible to everyone';
|
|
@@ -56091,7 +56163,7 @@ var appJs = `
|
|
|
56091
56163
|
function detailVisLineEl(row) {
|
|
56092
56164
|
var a = row._access;
|
|
56093
56165
|
if (!a) return '';
|
|
56094
|
-
var vis = a
|
|
56166
|
+
var vis = effectiveVisibility(a);
|
|
56095
56167
|
var labelMap = { everyone: 'Visible to everyone', private: 'Private to you', custom: 'Shared with specific people' };
|
|
56096
56168
|
// Clear visual indicator: a lock when private, an eye when shared (everyone
|
|
56097
56169
|
// or specific people), with a hover tooltip. The shared helper keeps the
|
|
@@ -56102,8 +56174,7 @@ var appJs = `
|
|
|
56102
56174
|
return '<div class="detail-vis muted" style="display:flex;align-items:center;gap:6px;margin:6px 0;font-size:13px">' +
|
|
56103
56175
|
visIcon + '<span>' + escapeHtml(seen) + '</span></div>';
|
|
56104
56176
|
}
|
|
56105
|
-
var info =
|
|
56106
|
-
if (vis === 'custom' && a.grantees) info += ' (' + a.grantees.length + ')';
|
|
56177
|
+
var info = visInfoLabel(a);
|
|
56107
56178
|
var buttons;
|
|
56108
56179
|
if (vis === 'custom') {
|
|
56109
56180
|
// Leaving custom stops the grant list from applying \u2014 the toggle
|
|
@@ -56158,9 +56229,12 @@ var appJs = `
|
|
|
56158
56229
|
if (!panel) return;
|
|
56159
56230
|
if (!panel.hidden) { panel.hidden = true; return; }
|
|
56160
56231
|
var access = row._access || {};
|
|
56161
|
-
|
|
56162
|
-
|
|
56163
|
-
|
|
56232
|
+
// Opening the panel must NOT pre-flip the row to 'custom' \u2014 that left a
|
|
56233
|
+
// row the user never actually shared stuck at "custom (0)". The first
|
|
56234
|
+
// grant flips it to custom server-side (lattice_grant_row); revoking the
|
|
56235
|
+
// last leaves it custom-with-0-grantees, which now reads as private. So
|
|
56236
|
+
// just load the member checklist.
|
|
56237
|
+
var ensure = Promise.resolve();
|
|
56164
56238
|
withBusy(detailVisManage, function () {
|
|
56165
56239
|
return ensure.then(function () {
|
|
56166
56240
|
return fetchJson('/api/cloud/members');
|
|
@@ -56194,8 +56268,13 @@ var appJs = `
|
|
|
56194
56268
|
var at = list.indexOf(role);
|
|
56195
56269
|
if (cb.checked && at === -1) list.push(role);
|
|
56196
56270
|
if (!cb.checked && at !== -1) list.splice(at, 1);
|
|
56271
|
+
// The first grant flips the row to custom server-side; mirror
|
|
56272
|
+
// that locally so the indicator updates. Revoking the last leaves
|
|
56273
|
+
// visibility 'custom' but effectiveVisibility renders custom-0 as
|
|
56274
|
+
// private, so the label flips back to "Private to you".
|
|
56275
|
+
if (list.length > 0) access.visibility = 'custom';
|
|
56197
56276
|
var infoEl = content.querySelector('#detail-vis-info');
|
|
56198
|
-
if (infoEl) infoEl.textContent =
|
|
56277
|
+
if (infoEl) infoEl.textContent = visInfoLabel(access);
|
|
56199
56278
|
invalidate(tableName);
|
|
56200
56279
|
}).catch(function (e) {
|
|
56201
56280
|
cb.checked = !cb.checked; // revert the failed change
|
|
@@ -56203,10 +56282,8 @@ var appJs = `
|
|
|
56203
56282
|
}).then(function () { cb.disabled = false; });
|
|
56204
56283
|
});
|
|
56205
56284
|
});
|
|
56206
|
-
|
|
56207
|
-
|
|
56208
|
-
if (infoEl) infoEl.textContent = 'Shared with specific people (' + (access.grantees || []).length + ')';
|
|
56209
|
-
}
|
|
56285
|
+
var infoEl = content.querySelector('#detail-vis-info');
|
|
56286
|
+
if (infoEl) infoEl.textContent = visInfoLabel(access);
|
|
56210
56287
|
}).catch(function (e) { showToast('Could not load members: ' + e.message, {}); });
|
|
56211
56288
|
});
|
|
56212
56289
|
});
|
|
@@ -58317,6 +58394,29 @@ var appJs = `
|
|
|
58317
58394
|
var EYE_SVG =
|
|
58318
58395
|
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
|
|
58319
58396
|
'<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>';
|
|
58397
|
+
// A custom row with zero grantees is effectively PRIVATE (RLS shows it only to
|
|
58398
|
+
// the owner), so it must read as private, not "Shared with specific people
|
|
58399
|
+
// (0)". Only collapse for the OWNER's own view, where the grantees list is
|
|
58400
|
+
// authoritative \u2014 a member viewing a row shared WITH them gets custom with no
|
|
58401
|
+
// grantees list and must still read as "Shared with you".
|
|
58402
|
+
function effectiveVisibility(access) {
|
|
58403
|
+
if (!access || !access.visibility) return 'private';
|
|
58404
|
+
if (access.visibility === 'custom' && access.ownedByMe &&
|
|
58405
|
+
(!access.grantees || access.grantees.length === 0)) {
|
|
58406
|
+
return 'private';
|
|
58407
|
+
}
|
|
58408
|
+
return access.visibility;
|
|
58409
|
+
}
|
|
58410
|
+
// Owner-facing status label for a row's sharing, honoring effectiveVisibility
|
|
58411
|
+
// (so custom-0 reads as "Private to you") and appending the grantee count only
|
|
58412
|
+
// for a genuine specific-people share.
|
|
58413
|
+
function visInfoLabel(access) {
|
|
58414
|
+
var v = effectiveVisibility(access);
|
|
58415
|
+
var map = { everyone: 'Visible to everyone', private: 'Private to you', custom: 'Shared with specific people' };
|
|
58416
|
+
var s = map[v] || '';
|
|
58417
|
+
if (v === 'custom' && access && access.grantees) s += ' (' + access.grantees.length + ')';
|
|
58418
|
+
return s;
|
|
58419
|
+
}
|
|
58320
58420
|
// Shared per-row lock/eye indicator, reused on the entity-detail header and the
|
|
58321
58421
|
// fs card tiles so the meaning is consistent. The access arg is the server-
|
|
58322
58422
|
// attached row._access (visibility + ownedByMe); returns empty when absent (a
|
|
@@ -58324,7 +58424,7 @@ var appJs = `
|
|
|
58324
58424
|
// A hover tooltip spells out what the lock/eye means (state + ownership aware).
|
|
58325
58425
|
function visIndicator(access, extraClass) {
|
|
58326
58426
|
if (!access || !access.visibility) return '';
|
|
58327
|
-
var vis = access
|
|
58427
|
+
var vis = effectiveVisibility(access);
|
|
58328
58428
|
var tip = vis === 'private'
|
|
58329
58429
|
? 'Private \u2014 only you can see this'
|
|
58330
58430
|
: vis === 'custom'
|
|
@@ -60113,7 +60213,6 @@ var appJs = `
|
|
|
60113
60213
|
|
|
60114
60214
|
|
|
60115
60215
|
// ============ AI assistant rail (2.0) ============
|
|
60116
|
-
var feedSource = null;
|
|
60117
60216
|
var FEED_ICONS = {
|
|
60118
60217
|
insert: '\u2795', update: '\u270F\uFE0F', delete: '\u{1F5D1}',
|
|
60119
60218
|
link: '\u{1F517}', unlink: '\u26D3', undo: '\u21B6', redo: '\u21B7', schema: '\u{1F6E0}',
|
|
@@ -60366,32 +60465,13 @@ var appJs = `
|
|
|
60366
60465
|
else { card.timeEl.textContent = formatElapsed(Math.max(0, evMs - startMs)); }
|
|
60367
60466
|
}
|
|
60368
60467
|
}
|
|
60369
|
-
|
|
60370
|
-
|
|
60371
|
-
|
|
60372
|
-
|
|
60373
|
-
|
|
60374
|
-
|
|
60375
|
-
|
|
60376
|
-
feedSource.addEventListener('feed', function (ev) {
|
|
60377
|
-
var data;
|
|
60378
|
-
try { data = JSON.parse(ev.data); } catch (_) { return; /* ignore malformed */ }
|
|
60379
|
-
try { renderFeedItem(data); } catch (_) { /* render best-effort */ }
|
|
60380
|
-
// Refresh on ANY data mutation, not just schema/new-table events. The
|
|
60381
|
-
// local feed bus delivers every insert/update/delete/link even when
|
|
60382
|
-
// there's no realtime broker (SQLite/local), so this is what makes the
|
|
60383
|
-
// home dashboard counts AND the open entity view live-update without a
|
|
60384
|
-
// manual reload (previously only schema ops or brand-new tables did).
|
|
60385
|
-
// scheduleRealtimeRefresh is debounced (200ms) so a burst from one
|
|
60386
|
-
// ingest still coalesces into a single refetch \u2014 and on Postgres/cloud
|
|
60387
|
-
// it shares that debounce with the realtime 'change' handler (no double
|
|
60388
|
-
// fetch). /api/entities batches its row counts into one query, not N.
|
|
60389
|
-
if (data && (data.table || data.op === 'schema')) {
|
|
60390
|
-
scheduleRealtimeRefresh();
|
|
60391
|
-
}
|
|
60392
|
-
});
|
|
60393
|
-
// EventSource auto-reconnects on error; no extra handling needed.
|
|
60394
|
-
}
|
|
60468
|
+
// Feed events arrive over the multiplexed /api/stream WebSocket and are
|
|
60469
|
+
// handled in dispatchStreamMessage('feed', \u2026): renderFeedItem() paints the
|
|
60470
|
+
// card, then scheduleRealtimeRefresh() refetches on ANY data mutation (the
|
|
60471
|
+
// local feed bus delivers every insert/update/delete/link even with no
|
|
60472
|
+
// realtime broker, so this is what live-updates the dashboard counts and the
|
|
60473
|
+
// open entity view without a manual reload). The 200ms debounce coalesces a
|
|
60474
|
+
// burst into a single refetch and is shared with the realtime 'change' path.
|
|
60395
60475
|
|
|
60396
60476
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
60397
60477
|
// Assistant rail resize \u2014 drag the left edge, clamp, persist.
|
|
@@ -61087,6 +61167,16 @@ var analyticsJs = `
|
|
|
61087
61167
|
page_title: t,
|
|
61088
61168
|
});
|
|
61089
61169
|
},
|
|
61170
|
+
// Set the GA user_id to an OPAQUE per-user hash so active users are
|
|
61171
|
+
// deduplicated across sessions/devices (without it, unique users \u2248 events).
|
|
61172
|
+
// Accepts ONLY a 64-char hex digest \u2014 a raw email or any other PII is
|
|
61173
|
+
// rejected (never sent), preserving the privacy contract above. The caller
|
|
61174
|
+
// hashes the email; this module never sees the plaintext.
|
|
61175
|
+
setUser: function (idHash) {
|
|
61176
|
+
if (!consent || !loaded) return;
|
|
61177
|
+
if (typeof idHash !== 'string' || !/^[a-f0-9]{64}$/.test(idHash)) return;
|
|
61178
|
+
gtag('config', MEASUREMENT_ID, { user_id: idHash });
|
|
61179
|
+
},
|
|
61090
61180
|
};
|
|
61091
61181
|
})();
|
|
61092
61182
|
`;
|
|
@@ -65194,19 +65284,36 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
|
|
|
65194
65284
|
}
|
|
65195
65285
|
let memberOpen = false;
|
|
65196
65286
|
const maskedReadViews = /* @__PURE__ */ new Map();
|
|
65287
|
+
const discoveredJunctions = /* @__PURE__ */ new Set();
|
|
65197
65288
|
if (db.getDialect() === "postgres") {
|
|
65198
65289
|
const peek = new Lattice({ config: configPath }, { encryptionKey });
|
|
65199
65290
|
try {
|
|
65200
65291
|
await peek.init({ introspectOnly: true });
|
|
65201
|
-
|
|
65202
|
-
|
|
65292
|
+
const [rlsInstalled, canManage] = await Promise.all([
|
|
65293
|
+
cloudRlsInstalled(peek),
|
|
65294
|
+
canManageRoles(peek)
|
|
65295
|
+
]);
|
|
65296
|
+
if (rlsInstalled) {
|
|
65297
|
+
memberOpen = !canManage;
|
|
65203
65298
|
if (memberOpen) {
|
|
65204
65299
|
const declared = new Set(db.getRegisteredTableNames());
|
|
65205
|
-
const discovered = await
|
|
65300
|
+
const [discovered, viewsRaw] = await Promise.all([
|
|
65301
|
+
discoverCloudTables(peek),
|
|
65302
|
+
allAsyncOrSync(
|
|
65303
|
+
peek.adapter,
|
|
65304
|
+
`SELECT table_name AS name FROM information_schema.views
|
|
65305
|
+
WHERE table_schema = current_schema() AND table_name LIKE '%\\_v' ESCAPE '\\'`
|
|
65306
|
+
)
|
|
65307
|
+
]);
|
|
65308
|
+
const views = viewsRaw;
|
|
65206
65309
|
const knownTables = /* @__PURE__ */ new Set([...declared, ...discovered.map((t8) => t8.name)]);
|
|
65207
65310
|
for (const t8 of discovered) {
|
|
65208
65311
|
if (declared.has(t8.name)) continue;
|
|
65209
65312
|
if (t8.columns.length === 0) continue;
|
|
65313
|
+
if (isJunctionByColumns(t8.columns)) {
|
|
65314
|
+
discoveredJunctions.add(t8.name);
|
|
65315
|
+
continue;
|
|
65316
|
+
}
|
|
65210
65317
|
db.define(t8.name, {
|
|
65211
65318
|
columns: Object.fromEntries(t8.columns.map((c6) => [c6, "TEXT"])),
|
|
65212
65319
|
...t8.pk.length > 0 ? { primaryKey: t8.pk.length === 1 ? t8.pk[0] : t8.pk } : {},
|
|
@@ -65214,11 +65321,6 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
|
|
|
65214
65321
|
outputFile: `${t8.name}/.lattice/${t8.name}.md`
|
|
65215
65322
|
});
|
|
65216
65323
|
}
|
|
65217
|
-
const views = await allAsyncOrSync(
|
|
65218
|
-
peek.adapter,
|
|
65219
|
-
`SELECT table_name AS name FROM information_schema.views
|
|
65220
|
-
WHERE table_schema = current_schema() AND table_name LIKE '%\\_v' ESCAPE '\\'`
|
|
65221
|
-
);
|
|
65222
65324
|
for (const { name } of views) {
|
|
65223
65325
|
const base = name.slice(0, -2);
|
|
65224
65326
|
if (knownTables.has(base)) maskedReadViews.set(base, name);
|
|
@@ -65255,9 +65357,12 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
|
|
|
65255
65357
|
if (name.startsWith("__lattice_") || name.startsWith("_lattice_")) continue;
|
|
65256
65358
|
validTables.add(name);
|
|
65257
65359
|
}
|
|
65258
|
-
const junctionTables = new Set(
|
|
65259
|
-
getGuiEntities(configPath, outputDir).tables.filter(isJunctionTable).map((t8) => t8.name)
|
|
65260
|
-
|
|
65360
|
+
const junctionTables = /* @__PURE__ */ new Set([
|
|
65361
|
+
...getGuiEntities(configPath, outputDir).tables.filter(isJunctionTable).map((t8) => t8.name),
|
|
65362
|
+
// Member-discovered junctions (classified from the physical shape above);
|
|
65363
|
+
// empty for an owner/local open.
|
|
65364
|
+
...discoveredJunctions
|
|
65365
|
+
]);
|
|
65261
65366
|
const entityContextByTable = db.entityContexts();
|
|
65262
65367
|
const manifest = readManifest(outputDir);
|
|
65263
65368
|
const softDeletable = /* @__PURE__ */ new Set();
|
|
@@ -65458,6 +65563,9 @@ async function changeVisibleToActiveRole(db, payload) {
|
|
|
65458
65563
|
function isDeleteOp(op) {
|
|
65459
65564
|
return op === "delete" || op === "DELETE";
|
|
65460
65565
|
}
|
|
65566
|
+
function isFeedHiddenTable(t8) {
|
|
65567
|
+
return t8.startsWith("_lattice") || t8.startsWith("__lattice") || isInternalNativeEntity(t8);
|
|
65568
|
+
}
|
|
65461
65569
|
function readRelationFor(active, table) {
|
|
65462
65570
|
return active.maskedReadViews.get(table) ?? table;
|
|
65463
65571
|
}
|
|
@@ -65839,158 +65947,10 @@ async function startGuiServer(options) {
|
|
|
65839
65947
|
sendJson(res, { mode, state: active.realtime?.state() ?? "local", connected });
|
|
65840
65948
|
return;
|
|
65841
65949
|
}
|
|
65842
|
-
if (method === "GET" && pathname === "/api/realtime/stream") {
|
|
65843
|
-
res.writeHead(200, {
|
|
65844
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
65845
|
-
"cache-control": "no-store, no-transform",
|
|
65846
|
-
connection: "keep-alive",
|
|
65847
|
-
"x-accel-buffering": "no"
|
|
65848
|
-
});
|
|
65849
|
-
const broker = active.realtime;
|
|
65850
|
-
const initialMode = broker ? "cloud" : "local";
|
|
65851
|
-
const writeEvent = (event, data) => {
|
|
65852
|
-
try {
|
|
65853
|
-
res.write(`event: ${event}
|
|
65854
|
-
data: ${JSON.stringify(data)}
|
|
65855
|
-
|
|
65856
|
-
`);
|
|
65857
|
-
} catch {
|
|
65858
|
-
}
|
|
65859
|
-
};
|
|
65860
|
-
writeEvent("state", {
|
|
65861
|
-
mode: initialMode,
|
|
65862
|
-
state: broker?.state() ?? "local"
|
|
65863
|
-
});
|
|
65864
|
-
const keepalive = setInterval(() => {
|
|
65865
|
-
try {
|
|
65866
|
-
res.write(`: keepalive
|
|
65867
|
-
|
|
65868
|
-
`);
|
|
65869
|
-
} catch {
|
|
65870
|
-
}
|
|
65871
|
-
}, 25e3);
|
|
65872
|
-
const offState = broker?.subscribeState((state2) => {
|
|
65873
|
-
writeEvent("state", { mode: "cloud", state: state2 });
|
|
65874
|
-
});
|
|
65875
|
-
const offPayload = broker?.subscribePayload((payload) => {
|
|
65876
|
-
if (activeRef !== active) return;
|
|
65877
|
-
void changeVisibleToActiveRole(active.db, payload).then((visible) => {
|
|
65878
|
-
if (!visible) return;
|
|
65879
|
-
const out = isDeleteOp(payload.op) ? { ...payload, owner_role: null } : payload;
|
|
65880
|
-
writeEvent("change", out);
|
|
65881
|
-
});
|
|
65882
|
-
});
|
|
65883
|
-
const cleanup = () => {
|
|
65884
|
-
clearInterval(keepalive);
|
|
65885
|
-
if (offState) offState();
|
|
65886
|
-
if (offPayload) offPayload();
|
|
65887
|
-
};
|
|
65888
|
-
req.on("close", cleanup);
|
|
65889
|
-
req.on("error", cleanup);
|
|
65890
|
-
return;
|
|
65891
|
-
}
|
|
65892
|
-
if (method === "GET" && pathname === "/api/feed/stream") {
|
|
65893
|
-
res.writeHead(200, {
|
|
65894
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
65895
|
-
"cache-control": "no-store, no-transform",
|
|
65896
|
-
connection: "keep-alive",
|
|
65897
|
-
"x-accel-buffering": "no"
|
|
65898
|
-
});
|
|
65899
|
-
const writeFeed = (data) => {
|
|
65900
|
-
try {
|
|
65901
|
-
res.write(`event: feed
|
|
65902
|
-
data: ${JSON.stringify(data)}
|
|
65903
|
-
|
|
65904
|
-
`);
|
|
65905
|
-
} catch {
|
|
65906
|
-
}
|
|
65907
|
-
};
|
|
65908
|
-
const keepalive = setInterval(() => {
|
|
65909
|
-
try {
|
|
65910
|
-
res.write(`: keepalive
|
|
65911
|
-
|
|
65912
|
-
`);
|
|
65913
|
-
} catch {
|
|
65914
|
-
}
|
|
65915
|
-
}, 25e3);
|
|
65916
|
-
const recentSelf = /* @__PURE__ */ new Map();
|
|
65917
|
-
const isFeedHiddenTable = (t8) => t8.startsWith("_lattice") || t8.startsWith("__lattice") || isInternalNativeEntity(t8);
|
|
65918
|
-
const offFeed = active.feed.subscribe((e6) => {
|
|
65919
|
-
if (e6.table && isFeedHiddenTable(e6.table)) return;
|
|
65920
|
-
recentSelf.set(`${e6.table ?? ""}:${e6.rowId ?? ""}:${e6.op}`, Date.now());
|
|
65921
|
-
writeFeed(e6);
|
|
65922
|
-
});
|
|
65923
|
-
const offBroker = active.realtime?.subscribePayload((p3) => {
|
|
65924
|
-
const op = feedOpForChange(p3.op);
|
|
65925
|
-
if (!op || !p3.table_name || isFeedHiddenTable(p3.table_name)) return;
|
|
65926
|
-
const tableName = p3.table_name;
|
|
65927
|
-
const key = `${tableName}:${p3.pk ?? ""}:${op}`;
|
|
65928
|
-
const seen = recentSelf.get(key);
|
|
65929
|
-
if (seen && Date.now() - seen < 5e3) return;
|
|
65930
|
-
if (activeRef !== active) return;
|
|
65931
|
-
void changeVisibleToActiveRole(active.db, p3).then((visible) => {
|
|
65932
|
-
if (!visible) return;
|
|
65933
|
-
writeFeed({
|
|
65934
|
-
seq: p3.seq,
|
|
65935
|
-
table: tableName,
|
|
65936
|
-
op,
|
|
65937
|
-
rowId: p3.pk,
|
|
65938
|
-
source: "cli",
|
|
65939
|
-
actor: isDeleteOp(p3.op) ? void 0 : p3.owner_role ?? void 0,
|
|
65940
|
-
ts: p3.created_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
65941
|
-
summary: `${op} on ${tableName} (another client)`
|
|
65942
|
-
});
|
|
65943
|
-
});
|
|
65944
|
-
});
|
|
65945
|
-
const cleanup = () => {
|
|
65946
|
-
clearInterval(keepalive);
|
|
65947
|
-
offFeed();
|
|
65948
|
-
if (offBroker) offBroker();
|
|
65949
|
-
};
|
|
65950
|
-
req.on("close", cleanup);
|
|
65951
|
-
req.on("error", cleanup);
|
|
65952
|
-
return;
|
|
65953
|
-
}
|
|
65954
65950
|
if (method === "GET" && pathname === "/api/render/status") {
|
|
65955
65951
|
sendJson(res, active.renderState);
|
|
65956
65952
|
return;
|
|
65957
65953
|
}
|
|
65958
|
-
if (method === "GET" && pathname === "/api/render/progress") {
|
|
65959
|
-
res.writeHead(200, {
|
|
65960
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
65961
|
-
"cache-control": "no-store, no-transform",
|
|
65962
|
-
connection: "keep-alive",
|
|
65963
|
-
"x-accel-buffering": "no"
|
|
65964
|
-
});
|
|
65965
|
-
const writeEvent = (event, data) => {
|
|
65966
|
-
try {
|
|
65967
|
-
res.write(`event: ${event}
|
|
65968
|
-
data: ${JSON.stringify(data)}
|
|
65969
|
-
|
|
65970
|
-
`);
|
|
65971
|
-
} catch {
|
|
65972
|
-
}
|
|
65973
|
-
};
|
|
65974
|
-
writeEvent("snapshot", active.renderState);
|
|
65975
|
-
const keepalive = setInterval(() => {
|
|
65976
|
-
try {
|
|
65977
|
-
res.write(`: keepalive
|
|
65978
|
-
|
|
65979
|
-
`);
|
|
65980
|
-
} catch {
|
|
65981
|
-
}
|
|
65982
|
-
}, 25e3);
|
|
65983
|
-
const offProgress = active.renderProgress.subscribe((e6) => {
|
|
65984
|
-
writeEvent("progress", e6);
|
|
65985
|
-
});
|
|
65986
|
-
const cleanup = () => {
|
|
65987
|
-
clearInterval(keepalive);
|
|
65988
|
-
offProgress();
|
|
65989
|
-
};
|
|
65990
|
-
req.on("close", cleanup);
|
|
65991
|
-
req.on("error", cleanup);
|
|
65992
|
-
return;
|
|
65993
|
-
}
|
|
65994
65954
|
if (method === "GET" && pathname === "/api/project") {
|
|
65995
65955
|
sendJson(res, getGuiProject(active.configPath, active.outputDir));
|
|
65996
65956
|
return;
|
|
@@ -67508,6 +67468,107 @@ ${e6.stack ?? ""}`
|
|
|
67508
67468
|
}
|
|
67509
67469
|
})();
|
|
67510
67470
|
});
|
|
67471
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
67472
|
+
const handleEventStream = (ws) => {
|
|
67473
|
+
const bound = activeRef;
|
|
67474
|
+
const send = (type, data) => {
|
|
67475
|
+
if (ws.readyState !== WebSocket.OPEN) return;
|
|
67476
|
+
try {
|
|
67477
|
+
ws.send(JSON.stringify({ type, data }));
|
|
67478
|
+
} catch {
|
|
67479
|
+
}
|
|
67480
|
+
};
|
|
67481
|
+
const broker = bound?.realtime ?? null;
|
|
67482
|
+
send("realtime-state", { mode: broker ? "cloud" : "local", state: broker?.state() ?? "local" });
|
|
67483
|
+
if (bound) send("render-snapshot", bound.renderState);
|
|
67484
|
+
const offs = [];
|
|
67485
|
+
if (bound) {
|
|
67486
|
+
if (broker) {
|
|
67487
|
+
offs.push(
|
|
67488
|
+
broker.subscribeState((state2) => {
|
|
67489
|
+
send("realtime-state", { mode: "cloud", state: state2 });
|
|
67490
|
+
})
|
|
67491
|
+
);
|
|
67492
|
+
offs.push(
|
|
67493
|
+
broker.subscribePayload((payload) => {
|
|
67494
|
+
if (activeRef !== bound) return;
|
|
67495
|
+
void changeVisibleToActiveRole(bound.db, payload).then((visible) => {
|
|
67496
|
+
if (!visible) return;
|
|
67497
|
+
const out = isDeleteOp(payload.op) ? { ...payload, owner_role: null } : payload;
|
|
67498
|
+
send("realtime-change", out);
|
|
67499
|
+
});
|
|
67500
|
+
})
|
|
67501
|
+
);
|
|
67502
|
+
}
|
|
67503
|
+
const recentSelf = /* @__PURE__ */ new Map();
|
|
67504
|
+
offs.push(
|
|
67505
|
+
bound.feed.subscribe((e6) => {
|
|
67506
|
+
if (e6.table && isFeedHiddenTable(e6.table)) return;
|
|
67507
|
+
recentSelf.set(`${e6.table ?? ""}:${e6.rowId ?? ""}:${e6.op}`, Date.now());
|
|
67508
|
+
send("feed", e6);
|
|
67509
|
+
})
|
|
67510
|
+
);
|
|
67511
|
+
if (broker) {
|
|
67512
|
+
offs.push(
|
|
67513
|
+
broker.subscribePayload((p3) => {
|
|
67514
|
+
const op = feedOpForChange(p3.op);
|
|
67515
|
+
if (!op || !p3.table_name || isFeedHiddenTable(p3.table_name)) return;
|
|
67516
|
+
const tableName = p3.table_name;
|
|
67517
|
+
const key = `${tableName}:${p3.pk ?? ""}:${op}`;
|
|
67518
|
+
const seen = recentSelf.get(key);
|
|
67519
|
+
if (seen && Date.now() - seen < 5e3) return;
|
|
67520
|
+
if (activeRef !== bound) return;
|
|
67521
|
+
void changeVisibleToActiveRole(bound.db, p3).then((visible) => {
|
|
67522
|
+
if (!visible) return;
|
|
67523
|
+
send("feed", {
|
|
67524
|
+
seq: p3.seq,
|
|
67525
|
+
table: tableName,
|
|
67526
|
+
op,
|
|
67527
|
+
rowId: p3.pk,
|
|
67528
|
+
source: "cli",
|
|
67529
|
+
actor: isDeleteOp(p3.op) ? void 0 : p3.owner_role ?? void 0,
|
|
67530
|
+
ts: p3.created_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
67531
|
+
summary: `${op} on ${tableName} (another client)`
|
|
67532
|
+
});
|
|
67533
|
+
});
|
|
67534
|
+
})
|
|
67535
|
+
);
|
|
67536
|
+
}
|
|
67537
|
+
offs.push(
|
|
67538
|
+
bound.renderProgress.subscribe((e6) => {
|
|
67539
|
+
send("render-progress", e6);
|
|
67540
|
+
})
|
|
67541
|
+
);
|
|
67542
|
+
}
|
|
67543
|
+
const keepalive = setInterval(() => {
|
|
67544
|
+
if (ws.readyState !== WebSocket.OPEN) return;
|
|
67545
|
+
try {
|
|
67546
|
+
ws.ping();
|
|
67547
|
+
} catch {
|
|
67548
|
+
}
|
|
67549
|
+
}, 25e3);
|
|
67550
|
+
const cleanup = () => {
|
|
67551
|
+
clearInterval(keepalive);
|
|
67552
|
+
for (const off of offs) {
|
|
67553
|
+
try {
|
|
67554
|
+
off();
|
|
67555
|
+
} catch {
|
|
67556
|
+
}
|
|
67557
|
+
}
|
|
67558
|
+
};
|
|
67559
|
+
ws.on("close", cleanup);
|
|
67560
|
+
ws.on("error", cleanup);
|
|
67561
|
+
};
|
|
67562
|
+
server.on("upgrade", (req, socket, head) => {
|
|
67563
|
+
const { pathname } = new URL(req.url ?? "/", `http://${host}`);
|
|
67564
|
+
if (pathname !== "/api/stream") {
|
|
67565
|
+
socket.destroy();
|
|
67566
|
+
return;
|
|
67567
|
+
}
|
|
67568
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
67569
|
+
handleEventStream(ws);
|
|
67570
|
+
});
|
|
67571
|
+
});
|
|
67511
67572
|
const port = await listenWithPortFallback(server, startPort, host);
|
|
67512
67573
|
if (activeRef) startBackgroundRender(activeRef);
|
|
67513
67574
|
const displayHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
|
|
@@ -67518,6 +67579,13 @@ ${e6.stack ?? ""}`
|
|
|
67518
67579
|
port,
|
|
67519
67580
|
url,
|
|
67520
67581
|
close: () => new Promise((resolveClose, reject) => {
|
|
67582
|
+
for (const client of wss.clients) {
|
|
67583
|
+
try {
|
|
67584
|
+
client.terminate();
|
|
67585
|
+
} catch {
|
|
67586
|
+
}
|
|
67587
|
+
}
|
|
67588
|
+
wss.close();
|
|
67521
67589
|
server.close((err) => {
|
|
67522
67590
|
if (err) {
|
|
67523
67591
|
reject(err);
|
|
@@ -67715,6 +67783,7 @@ function printHelp() {
|
|
|
67715
67783
|
);
|
|
67716
67784
|
}
|
|
67717
67785
|
function getVersion() {
|
|
67786
|
+
if (true) return "3.3.2";
|
|
67718
67787
|
try {
|
|
67719
67788
|
const pkgPath = new URL("../package.json", import.meta.url).pathname;
|
|
67720
67789
|
const pkg = JSON.parse(readFileSync18(pkgPath, "utf-8"));
|