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.
Files changed (2) hide show
  1. package/dist/cli.js +264 -102
  2. 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
- var recent = '';
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
- var tiles = rows.length
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">' + tiles + '</div>';
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 s; }
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>' + (d.active ? '\u2014' : '<button class="btn" data-switch="' + escapeHtml(d.configPath) + '">Switch</button>') + '</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 (btn) {
11124
- btn.addEventListener('click', function () {
11125
- var configPath = btn.getAttribute('data-switch');
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
- fetchJson('/api/databases').then(function (data) {
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
- var actions = (c.active ? '' : '<button class="btn" data-switch="' + escapeHtml(c.path) + '">Switch</button> ') + del;
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>' + actions + '</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 (btn) {
11351
- btn.addEventListener('click', function () {
11352
- var configPath = btn.getAttribute('data-switch');
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">Generate invite token</button>' : '') +
11617
+ (isCreator ? '<button class="btn primary" data-act="open-invite">Invite member</button>' : '') +
11465
11618
  '</div>' +
11466
- // Leave (member) / Destroy (creator) now live on your own row in
11467
- // the members list below \u2014 no separate top-level button.
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
- // Your own row: Leave (member) or Destroy team (creator). Other
11936
- // rows: Kick, but only the creator may remove other members.
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) myUserId = await resolveUserIdByEmail(db, 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
- const conn = conns.find((c) => c.cloud_url === cloudUrl) ?? conns.find((c) => c.team_id === teamId);
13168
- myUserId = conn?.my_user_id ?? "";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latticesql",
3
- "version": "1.16.1",
3
+ "version": "1.16.2",
4
4
  "description": "Persistent structured memory for AI agent systems — pluggable SQLite or Postgres backend, LLM context bridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",