latticesql 1.16.1 → 1.16.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 +264 -102
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6544,13 +6544,6 @@ var css = `
|
|
|
6544
6544
|
.stat-n { font-size: 24px; font-weight: 700; color: var(--text); }
|
|
6545
6545
|
.stat-tile.warn .stat-n { color: var(--warn); }
|
|
6546
6546
|
.stat-l { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
|
|
6547
|
-
.dash-recent { margin-top: 26px; max-width: 1100px; }
|
|
6548
|
-
.dash-recent-head { font-size: 13px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 10px; }
|
|
6549
|
-
.dash-recent ul { list-style: none; margin: 0; padding: 0; }
|
|
6550
|
-
.dash-act { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 14px; }
|
|
6551
|
-
.dash-act-ic { width: 18px; text-align: center; }
|
|
6552
|
-
.dash-act-txt { flex: 1; color: var(--text); }
|
|
6553
|
-
.dash-act-time { font-size: 12px; color: var(--text-muted); }
|
|
6554
6547
|
|
|
6555
6548
|
/* \u2500\u2500 Table view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6556
6549
|
.view-header {
|
|
@@ -7062,6 +7055,8 @@ var css = `
|
|
|
7062
7055
|
transition: transform 0.05s ease, border-color 0.15s ease, box-shadow 0.15s ease;
|
|
7063
7056
|
}
|
|
7064
7057
|
.fs-tile:hover { border-color: var(--accent); transform: translateY(-1px); }
|
|
7058
|
+
.fs-tile-create { border-style: dashed; background: transparent; }
|
|
7059
|
+
.fs-tile-create .fs-tile-icon { color: var(--accent); }
|
|
7065
7060
|
.fs-tile-icon { font-size: 40px; line-height: 1; }
|
|
7066
7061
|
.fs-tile-label {
|
|
7067
7062
|
font-size: 13px; font-weight: 500; color: var(--text);
|
|
@@ -8298,7 +8293,6 @@ var appJs = `
|
|
|
8298
8293
|
entities: tables.map(function (t) {
|
|
8299
8294
|
return { name: t.name, rowCount: t.rowCount, lastUpdatedAt: null, stale: false };
|
|
8300
8295
|
}),
|
|
8301
|
-
recent: [],
|
|
8302
8296
|
};
|
|
8303
8297
|
}
|
|
8304
8298
|
function drawDashboard(content, d) {
|
|
@@ -8336,21 +8330,7 @@ var appJs = `
|
|
|
8336
8330
|
fresh +
|
|
8337
8331
|
'</a>';
|
|
8338
8332
|
}).join('');
|
|
8339
|
-
|
|
8340
|
-
if (d.recent && d.recent.length) {
|
|
8341
|
-
var items = d.recent.map(function (r) {
|
|
8342
|
-
var ic = FEED_ICONS[r.op] || '\u2022';
|
|
8343
|
-
var label = displayFor(r.table).label;
|
|
8344
|
-
return '<li class="dash-act">' +
|
|
8345
|
-
'<span class="dash-act-ic">' + ic + '</span>' +
|
|
8346
|
-
'<span class="dash-act-txt">' + escapeHtml(String(r.op)) + ' \xB7 ' + escapeHtml(label) + '</span>' +
|
|
8347
|
-
'<span class="dash-act-time">' + relTime(r.ts) + '</span>' +
|
|
8348
|
-
'</li>';
|
|
8349
|
-
}).join('');
|
|
8350
|
-
recent = '<div class="dash-recent"><div class="dash-recent-head">Recent activity</div>' +
|
|
8351
|
-
'<ul>' + items + '</ul></div>';
|
|
8352
|
-
}
|
|
8353
|
-
content.innerHTML = stats + '<div class="dashboard">' + cards + '</div>' + recent;
|
|
8333
|
+
content.innerHTML = stats + '<div class="dashboard">' + cards + '</div>';
|
|
8354
8334
|
}
|
|
8355
8335
|
function renderDashboard(content) {
|
|
8356
8336
|
// Workspace overview: counts + freshness + recent activity from
|
|
@@ -9230,7 +9210,16 @@ var appJs = `
|
|
|
9230
9210
|
return rowsP.then(function (rows) {
|
|
9231
9211
|
var d = displayFor(table);
|
|
9232
9212
|
var base = fsHref(segs);
|
|
9233
|
-
|
|
9213
|
+
// "New" tile (top-level collections only) \u2014 a folder box with a + that
|
|
9214
|
+
// opens a create form. Related-row folders aren't a place to mint a
|
|
9215
|
+
// brand-new object, so the tile is top-level only.
|
|
9216
|
+
var createTile = topLevel
|
|
9217
|
+
? '<a class="fs-tile fs-tile-create" href="#" data-fs-create="1" title="Create a new ' + escapeHtml(d.label) + '">' +
|
|
9218
|
+
'<div class="fs-tile-icon">\u2795</div>' +
|
|
9219
|
+
'<div class="fs-tile-label">New ' + escapeHtml(d.label) + '</div>' +
|
|
9220
|
+
'</a>'
|
|
9221
|
+
: '';
|
|
9222
|
+
var rowTiles = rows.length
|
|
9234
9223
|
? rows.map(function (r) {
|
|
9235
9224
|
var icon = (table === 'files') ? fileEmoji(r) : '\u{1F4C1}';
|
|
9236
9225
|
return '<a class="fs-tile" href="' + base + '/' + encodeURIComponent(r.id) + '">' +
|
|
@@ -9238,7 +9227,7 @@ var appJs = `
|
|
|
9238
9227
|
'<div class="fs-tile-label">' + escapeHtml(fsDisplayName(r)) + '</div>' +
|
|
9239
9228
|
'</a>';
|
|
9240
9229
|
}).join('')
|
|
9241
|
-
: '<div class="fs-empty">Nothing here yet.</div>';
|
|
9230
|
+
: (topLevel ? '' : '<div class="fs-empty">Nothing here yet.</div>');
|
|
9242
9231
|
content.innerHTML =
|
|
9243
9232
|
fsBreadcrumb(segs, crumbs) +
|
|
9244
9233
|
'<div class="view-header">' +
|
|
@@ -9246,13 +9235,111 @@ var appJs = `
|
|
|
9246
9235
|
'<h1>' + escapeHtml(d.label) + '</h1>' +
|
|
9247
9236
|
'<span class="count">' + rows.length + ' item' + (rows.length === 1 ? '' : 's') + '</span>' +
|
|
9248
9237
|
'</div>' +
|
|
9249
|
-
'<div class="fs-grid">' +
|
|
9238
|
+
'<div class="fs-grid">' + createTile + rowTiles + '</div>';
|
|
9239
|
+
var ctile = content.querySelector('[data-fs-create]');
|
|
9240
|
+
if (ctile) ctile.addEventListener('click', function (e) {
|
|
9241
|
+
e.preventDefault();
|
|
9242
|
+
openFsCreateModal(content, table, segs);
|
|
9243
|
+
});
|
|
9250
9244
|
});
|
|
9251
9245
|
}).catch(function (err) {
|
|
9252
9246
|
content.innerHTML = '<div class="placeholder"><h2>Failed</h2>' + escapeHtml(err.message) + '</div>';
|
|
9253
9247
|
});
|
|
9254
9248
|
}
|
|
9255
9249
|
|
|
9250
|
+
// Create a new object from the simple view \u2014 a form styled like the item
|
|
9251
|
+
// page with blank fields + a Save button, plus a select-menu + "+" for each
|
|
9252
|
+
// many-to-many link. Reuses fieldFor() (intrinsic + belongsTo) and the
|
|
9253
|
+
// existing row-create + junction-row endpoints (no new backend).
|
|
9254
|
+
function openFsCreateModal(content, table, segs) {
|
|
9255
|
+
var t = tableByName(table);
|
|
9256
|
+
if (!t) return;
|
|
9257
|
+
var bt = belongsToColumns(t);
|
|
9258
|
+
var juncs = junctionsFor(table);
|
|
9259
|
+
// Preload FK + junction-remote target rows so the <select> menus populate.
|
|
9260
|
+
var needed = bt.map(function (b) { return b.rel.table; })
|
|
9261
|
+
.concat(juncs.map(function (j) { return j.remoteRel.table; }));
|
|
9262
|
+
Promise.all(needed.map(loadAllRows)).then(function () {
|
|
9263
|
+
var fieldsHtml = '';
|
|
9264
|
+
intrinsicColumns(t).forEach(function (c) {
|
|
9265
|
+
fieldsHtml += '<div class="fs-field"><div class="fs-field-label">' + escapeHtml(titleCase(c)) + '</div>' +
|
|
9266
|
+
'<div class="fs-field-val">' + fieldFor(c, '', t) + '</div></div>';
|
|
9267
|
+
});
|
|
9268
|
+
bt.forEach(function (b) {
|
|
9269
|
+
fieldsHtml += '<div class="fs-field"><div class="fs-field-label">' + escapeHtml(titleCase(b.relName)) + '</div>' +
|
|
9270
|
+
'<div class="fs-field-val">' + fieldFor(b.rel.foreignKey, '', t) + '</div></div>';
|
|
9271
|
+
});
|
|
9272
|
+
juncs.forEach(function (j) {
|
|
9273
|
+
var remoteRows = loadedTables[j.remoteRel.table] || [];
|
|
9274
|
+
var opts = '<option value="">(none)</option>' + remoteRows.map(function (r) {
|
|
9275
|
+
return '<option value="' + escapeHtml(r.id) + '">' + escapeHtml(displayNameFor(r)) + '</option>';
|
|
9276
|
+
}).join('');
|
|
9277
|
+
fieldsHtml += '<div class="fs-field"><div class="fs-field-label">' + escapeHtml(titleCase(j.remoteRel.table)) + ' (links)</div>' +
|
|
9278
|
+
'<div class="fs-field-val">' +
|
|
9279
|
+
'<div class="fs-link-stage" data-junction="' + escapeHtml(j.junction) + '" data-local-fk="' + escapeHtml(j.localFk) + '" data-remote-fk="' + escapeHtml(j.remoteRel.foreignKey) + '">' +
|
|
9280
|
+
'<select class="fs-link-select">' + opts + '</select>' +
|
|
9281
|
+
'</div>' +
|
|
9282
|
+
'<button type="button" class="btn fs-link-add">+ Add another</button>' +
|
|
9283
|
+
'</div></div>';
|
|
9284
|
+
});
|
|
9285
|
+
showModal('New ' + displayFor(table).label, '<div class="fs-doc fs-create-form">' + fieldsHtml + '</div>', {
|
|
9286
|
+
primaryLabel: 'Save',
|
|
9287
|
+
onBody: function (backdrop) {
|
|
9288
|
+
backdrop.querySelectorAll('.fs-link-add').forEach(function (addBtn) {
|
|
9289
|
+
addBtn.addEventListener('click', function () {
|
|
9290
|
+
var stage = addBtn.previousElementSibling; // the .fs-link-stage
|
|
9291
|
+
var firstSel = stage && stage.querySelector('.fs-link-select');
|
|
9292
|
+
if (!firstSel) return;
|
|
9293
|
+
var clone = firstSel.cloneNode(true);
|
|
9294
|
+
clone.value = '';
|
|
9295
|
+
stage.appendChild(clone);
|
|
9296
|
+
});
|
|
9297
|
+
});
|
|
9298
|
+
},
|
|
9299
|
+
onSubmit: function (scope) {
|
|
9300
|
+
// Intrinsic + belongsTo values (the [name] inputs/selects).
|
|
9301
|
+
var values = {};
|
|
9302
|
+
scope.querySelectorAll('.fs-create-form [name]').forEach(function (el) {
|
|
9303
|
+
var v = el.value;
|
|
9304
|
+
if (v !== '' && v != null) values[el.getAttribute('name')] = v;
|
|
9305
|
+
});
|
|
9306
|
+
// Staged many-to-many links \u2014 one junction row per chosen target.
|
|
9307
|
+
var links = [];
|
|
9308
|
+
scope.querySelectorAll('.fs-link-stage').forEach(function (stage) {
|
|
9309
|
+
var junction = stage.getAttribute('data-junction');
|
|
9310
|
+
var localFk = stage.getAttribute('data-local-fk');
|
|
9311
|
+
var remoteFk = stage.getAttribute('data-remote-fk');
|
|
9312
|
+
stage.querySelectorAll('.fs-link-select').forEach(function (sel) {
|
|
9313
|
+
if (sel.value) links.push({ junction: junction, localFk: localFk, remoteFk: remoteFk, remoteId: sel.value });
|
|
9314
|
+
});
|
|
9315
|
+
});
|
|
9316
|
+
return rowWrite('POST', '/api/tables/' + encodeURIComponent(table) + '/rows', values).then(function (res) {
|
|
9317
|
+
var newId = res && (res.id || (res.row && res.row.id));
|
|
9318
|
+
var chain = Promise.resolve();
|
|
9319
|
+
links.forEach(function (lk) {
|
|
9320
|
+
chain = chain.then(function () {
|
|
9321
|
+
// Use the junction's /link endpoint (INSERT OR IGNORE on the
|
|
9322
|
+
// two FK columns) \u2014 works for junctions with no own pk and is
|
|
9323
|
+
// idempotent, unlike a raw row insert.
|
|
9324
|
+
var jrow = {};
|
|
9325
|
+
jrow[lk.localFk] = newId;
|
|
9326
|
+
jrow[lk.remoteFk] = lk.remoteId;
|
|
9327
|
+
return rowWrite('POST', '/api/tables/' + encodeURIComponent(lk.junction) + '/link', jrow);
|
|
9328
|
+
});
|
|
9329
|
+
});
|
|
9330
|
+
return chain;
|
|
9331
|
+
}).then(function () {
|
|
9332
|
+
invalidate(table);
|
|
9333
|
+
return refreshEntities();
|
|
9334
|
+
}).then(function () {
|
|
9335
|
+
showToast('Created', {});
|
|
9336
|
+
renderFsCollection(content, segs);
|
|
9337
|
+
});
|
|
9338
|
+
},
|
|
9339
|
+
});
|
|
9340
|
+
}).catch(function (err) { showToast('Create failed: ' + err.message, {}); });
|
|
9341
|
+
}
|
|
9342
|
+
|
|
9256
9343
|
// Item view \u2014 one row as a document (click-to-edit) + its relationship folders.
|
|
9257
9344
|
function renderFsItem(content, segs) {
|
|
9258
9345
|
fsWalk(segs).then(function (crumbs) {
|
|
@@ -9597,8 +9684,11 @@ var appJs = `
|
|
|
9597
9684
|
if (!s) return '';
|
|
9598
9685
|
try {
|
|
9599
9686
|
var d = new Date(s);
|
|
9687
|
+
// Never render the literal "Invalid Date" \u2014 new Date() returns an
|
|
9688
|
+
// Invalid Date (not a throw) for an unparseable value.
|
|
9689
|
+
if (isNaN(d.getTime())) return '(no timestamp)';
|
|
9600
9690
|
return d.toLocaleString();
|
|
9601
|
-
} catch (_e) { return
|
|
9691
|
+
} catch (_e) { return '(no timestamp)'; }
|
|
9602
9692
|
}
|
|
9603
9693
|
|
|
9604
9694
|
/** Side-by-side-ish text diff. Shows changed columns only for updates. */
|
|
@@ -11097,12 +11187,12 @@ var appJs = `
|
|
|
11097
11187
|
fetchJson('/api/userconfig/databases').then(function (cat) {
|
|
11098
11188
|
var localRows = (cat.local || []).map(function (d) {
|
|
11099
11189
|
var stateBadge = '<span style="font-family:JetBrains Mono,monospace;font-size:10px;color:var(--text-muted)">' + escapeHtml((d.state || 'local').toUpperCase()) + '</span>';
|
|
11100
|
-
return '<tr>' +
|
|
11190
|
+
return '<tr' + (d.active ? '' : ' class="db-row" data-switch-path="' + escapeHtml(d.configPath) + '"') + '>' +
|
|
11101
11191
|
'<td>' + escapeHtml(d.label) + (d.active ? ' <span class="role-tag">active</span>' : '') + '</td>' +
|
|
11102
11192
|
'<td>SQLite</td>' +
|
|
11103
11193
|
'<td>' + stateBadge + '</td>' +
|
|
11104
11194
|
'<td><code>' + escapeHtml(d.dbFile) + '</code></td>' +
|
|
11105
|
-
'<td
|
|
11195
|
+
'<td>\u2014</td>' +
|
|
11106
11196
|
'</tr>';
|
|
11107
11197
|
}).join('');
|
|
11108
11198
|
var cloudRows = (cat.cloud || []).map(function (d) {
|
|
@@ -11120,9 +11210,9 @@ var appJs = `
|
|
|
11120
11210
|
'<tbody>' + (localRows + cloudRows || '<tr><td colspan="5" style="padding:8px;color:var(--text-muted)">No databases configured.</td></tr>') + '</tbody>' +
|
|
11121
11211
|
'</table>' +
|
|
11122
11212
|
'</div>';
|
|
11123
|
-
host.querySelectorAll('[data-switch]').forEach(function (
|
|
11124
|
-
|
|
11125
|
-
var configPath =
|
|
11213
|
+
host.querySelectorAll('tr.db-row[data-switch-path]').forEach(function (row) {
|
|
11214
|
+
row.addEventListener('click', function () {
|
|
11215
|
+
var configPath = row.getAttribute('data-switch-path');
|
|
11126
11216
|
fetch('/api/databases/switch', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path: configPath }) })
|
|
11127
11217
|
.then(function (r) { return r.json(); })
|
|
11128
11218
|
.then(function () { renderUserConfig(document.getElementById('content')); });
|
|
@@ -11223,11 +11313,74 @@ var appJs = `
|
|
|
11223
11313
|
|
|
11224
11314
|
function renderDatabaseDangerZone(host) {
|
|
11225
11315
|
if (!host) return;
|
|
11226
|
-
|
|
11316
|
+
Promise.all([
|
|
11317
|
+
fetchJson('/api/databases'),
|
|
11318
|
+
fetchJson('/api/dbconfig').catch(function () { return {}; }),
|
|
11319
|
+
]).then(function (results) {
|
|
11320
|
+
var data = results[0];
|
|
11321
|
+
var cfg = results[1] || {};
|
|
11227
11322
|
var current = (data && data.current) || {};
|
|
11228
11323
|
var label = current.label || current.dbFile || '';
|
|
11229
11324
|
var path = current.path || '';
|
|
11230
11325
|
if (!path) { host.innerHTML = ''; return; }
|
|
11326
|
+
|
|
11327
|
+
// After tearing down / leaving the active DB, switch to another the
|
|
11328
|
+
// operator still has and navigate off the (now-gone) page.
|
|
11329
|
+
var switchAway = function () {
|
|
11330
|
+
var cur = (data && data.current && data.current.path) || null;
|
|
11331
|
+
var target = ((data && data.configs) || []).filter(function (c) { return c.path !== cur; })[0];
|
|
11332
|
+
var p = target
|
|
11333
|
+
? fetchJson('/api/databases/switch', {
|
|
11334
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
11335
|
+
body: JSON.stringify({ path: target.path }),
|
|
11336
|
+
}).then(function () { return reloadEverything(); })
|
|
11337
|
+
: reloadEverything();
|
|
11338
|
+
return p.then(function () { location.hash = '#/'; renderRoute(); });
|
|
11339
|
+
};
|
|
11340
|
+
|
|
11341
|
+
if (cfg.state === 'team-cloud-creator') {
|
|
11342
|
+
// Owner: disconnect the database from the cloud \u2014 kicks all members.
|
|
11343
|
+
host.innerHTML =
|
|
11344
|
+
'<div class="danger-zone">' +
|
|
11345
|
+
'<h3>Danger zone</h3>' +
|
|
11346
|
+
'<p style="font-size:12px;color:var(--text-muted);margin:0 0 10px">' +
|
|
11347
|
+
'Disconnect this database from the cloud. This removes the team and <strong>kicks all members</strong>. This cannot be undone.' +
|
|
11348
|
+
'</p>' +
|
|
11349
|
+
'<button class="btn destructive" id="db-disconnect-btn">Disconnect from cloud</button>' +
|
|
11350
|
+
'</div>';
|
|
11351
|
+
host.querySelector('#db-disconnect-btn').addEventListener('click', function () {
|
|
11352
|
+
if (!confirm('Disconnect "' + (cfg.teamName || label || 'this database') + '" from the cloud? This kicks all members and cannot be undone.')) return;
|
|
11353
|
+
var dbtn = host.querySelector('#db-disconnect-btn');
|
|
11354
|
+
withBusy(dbtn, function () {
|
|
11355
|
+
return fetchJson('/api/teams-gui/teams/' + cfg.teamId, { method: 'DELETE' })
|
|
11356
|
+
.then(function () { showToast('Disconnected from cloud', {}); return switchAway(); })
|
|
11357
|
+
.catch(function (e) { alert('Disconnect failed: ' + e.message); });
|
|
11358
|
+
});
|
|
11359
|
+
});
|
|
11360
|
+
return;
|
|
11361
|
+
}
|
|
11362
|
+
if (cfg.state === 'team-cloud-member') {
|
|
11363
|
+
// Member: leave the team. The cloud DB keeps running for others.
|
|
11364
|
+
host.innerHTML =
|
|
11365
|
+
'<div class="danger-zone">' +
|
|
11366
|
+
'<h3>Danger zone</h3>' +
|
|
11367
|
+
'<p style="font-size:12px;color:var(--text-muted);margin:0 0 10px">' +
|
|
11368
|
+
'Leave this team. The cloud database keeps running for everyone else; you simply stop being a member.' +
|
|
11369
|
+
'</p>' +
|
|
11370
|
+
'<button class="btn destructive" id="db-leave-btn">Leave team</button>' +
|
|
11371
|
+
'</div>';
|
|
11372
|
+
host.querySelector('#db-leave-btn').addEventListener('click', function () {
|
|
11373
|
+
if (!confirm('Leave "' + (cfg.teamName || label || 'this team') + '"?')) return;
|
|
11374
|
+
var lbtn = host.querySelector('#db-leave-btn');
|
|
11375
|
+
withBusy(lbtn, function () {
|
|
11376
|
+
return fetchJson('/api/teams-gui/teams/' + cfg.teamId + '/members/' + encodeURIComponent(cfg.myUserId), { method: 'DELETE' })
|
|
11377
|
+
.then(function () { showToast('Left the team', {}); return switchAway(); })
|
|
11378
|
+
.catch(function (e) { alert('Leave failed: ' + e.message); });
|
|
11379
|
+
});
|
|
11380
|
+
});
|
|
11381
|
+
return;
|
|
11382
|
+
}
|
|
11383
|
+
// Local / non-team cloud database: delete it.
|
|
11231
11384
|
host.innerHTML =
|
|
11232
11385
|
'<div class="danger-zone">' +
|
|
11233
11386
|
'<h3>Danger zone</h3>' +
|
|
@@ -11328,12 +11481,11 @@ var appJs = `
|
|
|
11328
11481
|
: '\u2014';
|
|
11329
11482
|
var rowLabel = c.label || c.name;
|
|
11330
11483
|
var del = '<button class="btn danger" data-delete-path="' + escapeHtml(c.path) + '" data-delete-label="' + escapeHtml(rowLabel) + '">Delete</button>';
|
|
11331
|
-
|
|
11332
|
-
return '<tr>' +
|
|
11484
|
+
return '<tr' + (c.active ? '' : ' class="ws-row" data-switch-path="' + escapeHtml(c.path) + '"') + '>' +
|
|
11333
11485
|
'<td>' + escapeHtml(rowLabel) + (c.active ? ' <span class="role-tag">active</span>' : '') + '</td>' +
|
|
11334
11486
|
'<td>' + kind + '</td>' +
|
|
11335
11487
|
'<td><code>' + escapeHtml(c.dbFile || '') + '</code></td>' +
|
|
11336
|
-
'<td>' +
|
|
11488
|
+
'<td>' + del + '</td>' +
|
|
11337
11489
|
'</tr>';
|
|
11338
11490
|
}).join('');
|
|
11339
11491
|
host.innerHTML =
|
|
@@ -11347,9 +11499,9 @@ var appJs = `
|
|
|
11347
11499
|
'<tbody>' + (rows || '<tr><td colspan="4" style="padding:8px;color:var(--text-muted)">No workspaces configured.</td></tr>') + '</tbody>' +
|
|
11348
11500
|
'</table>' +
|
|
11349
11501
|
'</div>';
|
|
11350
|
-
host.querySelectorAll('[data-switch]').forEach(function (
|
|
11351
|
-
|
|
11352
|
-
var configPath =
|
|
11502
|
+
host.querySelectorAll('tr.ws-row[data-switch-path]').forEach(function (row) {
|
|
11503
|
+
row.addEventListener('click', function () {
|
|
11504
|
+
var configPath = row.getAttribute('data-switch-path');
|
|
11353
11505
|
fetch('/api/databases/switch', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path: configPath }) })
|
|
11354
11506
|
.then(function (r) { return r.json(); })
|
|
11355
11507
|
.then(function () { return reloadEverything(); })
|
|
@@ -11357,7 +11509,8 @@ var appJs = `
|
|
|
11357
11509
|
});
|
|
11358
11510
|
});
|
|
11359
11511
|
host.querySelectorAll('[data-delete-path]').forEach(function (btn) {
|
|
11360
|
-
btn.addEventListener('click', function () {
|
|
11512
|
+
btn.addEventListener('click', function (e) {
|
|
11513
|
+
e.stopPropagation(); // don't trigger the row's switch handler
|
|
11361
11514
|
confirmDeleteDatabase(
|
|
11362
11515
|
btn.getAttribute('data-delete-path'),
|
|
11363
11516
|
btn.getAttribute('data-delete-label'),
|
|
@@ -11461,10 +11614,10 @@ var appJs = `
|
|
|
11461
11614
|
(isCreator ? ' \xB7 <span style="color:var(--accent)">you are the creator</span>' : ' \xB7 <span style="color:var(--text-muted)">member</span>') +
|
|
11462
11615
|
'</div>' +
|
|
11463
11616
|
'<div class="team-actions" style="margin-top:10px">' +
|
|
11464
|
-
(isCreator ? '<button class="btn primary" data-act="open-invite">
|
|
11617
|
+
(isCreator ? '<button class="btn primary" data-act="open-invite">Invite member</button>' : '') +
|
|
11465
11618
|
'</div>' +
|
|
11466
|
-
//
|
|
11467
|
-
// the
|
|
11619
|
+
// Exit actions (Disconnect for the owner / Leave for a member) live
|
|
11620
|
+
// in the Danger Zone below \u2014 not on a member row.
|
|
11468
11621
|
'<div id="db-members-host" style="margin-top:12px"><div style="font-size:12px;color:var(--text-muted)">Loading members\u2026</div></div>'
|
|
11469
11622
|
);
|
|
11470
11623
|
}
|
|
@@ -11523,27 +11676,10 @@ var appJs = `
|
|
|
11523
11676
|
var myUserId = info.myUserId;
|
|
11524
11677
|
var isCreator = !!info.isCreator;
|
|
11525
11678
|
|
|
11526
|
-
// After leaving/destroying, the left DB is torn down on the backend
|
|
11527
|
-
// (config + credential removed). Switch to another database the
|
|
11528
|
-
// operator still has and navigate off the (now-gone) DB page.
|
|
11529
|
-
var switchAway = function () {
|
|
11530
|
-
return fetchJson('/api/databases').then(function (data) {
|
|
11531
|
-
var current = (data && data.current && data.current.path) || null;
|
|
11532
|
-
var configs = (data && data.configs) || [];
|
|
11533
|
-
var target = configs.filter(function (c) { return c.path !== current; })[0];
|
|
11534
|
-
if (!target) return reloadEverything();
|
|
11535
|
-
return fetchJson('/api/databases/switch', {
|
|
11536
|
-
method: 'POST',
|
|
11537
|
-
headers: { 'content-type': 'application/json' },
|
|
11538
|
-
body: JSON.stringify({ path: target.path }),
|
|
11539
|
-
}).then(function () { return reloadEverything(); });
|
|
11540
|
-
}).then(function () { location.hash = '#/'; renderRoute(); });
|
|
11541
|
-
};
|
|
11542
|
-
|
|
11543
11679
|
var inviteBtn = host.querySelector('[data-act="open-invite"]');
|
|
11544
11680
|
if (inviteBtn) inviteBtn.addEventListener('click', function () {
|
|
11545
11681
|
if (!teamId) { alert('No team is active.'); return; }
|
|
11546
|
-
showInviteByEmailModal(teamId);
|
|
11682
|
+
showInviteByEmailModal(teamId, info);
|
|
11547
11683
|
});
|
|
11548
11684
|
|
|
11549
11685
|
// Inline member list for the active team cloud. Marks "you"; your
|
|
@@ -11567,26 +11703,6 @@ var appJs = `
|
|
|
11567
11703
|
});
|
|
11568
11704
|
});
|
|
11569
11705
|
});
|
|
11570
|
-
// Leave (member) / Destroy team (creator) \u2014 your own row.
|
|
11571
|
-
var selfBtn = membersHost.querySelector('[data-act="leave-self"]');
|
|
11572
|
-
if (selfBtn) selfBtn.addEventListener('click', function () {
|
|
11573
|
-
|
|
11574
|
-
if (isCreator) {
|
|
11575
|
-
if (!confirm('Destroy team "' + (info.teamName || 'this team') + '"? This soft-deletes it on the cloud for everyone.')) return;
|
|
11576
|
-
withBusy(selfBtn, function () {
|
|
11577
|
-
return fetchJson('/api/teams-gui/teams/' + teamId, { method: 'DELETE' })
|
|
11578
|
-
.then(function () { showToast('Team destroyed', {}); return switchAway(); })
|
|
11579
|
-
.catch(function (e) { setMsg('Destroy failed: ' + e.message, false); });
|
|
11580
|
-
});
|
|
11581
|
-
} else {
|
|
11582
|
-
if (!confirm('Leave team "' + (info.teamName || 'this team') + '"?')) return;
|
|
11583
|
-
withBusy(selfBtn, function () {
|
|
11584
|
-
return fetchJson('/api/teams-gui/teams/' + teamId + '/members/' + encodeURIComponent(myUserId), { method: 'DELETE' })
|
|
11585
|
-
.then(function () { showToast('Left the team', {}); return switchAway(); })
|
|
11586
|
-
.catch(function (e) { setMsg('Leave failed: ' + e.message, false); });
|
|
11587
|
-
});
|
|
11588
|
-
}
|
|
11589
|
-
});
|
|
11590
11706
|
}).catch(function () { membersHost.innerHTML = '<div style="font-size:12px;color:var(--text-muted)">Members unavailable.</div>'; });
|
|
11591
11707
|
}
|
|
11592
11708
|
|
|
@@ -11932,13 +12048,11 @@ var appJs = `
|
|
|
11932
12048
|
var rows = members.map(function (m) {
|
|
11933
12049
|
var label = m.name || m.email || '(unknown)';
|
|
11934
12050
|
var isSelf = m.user_id === myUserId;
|
|
11935
|
-
//
|
|
11936
|
-
//
|
|
12051
|
+
// Other rows: Kick, but only the creator may remove other members.
|
|
12052
|
+
// Your own exit (Disconnect for the owner / Leave for a member) lives
|
|
12053
|
+
// in the Danger Zone, not on a member row.
|
|
11937
12054
|
var btn = '';
|
|
11938
|
-
if (isSelf) {
|
|
11939
|
-
btn = '<button class="btn danger-btn" data-act="leave-self">' +
|
|
11940
|
-
(isCreator ? 'Destroy team' : 'Leave') + '</button>';
|
|
11941
|
-
} else if (isCreator) {
|
|
12055
|
+
if (!isSelf && isCreator) {
|
|
11942
12056
|
btn = '<button class="btn danger-btn" data-act="kick">Kick</button>';
|
|
11943
12057
|
}
|
|
11944
12058
|
return '<div class="member-row" data-user-id="' + escapeHtml(m.user_id) + '">' +
|
|
@@ -11953,7 +12067,7 @@ var appJs = `
|
|
|
11953
12067
|
return '<div class="members-list"><h4>Members</h4>' + rows + '</div>';
|
|
11954
12068
|
}
|
|
11955
12069
|
|
|
11956
|
-
function showInviteByEmailModal(teamId) {
|
|
12070
|
+
function showInviteByEmailModal(teamId, info) {
|
|
11957
12071
|
var bodyHtml =
|
|
11958
12072
|
'<div class="field"><label>Invitee email</label>' +
|
|
11959
12073
|
'<input name="invitee_email" type="email" placeholder="bob@example.com" /></div>' +
|
|
@@ -11969,17 +12083,30 @@ var appJs = `
|
|
|
11969
12083
|
method: 'POST',
|
|
11970
12084
|
headers: { 'content-type': 'application/json' },
|
|
11971
12085
|
body: JSON.stringify({ invitee_email: data.invitee_email }),
|
|
11972
|
-
}).then(function (inv) { showInviteTokenModal(inv); });
|
|
12086
|
+
}).then(function (inv) { showInviteTokenModal(inv, info); });
|
|
11973
12087
|
},
|
|
11974
12088
|
});
|
|
11975
12089
|
}
|
|
11976
12090
|
|
|
11977
|
-
function showInviteTokenModal(inv) {
|
|
12091
|
+
function showInviteTokenModal(inv, info) {
|
|
12092
|
+
info = info || {};
|
|
12093
|
+
// The invitee needs the cloud connection string AND the token. Show the
|
|
12094
|
+
// URL with the password MASKED \u2014 redeem never needs the owner's password
|
|
12095
|
+
// (the invitee authenticates with their own credentials).
|
|
12096
|
+
var connStr = info.host
|
|
12097
|
+
? 'postgres://' + (info.user || 'user') + ':****@' + info.host + ':' + (info.port || 5432) + '/' + (info.dbname || '')
|
|
12098
|
+
: '';
|
|
12099
|
+
var connBlock = connStr
|
|
12100
|
+
? '<h4 style="margin:14px 0 4px">Cloud connection</h4>' +
|
|
12101
|
+
'<div class="copy-token" id="copy-conn">' + escapeHtml(connStr) + '</div>' +
|
|
12102
|
+
'<p style="font-size:12px;color:var(--text-muted);margin:4px 0 0">Share this URL with the invitee (password masked). Click to copy.</p>'
|
|
12103
|
+
: '';
|
|
11978
12104
|
var bodyHtml =
|
|
11979
12105
|
'<p style="margin-top:0">Share this token with the invitee (one-time use). It expires at <code>' +
|
|
11980
12106
|
escapeHtml(inv.expires_at || '(no expiry)') + '</code>.</p>' +
|
|
11981
12107
|
'<div class="copy-token" id="copy-token">' + escapeHtml(inv.raw_token) + '</div>' +
|
|
11982
|
-
'<p style="font-size:12px;color:var(--text-muted);margin-bottom:0">Click the token to copy.</p>'
|
|
12108
|
+
'<p style="font-size:12px;color:var(--text-muted);margin-bottom:0">Click the token to copy.</p>' +
|
|
12109
|
+
connBlock;
|
|
11983
12110
|
var handle = showModal('Invitation token', bodyHtml, { primaryLabel: 'Done', onSubmit: function () {} });
|
|
11984
12111
|
var tokenEl = document.getElementById('copy-token');
|
|
11985
12112
|
if (tokenEl) {
|
|
@@ -11990,6 +12117,15 @@ var appJs = `
|
|
|
11990
12117
|
});
|
|
11991
12118
|
});
|
|
11992
12119
|
}
|
|
12120
|
+
var connEl = document.getElementById('copy-conn');
|
|
12121
|
+
if (connEl) {
|
|
12122
|
+
connEl.addEventListener('click', function () {
|
|
12123
|
+
navigator.clipboard.writeText(connStr).then(function () {
|
|
12124
|
+
connEl.textContent = 'Copied!';
|
|
12125
|
+
setTimeout(function () { connEl.textContent = connStr; }, 1200);
|
|
12126
|
+
});
|
|
12127
|
+
});
|
|
12128
|
+
}
|
|
11993
12129
|
// Suppress unused-var on handle
|
|
11994
12130
|
void handle;
|
|
11995
12131
|
}
|
|
@@ -13155,17 +13291,22 @@ async function resolveTeamContext(db, teamsClient, cloudUrl, candidateTables) {
|
|
|
13155
13291
|
const teamId = identity.team_id;
|
|
13156
13292
|
const creatorUserId = identity.creator_email ? await resolveUserIdByEmail(db, identity.creator_email) : null;
|
|
13157
13293
|
let myUserId = "";
|
|
13294
|
+
let myEmail = "";
|
|
13158
13295
|
try {
|
|
13159
13296
|
const me = await db.get("__lattice_user_identity", "singleton");
|
|
13160
|
-
if (me?.email)
|
|
13297
|
+
if (me?.email) {
|
|
13298
|
+
myEmail = me.email;
|
|
13299
|
+
myUserId = await resolveUserIdByEmail(db, me.email) ?? "";
|
|
13300
|
+
}
|
|
13161
13301
|
} catch {
|
|
13162
13302
|
myUserId = "";
|
|
13163
13303
|
}
|
|
13304
|
+
let savedConn = null;
|
|
13164
13305
|
if (!myUserId) {
|
|
13165
13306
|
try {
|
|
13166
13307
|
const conns = await teamsClient.listConnections();
|
|
13167
|
-
|
|
13168
|
-
myUserId =
|
|
13308
|
+
savedConn = conns.find((c) => c.cloud_url === cloudUrl) ?? conns.find((c) => c.team_id === teamId) ?? null;
|
|
13309
|
+
myUserId = savedConn?.my_user_id ?? "";
|
|
13169
13310
|
} catch {
|
|
13170
13311
|
}
|
|
13171
13312
|
}
|
|
@@ -13184,6 +13325,26 @@ async function resolveTeamContext(db, teamsClient, cloudUrl, candidateTables) {
|
|
|
13184
13325
|
isMember = false;
|
|
13185
13326
|
}
|
|
13186
13327
|
}
|
|
13328
|
+
if (!isMember && (myEmail || savedConn)) {
|
|
13329
|
+
try {
|
|
13330
|
+
const memberRows = await db.query("__lattice_team_members", {
|
|
13331
|
+
filters: [
|
|
13332
|
+
{ col: "team_id", op: "eq", val: teamId },
|
|
13333
|
+
{ col: "deleted_at", op: "isNull" }
|
|
13334
|
+
]
|
|
13335
|
+
});
|
|
13336
|
+
if (memberRows.length > 0) {
|
|
13337
|
+
const matchId = myEmail ? await resolveUserIdByEmail(db, myEmail) : null;
|
|
13338
|
+
if (matchId && memberRows.some((m) => m.user_id === matchId)) {
|
|
13339
|
+
myUserId = matchId;
|
|
13340
|
+
isMember = true;
|
|
13341
|
+
} else if (savedConn) {
|
|
13342
|
+
isMember = true;
|
|
13343
|
+
}
|
|
13344
|
+
}
|
|
13345
|
+
} catch {
|
|
13346
|
+
}
|
|
13347
|
+
}
|
|
13187
13348
|
if (creatorUserId) {
|
|
13188
13349
|
await reconcileObjectOwners(db, teamId, creatorUserId, candidateTables);
|
|
13189
13350
|
}
|
|
@@ -13454,6 +13615,11 @@ async function appendAudit(db, feed, table, rowId, op, before, after, source = "
|
|
|
13454
13615
|
for (const r of undone) await db.delete("_lattice_gui_audit", r.id);
|
|
13455
13616
|
await db.insert("_lattice_gui_audit", {
|
|
13456
13617
|
id: crypto.randomUUID(),
|
|
13618
|
+
// Set ts explicitly (don't rely on the column DEFAULT — it uses the
|
|
13619
|
+
// SQLite-only `strftime(...)`, which doesn't yield a parseable ISO string
|
|
13620
|
+
// on Postgres, so cloud history rendered "Invalid Date"). Mirrors the
|
|
13621
|
+
// explicit `client_ts` below; adapter-agnostic.
|
|
13622
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13457
13623
|
table_name: table,
|
|
13458
13624
|
row_id: rowId,
|
|
13459
13625
|
operation: op,
|
|
@@ -13475,6 +13641,9 @@ async function recordSchemaAudit(db, feed, table, operation, before, after, summ
|
|
|
13475
13641
|
for (const r of undone) await db.delete("_lattice_gui_audit", r.id);
|
|
13476
13642
|
await db.insert("_lattice_gui_audit", {
|
|
13477
13643
|
id: crypto.randomUUID(),
|
|
13644
|
+
// Explicit ISO ts — see appendAudit (the SQLite-only strftime DEFAULT
|
|
13645
|
+
// rendered "Invalid Date" on the Postgres/cloud path).
|
|
13646
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13478
13647
|
table_name: table,
|
|
13479
13648
|
row_id: null,
|
|
13480
13649
|
operation,
|
|
@@ -17006,18 +17175,11 @@ async function dashboardPayload(db, configPath, outputDir, teamContext) {
|
|
|
17006
17175
|
if (stale) staleCount += 1;
|
|
17007
17176
|
return { ...t, lastUpdatedAt, stale };
|
|
17008
17177
|
});
|
|
17009
|
-
let recent = [];
|
|
17010
|
-
try {
|
|
17011
|
-
const raw = await db.query("_lattice_gui_audit", { limit: 15 });
|
|
17012
|
-
recent = raw.map(parseAudit).sort((a, b) => b.ts.localeCompare(a.ts)).slice(0, 15).map((e) => ({ table: e.table_name, op: e.operation, rowId: e.row_id, ts: e.ts }));
|
|
17013
|
-
} catch {
|
|
17014
|
-
}
|
|
17015
17178
|
return {
|
|
17016
17179
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17017
17180
|
staleDays: DASHBOARD_STALE_DAYS,
|
|
17018
17181
|
totals: { entities: entities.length, rows: totalRows, stale: staleCount },
|
|
17019
|
-
entities
|
|
17020
|
-
recent
|
|
17182
|
+
entities
|
|
17021
17183
|
};
|
|
17022
17184
|
}
|
|
17023
17185
|
var ROWS_PATH = /^\/api\/tables\/([^/]+)\/rows(?:\/(.+))?$/;
|
package/package.json
CHANGED