latticesql 1.16.2 → 1.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -6643,6 +6643,10 @@ var css = `
6643
6643
  .dm-graph .gnode-label { fill: var(--text); font-size: 12px; font-weight: 500; }
6644
6644
  .dm-graph .gnode-icon { dominant-baseline: middle; }
6645
6645
  .dm-graph .gnode:hover .gnode-dot { stroke: var(--text-muted); }
6646
+ /* Share-status stroke (cloud workspaces only): yellow = shared, red = private. */
6647
+ .dm-graph .gnode-shared .gnode-dot { stroke: #eab308; stroke-width: 2; }
6648
+ .dm-graph .gnode-private .gnode-dot { stroke: #ef4444; stroke-width: 2; }
6649
+ /* Selected (green) wins over share status \u2014 higher specificity (.gnode.active). */
6646
6650
  .dm-graph .gnode.active .gnode-dot { stroke: var(--accent); stroke-width: 2; }
6647
6651
  .dm-graph .gnode.active .gnode-glow { opacity: 0.18; }
6648
6652
  .dm-graph .gnode.active .gnode-label { fill: var(--accent); }
@@ -6656,6 +6660,11 @@ var css = `
6656
6660
  .dm-legend span { display: inline-flex; align-items: center; gap: 6px; }
6657
6661
  .dm-legend i { width: 16px; height: 0; border-top: 2px solid currentColor; display: inline-block; }
6658
6662
  .dm-legend i.dash { border-top-style: dashed; }
6663
+ /* Share-status swatches: filled dots rather than the relationship line. */
6664
+ .dm-legend i.sw { width: 10px; height: 10px; border-top: 0; border-radius: 50%; }
6665
+ .dm-legend i.sw-shared { background: #eab308; }
6666
+ .dm-legend i.sw-private { background: #ef4444; }
6667
+ .dm-legend i.sw-selected { background: var(--accent); }
6659
6668
  #dm-panel {
6660
6669
  background: var(--surface); border: 1px solid var(--border);
6661
6670
  border-radius: 10px; padding: 20px;
@@ -6954,6 +6963,17 @@ var css = `
6954
6963
  }
6955
6964
  .shared-row:hover, .member-row:hover { background: var(--row-hover); }
6956
6965
  .shared-row .table-name { font-family: ui-monospace, monospace; }
6966
+ /* Role/status pills inside the settings-drawer member list, which is not
6967
+ under .team-card \u2014 so the .team-card-scoped .role-tag rules don't reach
6968
+ it. Covers creator / member / and the pending-invitee invited/expired. */
6969
+ .members-list .role-tag {
6970
+ display: inline-block; padding: 2px 8px; border-radius: 4px;
6971
+ font-size: 11px; font-weight: 600; text-transform: uppercase;
6972
+ background: var(--accent-soft); color: var(--accent);
6973
+ }
6974
+ .members-list .role-tag.role-member { background: #eef0f3; color: var(--text-muted); }
6975
+ .members-list .role-tag.role-expired { background: #fde2e1; color: #b91c1c; }
6976
+ .member-row-pending { opacity: 0.85; }
6957
6977
  .teams-empty {
6958
6978
  padding: 32px; text-align: center; color: var(--text-muted);
6959
6979
  border: 1px dashed var(--border-strong); border-radius: 8px;
@@ -7089,6 +7109,8 @@ var css = `
7089
7109
  .fs-context-doc .md-body a { color: var(--accent); }
7090
7110
  .fs-field { padding: 12px 0; border-bottom: 1px solid var(--border); }
7091
7111
  .fs-field:last-child { border-bottom: none; }
7112
+ /* Inline create-view action row (Save / Cancel). */
7113
+ .fs-create-actions { display: flex; gap: 8px; justify-content: flex-end; max-width: 900px; margin-top: 16px; }
7092
7114
  .fs-field-label {
7093
7115
  font-size: 11px; color: var(--text-muted); text-transform: uppercase;
7094
7116
  letter-spacing: 0.04em; margin-bottom: 4px;
@@ -8127,11 +8149,11 @@ var appJs = `
8127
8149
  '</button>';
8128
8150
  }).join('');
8129
8151
  menu.innerHTML =
8130
- '<div class="db-section">Available databases</div>' +
8152
+ '<div class="db-section">Workspaces</div>' +
8131
8153
  items +
8132
- '<div class="db-section">New database</div>' +
8154
+ '<div class="db-section">New workspace</div>' +
8133
8155
  '<div class="db-create">' +
8134
- '<button class="btn primary" id="db-create-btn" style="width:100%;">+ New database\u2026</button>' +
8156
+ '<button class="btn primary" id="db-create-btn" style="width:100%;">+ New workspace\u2026</button>' +
8135
8157
  '</div>';
8136
8158
  menu.querySelectorAll('button.db-item').forEach(function (b) {
8137
8159
  b.addEventListener('click', function () {
@@ -8239,7 +8261,10 @@ var appJs = `
8239
8261
  // Even segment count \u2192 item view; odd \u2192 folder/collection view.
8240
8262
  var fsegs = fsParse(hash);
8241
8263
  if (fsegs) {
8242
- if (fsegs.length % 2 === 1) renderFsCollection(content, fsegs);
8264
+ // #/fs/<table>/new \u2192 inline create view (must precede the even/odd
8265
+ // item-vs-collection heuristic, since [table,'new'] is even-length).
8266
+ if (fsegs[fsegs.length - 1] === 'new') renderFsCreate(content, fsegs);
8267
+ else if (fsegs.length % 2 === 1) renderFsCollection(content, fsegs);
8243
8268
  else renderFsItem(content, fsegs);
8244
8269
  return;
8245
8270
  }
@@ -8300,10 +8325,14 @@ var appJs = `
8300
8325
  return dashboardPreferenceRank(a.name) - dashboardPreferenceRank(b.name);
8301
8326
  });
8302
8327
  if (ents.length === 0) {
8328
+ // Generic, role-agnostic empty state \u2014 the old copy told everyone to
8329
+ // "edit lattice.config.yml / db.define()", which a joined cloud member
8330
+ // cannot act on (they just have nothing shared with them yet).
8303
8331
  content.innerHTML =
8304
8332
  '<div class="placeholder">' +
8305
- '<h2>No entities yet</h2>' +
8306
- '<p>Define entities in your <code>lattice.config.yml</code> or register them via <code>db.define()</code>, then reload.</p>' +
8333
+ '<h2>This workspace is empty</h2>' +
8334
+ '<p>There are no tables to show yet. Create one in the Data Model editor, ' +
8335
+ 'or \u2014 on a cloud workspace \u2014 ask the owner to share a table with you.</p>' +
8307
8336
  '</div>';
8308
8337
  return;
8309
8338
  }
@@ -8479,8 +8508,13 @@ var appJs = `
8479
8508
  return '<input type="password" name="' + escapeHtml(col) + '" value="' +
8480
8509
  escapeHtml(value || '') + '" autocomplete="off" />';
8481
8510
  }
8482
- // Multiline for known long-form fields.
8483
- if (col === 'transcript' || col === 'summary' || col === 'body') {
8511
+ // Multiline for ALL long-form fields (matches FS_LONGFORM, the same set
8512
+ // fsValInner renders as markdown) AND any value that already contains a
8513
+ // newline. A single-line <input> normalizes/strips newlines, so a
8514
+ // multi-line markdown value put in one would be silently corrupted on the
8515
+ // next blur (a spurious PATCH) and then re-rendered as mangled markdown
8516
+ // ("huge text"). A <textarea> round-trips the exact text.
8517
+ if (FS_LONGFORM.indexOf(col) >= 0 || (value != null && String(value).indexOf('\\n') >= 0)) {
8484
8518
  return '<textarea name="' + escapeHtml(col) + '">' + escapeHtml(value || '') + '</textarea>';
8485
8519
  }
8486
8520
  return '<input type="text" name="' + escapeHtml(col) + '" value="' + escapeHtml(value || '') + '" />';
@@ -9214,7 +9248,7 @@ var appJs = `
9214
9248
  // opens a create form. Related-row folders aren't a place to mint a
9215
9249
  // brand-new object, so the tile is top-level only.
9216
9250
  var createTile = topLevel
9217
- ? '<a class="fs-tile fs-tile-create" href="#" data-fs-create="1" title="Create a new ' + escapeHtml(d.label) + '">' +
9251
+ ? '<a class="fs-tile fs-tile-create" href="' + fsHref([table, 'new']) + '" title="Create a new ' + escapeHtml(d.label) + '">' +
9218
9252
  '<div class="fs-tile-icon">\u2795</div>' +
9219
9253
  '<div class="fs-tile-label">New ' + escapeHtml(d.label) + '</div>' +
9220
9254
  '</a>'
@@ -9236,11 +9270,6 @@ var appJs = `
9236
9270
  '<span class="count">' + rows.length + ' item' + (rows.length === 1 ? '' : 's') + '</span>' +
9237
9271
  '</div>' +
9238
9272
  '<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
- });
9244
9273
  });
9245
9274
  }).catch(function (err) {
9246
9275
  content.innerHTML = '<div class="placeholder"><h2>Failed</h2>' + escapeHtml(err.message) + '</div>';
@@ -9251,11 +9280,17 @@ var appJs = `
9251
9280
  // page with blank fields + a Save button, plus a select-menu + "+" for each
9252
9281
  // many-to-many link. Reuses fieldFor() (intrinsic + belongsTo) and the
9253
9282
  // existing row-create + junction-row endpoints (no new backend).
9254
- function openFsCreateModal(content, table, segs) {
9283
+ // Inline create view (#/fs/<table>/new) \u2014 mirrors renderFsItem's formatted
9284
+ // layout (.fs-doc/.fs-field) with blank fields + Save/Cancel, instead of a
9285
+ // modal. Reuses fieldFor() + the row-create + junction /link endpoints.
9286
+ function renderFsCreate(content, segs) {
9287
+ var table = segs[0];
9255
9288
  var t = tableByName(table);
9256
- if (!t) return;
9289
+ if (!t) { content.innerHTML = '<div class="placeholder">Unknown entity: ' + escapeHtml(table) + '</div>'; return; }
9290
+ var d = displayFor(table);
9257
9291
  var bt = belongsToColumns(t);
9258
9292
  var juncs = junctionsFor(table);
9293
+ var collectionHref = fsHref([table]);
9259
9294
  // Preload FK + junction-remote target rows so the <select> menus populate.
9260
9295
  var needed = bt.map(function (b) { return b.rel.table; })
9261
9296
  .concat(juncs.map(function (j) { return j.remoteRel.table; }));
@@ -9282,62 +9317,73 @@ var appJs = `
9282
9317
  '<button type="button" class="btn fs-link-add">+ Add another</button>' +
9283
9318
  '</div></div>';
9284
9319
  });
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
- });
9320
+ content.innerHTML =
9321
+ '<nav class="fs-crumbs"><a href="#/">Home</a><span class="fs-sep">\u25B8</span>' +
9322
+ '<a href="' + collectionHref + '">' + escapeHtml(d.label) + '</a><span class="fs-sep">\u25B8</span>' +
9323
+ '<span>New</span></nav>' +
9324
+ '<div class="view-header">' +
9325
+ '<span class="entity-icon">' + d.icon + '</span>' +
9326
+ '<h1>New ' + escapeHtml(d.label) + '</h1>' +
9327
+ '</div>' +
9328
+ '<div class="fs-doc fs-create-form">' + fieldsHtml + '</div>' +
9329
+ '<div class="fs-create-actions">' +
9330
+ '<button class="btn" id="fs-create-cancel">Cancel</button>' +
9331
+ '<button class="btn primary" id="fs-create-save">Save</button>' +
9332
+ '</div>';
9333
+ content.querySelectorAll('.fs-link-add').forEach(function (addBtn) {
9334
+ addBtn.addEventListener('click', function () {
9335
+ var stage = addBtn.previousElementSibling; // the .fs-link-stage
9336
+ var firstSel = stage && stage.querySelector('.fs-link-select');
9337
+ if (!firstSel) return;
9338
+ var clone = firstSel.cloneNode(true);
9339
+ clone.value = '';
9340
+ stage.appendChild(clone);
9341
+ });
9342
+ });
9343
+ content.querySelector('#fs-create-cancel').addEventListener('click', function () {
9344
+ location.hash = collectionHref;
9345
+ });
9346
+ var saveBtn = content.querySelector('#fs-create-save');
9347
+ saveBtn.addEventListener('click', function () {
9348
+ var values = {};
9349
+ content.querySelectorAll('.fs-create-form [name]').forEach(function (el) {
9350
+ var v = el.value;
9351
+ if (v !== '' && v != null) values[el.getAttribute('name')] = v;
9352
+ });
9353
+ var links = [];
9354
+ content.querySelectorAll('.fs-link-stage').forEach(function (stage) {
9355
+ var junction = stage.getAttribute('data-junction');
9356
+ var localFk = stage.getAttribute('data-local-fk');
9357
+ var remoteFk = stage.getAttribute('data-remote-fk');
9358
+ stage.querySelectorAll('.fs-link-select').forEach(function (sel) {
9359
+ if (sel.value) links.push({ junction: junction, localFk: localFk, remoteFk: remoteFk, remoteId: sel.value });
9315
9360
  });
9361
+ });
9362
+ withBusy(saveBtn, function () {
9316
9363
  return rowWrite('POST', '/api/tables/' + encodeURIComponent(table) + '/rows', values).then(function (res) {
9317
9364
  var newId = res && (res.id || (res.row && res.row.id));
9318
9365
  var chain = Promise.resolve();
9319
9366
  links.forEach(function (lk) {
9320
9367
  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.
9368
+ // Junction /link endpoint (INSERT OR IGNORE on the two FKs) \u2014
9369
+ // works for pk-less junctions + is idempotent.
9324
9370
  var jrow = {};
9325
9371
  jrow[lk.localFk] = newId;
9326
9372
  jrow[lk.remoteFk] = lk.remoteId;
9327
9373
  return rowWrite('POST', '/api/tables/' + encodeURIComponent(lk.junction) + '/link', jrow);
9328
9374
  });
9329
9375
  });
9330
- return chain;
9331
- }).then(function () {
9376
+ return chain.then(function () { return newId; });
9377
+ }).then(function (newId) {
9332
9378
  invalidate(table);
9333
- return refreshEntities();
9334
- }).then(function () {
9335
- showToast('Created', {});
9336
- renderFsCollection(content, segs);
9337
- });
9338
- },
9379
+ return refreshEntities().then(function () {
9380
+ showToast('Created', {});
9381
+ location.hash = newId ? fsHref([table, String(newId)]) : collectionHref;
9382
+ });
9383
+ }).catch(function (err) { showToast('Create failed: ' + err.message, {}); });
9384
+ });
9339
9385
  });
9340
- }).catch(function (err) { showToast('Create failed: ' + err.message, {}); });
9386
+ }).catch(function (err) { content.innerHTML = '<div class="placeholder"><h2>Failed</h2>' + escapeHtml(err.message) + '</div>'; });
9341
9387
  }
9342
9388
 
9343
9389
  // Item view \u2014 one row as a document (click-to-edit) + its relationship folders.
@@ -9871,6 +9917,11 @@ var appJs = `
9871
9917
  rowCount: rc,
9872
9918
  cols: (meta.columns || []).length,
9873
9919
  r: Math.max(11, Math.min(26, 11 + Math.sqrt(rc))),
9920
+ // Share status (cloud workspaces only). ownedByMe is set by the
9921
+ // server solely on cloud workspaces, so its presence flags a cloud
9922
+ // DB; on local DBs share status is N/A (no coloring).
9923
+ shared: meta.shared === true,
9924
+ cloudWorkspace: meta.ownedByMe !== undefined,
9874
9925
  x: 0, y: 0, vx: 0, vy: 0,
9875
9926
  });
9876
9927
  });
@@ -9961,7 +10012,11 @@ var appJs = `
9961
10012
  dash + markStart + markEnd + ' opacity="0.7"><title>' + escapeHtml(title) + '</title></line>';
9962
10013
  }).join('');
9963
10014
  var nodeSvg = nodes.map(function (nd) {
9964
- return '<g class="gnode" data-table="' + escapeHtml(nd.name) + '" transform="translate(' +
10015
+ // Share-status coloring applies only on cloud workspaces (G). On a
10016
+ // local DB share status is N/A, so no extra class \u2192 neutral stroke.
10017
+ var shareCls = nd.cloudWorkspace ? (nd.shared ? ' gnode-shared' : ' gnode-private') : '';
10018
+ var shareTitle = nd.cloudWorkspace ? ' \xB7 ' + (nd.shared ? 'shared' : 'private') : '';
10019
+ return '<g class="gnode' + shareCls + '" data-table="' + escapeHtml(nd.name) + '" transform="translate(' +
9965
10020
  nd.x.toFixed(1) + ',' + nd.y.toFixed(1) + ')">' +
9966
10021
  '<circle class="gnode-glow" r="' + (nd.r + 8).toFixed(1) + '"/>' +
9967
10022
  '<circle class="gnode-dot" r="' + nd.r.toFixed(1) + '"/>' +
@@ -9969,13 +10024,22 @@ var appJs = `
9969
10024
  (nd.r * 0.95).toFixed(1) + '">' + nd.icon + '</text>' +
9970
10025
  '<text class="gnode-label" y="' + (nd.r + 15).toFixed(1) + '" text-anchor="middle">' +
9971
10026
  escapeHtml(nd.label) + '</text>' +
9972
- '<title>' + escapeHtml(nd.label + ' \xB7 ' + nd.rowCount + ' rows \xB7 ' + nd.cols + ' columns') + '</title>' +
10027
+ '<title>' + escapeHtml(nd.label + ' \xB7 ' + nd.rowCount + ' rows \xB7 ' + nd.cols + ' columns' + shareTitle) + '</title>' +
9973
10028
  '</g>';
9974
10029
  }).join('');
10030
+ // Share legend entries only make sense on a cloud workspace (where nodes
10031
+ // carry share status). Local DBs show just the relationship key.
10032
+ var anyCloud = nodes.some(function (nd) { return nd.cloudWorkspace; });
10033
+ var shareLegend = anyCloud
10034
+ ? '<span><i class="sw sw-shared"></i><span style="color:var(--text-muted)">shared</span></span>' +
10035
+ '<span><i class="sw sw-private"></i><span style="color:var(--text-muted)">private</span></span>' +
10036
+ '<span><i class="sw sw-selected"></i><span style="color:var(--text-muted)">selected</span></span>'
10037
+ : '';
9975
10038
  var legend =
9976
10039
  '<div class="dm-legend">' +
9977
10040
  '<span style="color:' + DM_FK_COLOR + '"><i></i><span style="color:var(--text-muted)">foreign key</span></span>' +
9978
10041
  '<span style="color:' + DM_M2M_COLOR + '"><i class="dash"></i><span style="color:var(--text-muted)">many-to-many</span></span>' +
10042
+ shareLegend +
9979
10043
  '</div>';
9980
10044
  return '<svg class="dm-graph" viewBox="' + vb.join(' ') + '" preserveAspectRatio="xMidYMid meet">' +
9981
10045
  defs + '<g class="dm-stage">' + edgeSvg + nodeSvg + '</g></svg>' + legend;
@@ -10124,7 +10188,7 @@ var appJs = `
10124
10188
  '</div>' +
10125
10189
  '<div class="muted" style="margin-top:14px;font-size:12px;">' +
10126
10190
  'New entities get id (uuid PK), name, and deleted_at columns. ' +
10127
- 'Add more columns once the entity exists. On a team cloud the ' +
10191
+ 'Add more columns once the entity exists. On a cloud workspace the ' +
10128
10192
  'entity is private to you until you share it.' +
10129
10193
  '</div>';
10130
10194
  wireEmojiPicker(panel, 'dm-create-icon');
@@ -10269,20 +10333,20 @@ var appJs = `
10269
10333
  '</div>'
10270
10334
  : '<span class="muted" style="font-size:12px">No other entities to link to.</span>';
10271
10335
 
10272
- // Team-cloud sharing row \u2014 only the owner of a table may toggle
10273
- // its team visibility (t.ownedByMe is set by the server only for
10274
- // team clouds). Tables shared to me by others, and all non-team
10336
+ // Cloud sharing row \u2014 only the owner of a table may toggle its
10337
+ // visibility (t.ownedByMe is set by the server only for cloud
10338
+ // workspaces). Tables shared to me by others, and all local-DB
10275
10339
  // tables, show no sharing control.
10276
10340
  var canShare = !!(t && t.ownedByMe === true);
10277
10341
  var isShared = !!(t && t.shared);
10278
10342
  var shareRow = canShare
10279
- ? '<label>Team sharing</label>' +
10343
+ ? '<label>Cloud sharing</label>' +
10280
10344
  '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">' +
10281
10345
  '<button class="btn' + (isShared ? '' : ' primary') + '" id="dm-share-btn">' +
10282
- (isShared ? 'Unshare from team' : 'Share with team') +
10346
+ (isShared ? 'Make private' : 'Share with workspace') +
10283
10347
  '</button>' +
10284
10348
  '<span style="font-size:12px;color:var(--text-muted)">' +
10285
- (isShared ? 'Visible to every team member.' : 'Private to you. Share to make it visible to the team.') +
10349
+ (isShared ? 'Visible to everyone on this cloud workspace.' : 'Private to you. Share to make it visible to everyone on this cloud workspace.') +
10286
10350
  '</span>' +
10287
10351
  '</div>'
10288
10352
  : '';
@@ -10347,7 +10411,7 @@ var appJs = `
10347
10411
  // so a light in-place refresh reflects it without a full reload.
10348
10412
  return dmRefreshPanel(tableName, false);
10349
10413
  }).then(function () {
10350
- showToast(isShared ? 'Unshared "' + tableName + '" from team' : 'Shared "' + tableName + '" with team', {});
10414
+ showToast(isShared ? 'Unshared "' + tableName + '" from workspace' : 'Shared "' + tableName + '" with workspace', {});
10351
10415
  }).catch(function (e) { showToast('Share update failed: ' + e.message, {}); });
10352
10416
  });
10353
10417
  });
@@ -10739,7 +10803,7 @@ var appJs = `
10739
10803
  backdrop.className = 'modal-backdrop';
10740
10804
  backdrop.innerHTML =
10741
10805
  '<div class="modal" style="min-width:560px;max-width:640px">' +
10742
- '<div class="modal-head" id="wiz-head">New database \u2014 step 1 of 3</div>' +
10806
+ '<div class="modal-head" id="wiz-head">New workspace \u2014 step 1 of 3</div>' +
10743
10807
  '<div class="modal-body" id="wiz-body"></div>' +
10744
10808
  '<div class="modal-foot">' +
10745
10809
  '<button class="btn" data-act="cancel">Cancel</button>' +
@@ -10760,7 +10824,7 @@ var appJs = `
10760
10824
  var body = backdrop.querySelector('#wiz-body');
10761
10825
  var nextBtn = backdrop.querySelector('[data-act="next"]');
10762
10826
  var backBtn = backdrop.querySelector('[data-act="back"]');
10763
- head.textContent = 'New database \u2014 step ' + wizState.step + ' of 3';
10827
+ head.textContent = 'New workspace \u2014 step ' + wizState.step + ' of 3';
10764
10828
  backBtn.style.display = wizState.step === 1 ? 'none' : '';
10765
10829
  nextBtn.textContent = wizState.step === 3 ? 'Create' : 'Next';
10766
10830
  if (wizState.step === 1) body.innerHTML = renderStep1();
@@ -10810,7 +10874,7 @@ var appJs = `
10810
10874
  '</label>' +
10811
10875
  '</div>' +
10812
10876
  '<p style="font-size:11px;color:var(--text-muted);margin:6px 0 0">' +
10813
- 'Local databases are single-user SQLite files on your machine. Cloud databases are Postgres, can be shared with invited team members, and stream realtime updates. Joining connects to a cloud DB you were invited to.' +
10877
+ 'Local databases are single-user SQLite files on your machine. Cloud databases are Postgres, can be shared with invited members, and stream realtime updates. Joining connects to a cloud DB you were invited to.' +
10814
10878
  '</p>' +
10815
10879
  '</div>' +
10816
10880
  cloudBlock;
@@ -10838,7 +10902,7 @@ var appJs = `
10838
10902
  '<button class="btn" id="wiz-add-entity" style="margin-top:10px">+ Add entity</button>' +
10839
10903
  (wizState.kind === 'cloud'
10840
10904
  ? '<p style="font-size:11px;color:var(--text-muted);margin:10px 0 0">' +
10841
- 'Entities with \u201CShare with cloud\u201D checked are visible to every team member. Unchecked entities live on the cloud DB but stay scoped to your own row links.' +
10905
+ 'Entities with \u201CShare with cloud\u201D checked are visible to everyone on the cloud workspace. Unchecked entities live on the cloud DB but stay scoped to your own row links.' +
10842
10906
  '</p>'
10843
10907
  : '');
10844
10908
  }
@@ -11073,7 +11137,7 @@ var appJs = `
11073
11137
  '<p style="font-size:12px;color:var(--text-muted);margin:0">' +
11074
11138
  'Use the same Postgres URL the inviter used (postgres://\u2026). Your email + display name come from User Settings \u2014 change them there. The email must match the address the invitation was addressed to.' +
11075
11139
  '</p>';
11076
- showModal('Join team', bodyHtml, {
11140
+ showModal('Join workspace', bodyHtml, {
11077
11141
  primaryLabel: 'Join',
11078
11142
  onSubmit: function (scope) {
11079
11143
  var data = collectFormValues(scope);
@@ -11093,7 +11157,7 @@ var appJs = `
11093
11157
  body: JSON.stringify({ path: path }),
11094
11158
  })
11095
11159
  .then(function () { return reloadEverything(); })
11096
- .then(function () { showToast('Joined "' + (res.team && res.team.name || 'team') + '" \u2014 switched to it', {}); });
11160
+ .then(function () { showToast('Joined "' + (res.team && res.team.name || 'workspace') + '" \u2014 switched to it', {}); });
11097
11161
  });
11098
11162
  },
11099
11163
  });
@@ -11156,7 +11220,7 @@ var appJs = `
11156
11220
  host.innerHTML =
11157
11221
  '<div class="dbconfig-panel" style="margin-bottom:18px;padding:14px;border:1px solid var(--border);border-radius:8px;background:var(--surface)">' +
11158
11222
  '<h3 style="margin:0 0 10px">Identity</h3>' +
11159
- '<p class="lead" style="margin:0 0 10px">Display name + email used when creating or joining teams. Saved to ~/.lattice/identity.json and mirrored into the active Lattice.</p>' +
11223
+ '<p class="lead" style="margin:0 0 10px">Display name + email used when creating or joining cloud workspaces. Saved to ~/.lattice/identity.json and mirrored into the active Lattice.</p>' +
11160
11224
  '<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px">' +
11161
11225
  '<div><label class="field-label">Display name</label><input id="id-display-name" type="text" value="' + escapeHtml(id.display_name || '') + '" style="width:100%"></div>' +
11162
11226
  '<div><label class="field-label">Email</label><input id="id-email" type="email" value="' + escapeHtml(id.email || '') + '" style="width:100%"></div>' +
@@ -11258,8 +11322,8 @@ var appJs = `
11258
11322
  // Database panel below.
11259
11323
  content.innerHTML =
11260
11324
  '<div class="teams-page">' +
11261
- '<h2>Database Settings</h2>' +
11262
- '<div id="db-name-host"><div class="placeholder" style="padding:14px">Loading database name\u2026</div></div>' +
11325
+ '<h2>Workspace Settings</h2>' +
11326
+ '<div id="db-name-host"><div class="placeholder" style="padding:14px">Loading workspace name\u2026</div></div>' +
11263
11327
  '<div id="dbconfig-host"><div class="placeholder" style="padding:18px">Loading database configuration\u2026</div></div>' +
11264
11328
  '<div id="data-model-host"><div class="placeholder" style="padding:18px">Loading data model\u2026</div></div>' +
11265
11329
  '<div id="db-danger-host"></div>' +
@@ -11282,8 +11346,8 @@ var appJs = `
11282
11346
  '<strong style="color:var(--danger)">This cannot be undone.</strong></p>' +
11283
11347
  '<p style="margin:0 0 6px;font-size:12px;color:var(--text-muted)">Type <strong>' + escapeHtml(safeLabel) + '</strong> to confirm:</p>' +
11284
11348
  '<input id="confirm-db-name" type="text" autocomplete="off" style="width:100%" />';
11285
- showModal('Delete database', body, {
11286
- primaryLabel: 'Delete database',
11349
+ showModal('Delete workspace', body, {
11350
+ primaryLabel: 'Delete workspace',
11287
11351
  primaryClass: 'destructive',
11288
11352
  onBody: function (backdrop) {
11289
11353
  var input = backdrop.querySelector('#confirm-db-name');
@@ -11344,7 +11408,7 @@ var appJs = `
11344
11408
  '<div class="danger-zone">' +
11345
11409
  '<h3>Danger zone</h3>' +
11346
11410
  '<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.' +
11411
+ 'Disconnect this database from the cloud. This dissolves the cloud workspace and <strong>kicks all members</strong>. This cannot be undone.' +
11348
11412
  '</p>' +
11349
11413
  '<button class="btn destructive" id="db-disconnect-btn">Disconnect from cloud</button>' +
11350
11414
  '</div>';
@@ -11365,16 +11429,16 @@ var appJs = `
11365
11429
  '<div class="danger-zone">' +
11366
11430
  '<h3>Danger zone</h3>' +
11367
11431
  '<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.' +
11432
+ 'Leave this cloud workspace. It keeps running for everyone else; you simply stop being a member.' +
11369
11433
  '</p>' +
11370
- '<button class="btn destructive" id="db-leave-btn">Leave team</button>' +
11434
+ '<button class="btn destructive" id="db-leave-btn">Leave workspace</button>' +
11371
11435
  '</div>';
11372
11436
  host.querySelector('#db-leave-btn').addEventListener('click', function () {
11373
11437
  if (!confirm('Leave "' + (cfg.teamName || label || 'this team') + '"?')) return;
11374
11438
  var lbtn = host.querySelector('#db-leave-btn');
11375
11439
  withBusy(lbtn, function () {
11376
11440
  return fetchJson('/api/teams-gui/teams/' + cfg.teamId + '/members/' + encodeURIComponent(cfg.myUserId), { method: 'DELETE' })
11377
- .then(function () { showToast('Left the team', {}); return switchAway(); })
11441
+ .then(function () { showToast('Left the workspace', {}); return switchAway(); })
11378
11442
  .catch(function (e) { alert('Leave failed: ' + e.message); });
11379
11443
  });
11380
11444
  });
@@ -11385,15 +11449,21 @@ var appJs = `
11385
11449
  '<div class="danger-zone">' +
11386
11450
  '<h3>Danger zone</h3>' +
11387
11451
  '<p style="font-size:12px;color:var(--text-muted);margin:0 0 10px">' +
11388
- 'Permanently delete this database. The configuration is removed and, for a local database, the underlying SQLite file is deleted. This cannot be undone.' +
11452
+ 'Permanently delete this workspace. The configuration is removed and, for a local database, the underlying SQLite file is deleted. This cannot be undone.' +
11389
11453
  '</p>' +
11390
- '<button class="btn destructive" id="db-delete-btn">Delete database</button>' +
11454
+ '<button class="btn destructive" id="db-delete-btn">Delete workspace</button>' +
11391
11455
  '</div>';
11392
11456
  host.querySelector('#db-delete-btn').addEventListener('click', function () {
11393
11457
  confirmDeleteDatabase(path, label, function () {
11394
- // We just deleted the active DB; the server switched to a fallback.
11458
+ // We just deleted the active workspace; the server switched to a
11459
+ // fallback. Re-render the drawer's Workspace-settings tab so it
11460
+ // reflects the NEW active workspace \u2014 previously this rendered into
11461
+ // #content behind the open drawer, leaving the user stuck on the
11462
+ // deleted workspace's settings.
11395
11463
  return reloadEverything().then(function () {
11396
- renderDatabaseSettings(document.getElementById('content'));
11464
+ var drawer = document.getElementById('settings-drawer');
11465
+ if (drawer && !drawer.hidden) selectDrawerTab('database');
11466
+ else closeSettingsDrawer();
11397
11467
  });
11398
11468
  });
11399
11469
  });
@@ -11430,9 +11500,9 @@ var appJs = `
11430
11500
  (canRename
11431
11501
  ? ('Friendly database name shown in the topbar and the dropdown. ' +
11432
11502
  (isCloud
11433
- ? 'For cloud databases, the rename is broadcast to every team member in realtime.'
11503
+ ? 'For cloud databases, the rename is broadcast to every member in realtime.'
11434
11504
  : 'Saved to the YAML config\\'s name: key.'))
11435
- : 'Only the team owner can rename this cloud database.') +
11505
+ : 'Only the workspace owner can rename this cloud database.') +
11436
11506
  '</p>' +
11437
11507
  '<div id="db-name-msg" style="margin-top:6px;font-size:12px;color:var(--text-muted)"></div>' +
11438
11508
  '</div>';
@@ -11480,12 +11550,12 @@ var appJs = `
11480
11550
  ? (current.kind === 'cloud' ? 'Cloud (Postgres)' : 'Local (SQLite)')
11481
11551
  : '\u2014';
11482
11552
  var rowLabel = c.label || c.name;
11483
- var del = '<button class="btn danger" data-delete-path="' + escapeHtml(c.path) + '" data-delete-label="' + escapeHtml(rowLabel) + '">Delete</button>';
11553
+ // No per-row Delete / Action column (1.16.3) \u2014 rows are click-to-switch;
11554
+ // deletion lives in Workspace Settings \u2192 Danger Zone.
11484
11555
  return '<tr' + (c.active ? '' : ' class="ws-row" data-switch-path="' + escapeHtml(c.path) + '"') + '>' +
11485
11556
  '<td>' + escapeHtml(rowLabel) + (c.active ? ' <span class="role-tag">active</span>' : '') + '</td>' +
11486
11557
  '<td>' + kind + '</td>' +
11487
11558
  '<td><code>' + escapeHtml(c.dbFile || '') + '</code></td>' +
11488
- '<td>' + del + '</td>' +
11489
11559
  '</tr>';
11490
11560
  }).join('');
11491
11561
  host.innerHTML =
@@ -11495,8 +11565,8 @@ var appJs = `
11495
11565
  '<button class="btn primary" id="action-add-db">+ Add new workspace</button>' +
11496
11566
  '</div>' +
11497
11567
  '<table style="width:100%;border-collapse:collapse">' +
11498
- '<thead><tr style="text-align:left"><th>Name</th><th>Kind</th><th>File / source</th><th>Action</th></tr></thead>' +
11499
- '<tbody>' + (rows || '<tr><td colspan="4" style="padding:8px;color:var(--text-muted)">No workspaces configured.</td></tr>') + '</tbody>' +
11568
+ '<thead><tr style="text-align:left"><th>Name</th><th>Kind</th><th>File / source</th></tr></thead>' +
11569
+ '<tbody>' + (rows || '<tr><td colspan="3" style="padding:8px;color:var(--text-muted)">No workspaces configured.</td></tr>') + '</tbody>' +
11500
11570
  '</table>' +
11501
11571
  '</div>';
11502
11572
  host.querySelectorAll('tr.ws-row[data-switch-path]').forEach(function (row) {
@@ -11508,22 +11578,6 @@ var appJs = `
11508
11578
  .then(function () { renderLatticeSettings(document.getElementById('content')); });
11509
11579
  });
11510
11580
  });
11511
- host.querySelectorAll('[data-delete-path]').forEach(function (btn) {
11512
- btn.addEventListener('click', function (e) {
11513
- e.stopPropagation(); // don't trigger the row's switch handler
11514
- confirmDeleteDatabase(
11515
- btn.getAttribute('data-delete-path'),
11516
- btn.getAttribute('data-delete-label'),
11517
- function () {
11518
- // Deleting any row may have switched the active DB (if it was
11519
- // the active one); refetch everything, then re-render the list.
11520
- return reloadEverything().then(function () {
11521
- renderLatticeSettings(document.getElementById('content'));
11522
- });
11523
- },
11524
- );
11525
- });
11526
- });
11527
11581
  host.querySelector('#action-add-db').addEventListener('click', showCreateDatabaseWizard);
11528
11582
  }).catch(function (err) {
11529
11583
  host.innerHTML = '<div class="placeholder">Failed to load databases: ' + escapeHtml(err.message) + '</div>';
@@ -11562,20 +11616,16 @@ var appJs = `
11562
11616
  label = 'LOCAL';
11563
11617
  color = 'var(--text-muted)';
11564
11618
  break;
11565
- case 'cloud-connected':
11566
- label = 'CLOUD \xB7 CONNECTED';
11567
- color = 'var(--accent)';
11568
- break;
11569
11619
  case 'team-cloud-creator':
11570
- label = '\u{1F451} TEAM CLOUD \xB7 CREATOR';
11620
+ label = '\u{1F451} CLOUD \xB7 OWNER';
11571
11621
  color = 'var(--accent)';
11572
11622
  break;
11573
11623
  case 'team-cloud-member':
11574
- label = 'TEAM CLOUD \xB7 MEMBER';
11624
+ label = 'CLOUD \xB7 MEMBER';
11575
11625
  color = 'var(--accent)';
11576
11626
  break;
11577
11627
  case 'team-cloud-needs-invite':
11578
- label = 'TEAM CLOUD \xB7 NEEDS INVITE';
11628
+ label = 'CLOUD \xB7 NEEDS INVITE';
11579
11629
  color = 'var(--warn)';
11580
11630
  break;
11581
11631
  default:
@@ -11597,24 +11647,16 @@ var appJs = `
11597
11647
  '</div>'
11598
11648
  );
11599
11649
  }
11600
- if (info.state === 'cloud-connected') {
11601
- return (
11602
- renderConnectionSummary(info) +
11603
- '<div class="team-actions" style="margin-top:10px">' +
11604
- '<button class="btn primary" data-act="open-upgrade">Upgrade to team cloud \u2192</button>' +
11605
- '</div>'
11606
- );
11607
- }
11608
11650
  if (info.state === 'team-cloud-creator' || info.state === 'team-cloud-member') {
11609
- var isCreator = info.state === 'team-cloud-creator';
11651
+ var isOwner = info.state === 'team-cloud-creator';
11610
11652
  return (
11611
11653
  renderConnectionSummary(info) +
11612
11654
  '<div style="margin-top:10px;font-size:13px">' +
11613
- '<strong>Team:</strong> ' + escapeHtml(info.teamName || '(unnamed)') +
11614
- (isCreator ? ' \xB7 <span style="color:var(--accent)">you are the creator</span>' : ' \xB7 <span style="color:var(--text-muted)">member</span>') +
11655
+ '<strong>Cloud workspace:</strong> ' + escapeHtml(info.teamName || '(unnamed)') +
11656
+ (isOwner ? ' \xB7 <span style="color:var(--accent)">you are the owner</span>' : ' \xB7 <span style="color:var(--text-muted)">member</span>') +
11615
11657
  '</div>' +
11616
11658
  '<div class="team-actions" style="margin-top:10px">' +
11617
- (isCreator ? '<button class="btn primary" data-act="open-invite">Invite member</button>' : '') +
11659
+ (isOwner ? '<button class="btn primary" data-act="open-invite">Invite member</button>' : '') +
11618
11660
  '</div>' +
11619
11661
  // Exit actions (Disconnect for the owner / Leave for a member) live
11620
11662
  // in the Danger Zone below \u2014 not on a member row.
@@ -11625,14 +11667,14 @@ var appJs = `
11625
11667
  return (
11626
11668
  renderConnectionSummary(info) +
11627
11669
  '<p style="margin-top:10px;color:var(--warn);font-size:13px">' +
11628
- 'This cloud DB is a team \u2014 paste your invite token to join.' +
11670
+ 'Not a member of this cloud workspace yet \u2014 paste your invite token to join.' +
11629
11671
  '</p>' +
11630
11672
  '<div style="display:grid;grid-template-columns:1fr;gap:8px;margin-top:6px">' +
11631
11673
  '<div><label class="field-label">Invite token</label>' +
11632
11674
  '<textarea id="db-rejoin-token" placeholder="latinv_..." style="width:100%;height:54px;font-family:JetBrains Mono,monospace"></textarea></div>' +
11633
11675
  '</div>' +
11634
11676
  '<div class="team-actions" style="margin-top:10px">' +
11635
- '<button class="btn primary" data-act="rejoin-with-token">Join team \u2192</button>' +
11677
+ '<button class="btn primary" data-act="rejoin-with-token">Join workspace \u2192</button>' +
11636
11678
  '</div>'
11637
11679
  );
11638
11680
  }
@@ -11662,11 +11704,6 @@ var appJs = `
11662
11704
  showMigrateToCloudModal(rerender);
11663
11705
  });
11664
11706
 
11665
- var upgradeBtn = host.querySelector('[data-act="open-upgrade"]');
11666
- if (upgradeBtn) upgradeBtn.addEventListener('click', function () {
11667
- showUpgradeToTeamModal(rerender);
11668
- });
11669
-
11670
11707
  // team_id / my_user_id / isCreator come from /api/dbconfig (info),
11671
11708
  // resolved against the ACTIVE cloud DB \u2014 not a local connection row
11672
11709
  // (which doesn't exist when the team cloud itself is active). This
@@ -11687,15 +11724,21 @@ var appJs = `
11687
11724
  // rows carry Kick, shown only to the creator.
11688
11725
  var membersHost = host.querySelector('#db-members-host');
11689
11726
  if (membersHost && teamId && (info.state === 'team-cloud-creator' || info.state === 'team-cloud-member')) {
11690
- fetchJson('/api/teams-gui/teams/' + teamId + '/members').then(function (res) {
11691
- var members = res.members || [];
11692
- membersHost.innerHTML = renderMembersList(members, myUserId, isCreator);
11727
+ Promise.all([
11728
+ fetchJson('/api/teams-gui/teams/' + teamId + '/members'),
11729
+ // Pending invitees (I). Resilient: an older cloud without the GET
11730
+ // invitations route shouldn't blank the whole member list.
11731
+ fetchJson('/api/teams-gui/teams/' + teamId + '/invitations').catch(function () { return { invitations: [] }; }),
11732
+ ]).then(function (results) {
11733
+ var members = (results[0] && results[0].members) || [];
11734
+ var invitations = (results[1] && results[1].invitations) || [];
11735
+ membersHost.innerHTML = renderMembersList(members, myUserId, isCreator, invitations);
11693
11736
  // Kick another member (creator only).
11694
11737
  membersHost.querySelectorAll('[data-act="kick"]').forEach(function (btn) {
11695
11738
  var row = btn.closest('[data-user-id]');
11696
11739
  var userId = row && row.getAttribute('data-user-id');
11697
11740
  btn.addEventListener('click', function () {
11698
- if (!confirm('Remove this member from the team?')) return;
11741
+ if (!confirm('Remove this member from the workspace?')) return;
11699
11742
  withBusy(btn, function () {
11700
11743
  return fetchJson('/api/teams-gui/teams/' + teamId + '/members/' + encodeURIComponent(userId), { method: 'DELETE' })
11701
11744
  .then(function () { rerender(); })
@@ -11714,7 +11757,7 @@ var appJs = `
11714
11757
  // call the connect-existing endpoint with just the invite
11715
11758
  // token. The handler reads credentials from db-credentials.enc
11716
11759
  // via the active configPath's label.
11717
- setMsg('Joining team\u2026');
11760
+ setMsg('Joining workspace\u2026');
11718
11761
  fetch('/api/dbconfig/connect-existing', {
11719
11762
  method: 'POST', headers: { 'content-type': 'application/json' },
11720
11763
  body: JSON.stringify({
@@ -11877,7 +11920,7 @@ var appJs = `
11877
11920
  '<div style="margin-top:14px;padding:10px;border:1px solid var(--border);border-radius:6px;background:rgba(255,255,255,0.02)">' +
11878
11921
  '<div style="font-size:12px;color:var(--text);text-transform:uppercase;letter-spacing:0.04em;font-weight:500;margin-bottom:6px">Share with cloud</div>' +
11879
11922
  '<p style="margin:0 0 8px;font-size:12px;color:var(--text-muted)">' +
11880
- 'Checked tables become visible to every team member you invite. Uncheck any you want to keep ' +
11923
+ 'Checked tables become visible to every member you invite. Uncheck any you want to keep ' +
11881
11924
  'cloud-stored but unshared. You can change this later from Data Model.' +
11882
11925
  '</p>' +
11883
11926
  shareRows +
@@ -11900,7 +11943,7 @@ var appJs = `
11900
11943
  return probeBeforeCredentialSave(body, msg).then(function (probe) {
11901
11944
  if (probe.teamEnabled) {
11902
11945
  throw new Error(
11903
- 'Target is already a cloud DB with a team' +
11946
+ 'Target is already a cloud workspace' +
11904
11947
  (probe.teamName ? ' (' + probe.teamName + ')' : '') +
11905
11948
  '. Migrate-to-cloud only works against fresh empty targets.'
11906
11949
  );
@@ -11960,7 +12003,7 @@ var appJs = `
11960
12003
  '<code>lattice.config.yml</code>\\'s <code>db:</code> line or via the ' +
11961
12004
  'Databases catalog under User Config. If you want to <em>push</em> ' +
11962
12005
  'your local rows into the target instead, use Migrate to cloud. If ' +
11963
- 'the target is a teams DB you\\'ll be asked for an invite token ' +
12006
+ 'the target is a shared cloud workspace you\\'ll be asked for an invite token ' +
11964
12007
  'after the probe.' +
11965
12008
  '</p>' +
11966
12009
  postgresFormHtml({}) +
@@ -11982,7 +12025,7 @@ var appJs = `
11982
12025
  var zone = document.getElementById('w-team-zone');
11983
12026
  zone.innerHTML =
11984
12027
  '<div style="padding:10px;background:rgba(251,146,60,0.08);border:1px solid var(--warn);border-radius:6px">' +
11985
- '<p style="margin:0 0 8px;font-size:13px;color:var(--warn)">Target is a teams DB' +
12028
+ '<p style="margin:0 0 8px;font-size:13px;color:var(--warn)">Target is a shared cloud workspace' +
11986
12029
  (probe.teamName ? ' (<strong>' + escapeHtml(probe.teamName) + '</strong>)' : '') +
11987
12030
  '. Paste your invite token to join:</p>' +
11988
12031
  '<textarea id="w-invite-token" placeholder="latinv_..." style="width:100%;height:54px;font-family:JetBrains Mono,monospace"></textarea>' +
@@ -12016,35 +12059,11 @@ var appJs = `
12016
12059
  });
12017
12060
  }
12018
12061
 
12019
- function showUpgradeToTeamModal(onClose) {
12020
- var bodyHtml =
12021
- '<p style="margin:0 0 12px;font-size:13px;color:var(--text-muted)">' +
12022
- 'Upgrade this cloud DB to a team DB by registering as the founding member. ' +
12023
- 'Your display name + email from <strong>User Config \u2192 Identity</strong> are used.' +
12024
- '</p>' +
12025
- '<div><label class="field-label">Team name</label>' +
12026
- '<input type="text" id="w-team-name" placeholder="Atlas" style="width:100%"></div>' +
12027
- '<div id="w-msg" style="margin-top:10px;font-size:12px;color:var(--text-muted)"></div>';
12028
- showModal('Upgrade to team cloud', bodyHtml, {
12029
- primaryLabel: 'Upgrade \u2192',
12030
- onSubmit: function () {
12031
- var teamName = (document.getElementById('w-team-name').value || '').trim();
12032
- if (!teamName) throw new Error('Team name is required.');
12033
- var msg = document.getElementById('w-msg');
12034
- msg.textContent = 'Upgrading\u2026';
12035
- return fetch('/api/dbconfig/upgrade-to-team', {
12036
- method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ team_name: teamName }),
12037
- })
12038
- .then(function (r) { return r.json().then(function (d) { return { status: r.status, body: d }; }); })
12039
- .then(function (r) {
12040
- if (!r.body.ok) throw new Error(r.body.error || ('HTTP ' + r.status));
12041
- if (onClose) onClose();
12042
- });
12043
- },
12044
- });
12045
- }
12062
+ // (Removed in 1.16.3) The standalone upgrade-to-cloud-sharing modal is
12063
+ // gone; cloud workspaces initialize their member/share machinery
12064
+ // automatically (see TeamsClient.ensureCloudWorkspaceIdentity).
12046
12065
 
12047
- function renderMembersList(members, myUserId, isCreator) {
12066
+ function renderMembersList(members, myUserId, isCreator, invitations) {
12048
12067
  var rows = members.map(function (m) {
12049
12068
  var label = m.name || m.email || '(unknown)';
12050
12069
  var isSelf = m.user_id === myUserId;
@@ -12064,7 +12083,22 @@ var appJs = `
12064
12083
  btn +
12065
12084
  '</div>';
12066
12085
  }).join('');
12067
- return '<div class="members-list"><h4>Members</h4>' + rows + '</div>';
12086
+ // Pending (unredeemed) invitations \u2014 shown below active members so the
12087
+ // owner can see who's been invited but hasn't joined yet (I).
12088
+ var pending = (invitations || []).filter(function (iv) { return iv && iv.invitee_email; });
12089
+ var pendingHtml = pending.length
12090
+ ? '<h4 style="margin-top:14px">Pending invitations</h4>' +
12091
+ pending.map(function (iv) {
12092
+ return '<div class="member-row member-row-pending">' +
12093
+ '<span style="color:var(--text-muted)">' + escapeHtml(iv.invitee_email) +
12094
+ ' <span class="role-tag' + (iv.expired ? ' role-expired' : ' role-member') + '">' +
12095
+ (iv.expired ? 'expired' : 'invited') +
12096
+ '</span>' +
12097
+ '</span>' +
12098
+ '</div>';
12099
+ }).join('')
12100
+ : '';
12101
+ return '<div class="members-list"><h4>Members</h4>' + rows + pendingHtml + '</div>';
12068
12102
  }
12069
12103
 
12070
12104
  function showInviteByEmailModal(teamId, info) {
@@ -12225,7 +12259,7 @@ var guiAppHtml = `<!doctype html>
12225
12259
  <button class="drawer-close" id="drawer-close" title="Close" aria-label="Close settings">\u2715</button>
12226
12260
  </div>
12227
12261
  <div class="drawer-tabs" id="drawer-tabs">
12228
- <button class="drawer-tab" data-tab="database">Database</button>
12262
+ <button class="drawer-tab" data-tab="database">Workspace</button>
12229
12263
  <button class="drawer-tab" data-tab="lattice">Lattice</button>
12230
12264
  <button class="drawer-tab" data-tab="user">User</button>
12231
12265
  </div>
@@ -12864,6 +12898,22 @@ async function listTeamMembers(db, teamId) {
12864
12898
  }
12865
12899
  return out;
12866
12900
  }
12901
+ async function listPendingInvitations(db, teamId) {
12902
+ const rows = await db.query("__lattice_invitations", {
12903
+ filters: [
12904
+ { col: "team_id", op: "eq", val: teamId },
12905
+ { col: "redeemed_at", op: "isNull" }
12906
+ ]
12907
+ });
12908
+ const nowMs = Date.now();
12909
+ return rows.map((r) => ({
12910
+ id: r.id,
12911
+ invitee_email: r.invitee_email,
12912
+ invited_at: r.created_at,
12913
+ expires_at: r.expires_at ?? null,
12914
+ expired: r.expires_at != null && new Date(r.expires_at).getTime() < nowMs
12915
+ })).sort((a, b) => a.invited_at < b.invited_at ? 1 : a.invited_at > b.invited_at ? -1 : 0);
12916
+ }
12867
12917
  async function appendChangeEnvelope(db, entry) {
12868
12918
  const rows = await db.query("__lattice_change_log", {
12869
12919
  filters: [{ col: "team_id", op: "eq", val: entry.team_id }],
@@ -12984,6 +13034,9 @@ async function unshareObject(db, teamId, table) {
12984
13034
  async function listMembersDirect(db, teamId) {
12985
13035
  return listTeamMembers(db, teamId);
12986
13036
  }
13037
+ async function listPendingInvitationsDirect(db, teamId) {
13038
+ return listPendingInvitations(db, teamId);
13039
+ }
12987
13040
  async function inviteDirect(db, teamId, inviterUserId, inviteeEmail, expiresInHours = 7 * 24) {
12988
13041
  const team = await db.get("__lattice_team", teamId);
12989
13042
  if (!team || team.deleted_at) {
@@ -13984,6 +14037,10 @@ async function dispatchTeamRoute(req, res, ctx) {
13984
14037
  await handleCreateInvitation(req, res, ctx, invitationsMatch[1] ?? "");
13985
14038
  return true;
13986
14039
  }
14040
+ if (invitationsMatch && method === "GET") {
14041
+ await handleListInvitations(res, ctx, invitationsMatch[1] ?? "");
14042
+ return true;
14043
+ }
13987
14044
  const objectsListMatch = /^\/api\/teams\/([^/]+)\/objects$/.exec(pathname);
13988
14045
  if (objectsListMatch) {
13989
14046
  if (method === "POST") {
@@ -14260,6 +14317,18 @@ async function handleListMembers(res, ctx, teamId) {
14260
14317
  }
14261
14318
  sendJson2(res, { members: await listTeamMembers(ctx.db, teamId) });
14262
14319
  }
14320
+ async function handleListInvitations(res, ctx, teamId) {
14321
+ if (!ctx.authContext) {
14322
+ sendJson2(res, { error: "Unauthorized" }, 401);
14323
+ return;
14324
+ }
14325
+ const role = await getMembershipRole(ctx.db, teamId, ctx.authContext.user.id);
14326
+ if (!role) {
14327
+ sendJson2(res, { error: "Not a member of this team" }, 403);
14328
+ return;
14329
+ }
14330
+ sendJson2(res, { invitations: await listPendingInvitations(ctx.db, teamId) });
14331
+ }
14263
14332
  async function handleKickMember(res, ctx, teamId, userId) {
14264
14333
  if (!ctx.authContext) {
14265
14334
  sendJson2(res, { error: "Unauthorized" }, 401);
@@ -14970,6 +15039,42 @@ var TeamsClient = class {
14970
15039
  });
14971
15040
  return reg;
14972
15041
  }
15042
+ /**
15043
+ * Idempotently initialize a cloud Postgres DB as a collaborative cloud
15044
+ * workspace (members + sharing). 1.16.3 deprecated the user-facing "team"
15045
+ * concept and the explicit "upgrade to team" step — every cloud workspace
15046
+ * gets this machinery automatically at migrate / connect / open time, so the
15047
+ * members + per-table sharing surface is always available on a cloud DB.
15048
+ *
15049
+ * No-op (returns created:false) when the cloud already carries an identity.
15050
+ * On a fresh cloud the caller becomes the owner. Race-safe: a concurrent
15051
+ * initializer that wins the singleton insert is treated as success.
15052
+ */
15053
+ async ensureCloudWorkspaceIdentity(opts) {
15054
+ const probe = await probeCloud(opts.cloudUrl);
15055
+ if (!probe.reachable) {
15056
+ throw new Error(`Cloud DB unreachable: ${probe.error ?? "unknown error"}`);
15057
+ }
15058
+ if (probe.teamEnabled) return { created: false };
15059
+ if (!opts.email) {
15060
+ throw new Error("Set your email in User settings to set up this cloud workspace.");
15061
+ }
15062
+ try {
15063
+ const displayName = opts.displayName?.trim() ? opts.displayName : opts.email;
15064
+ await this.upgradeToTeamCloud({
15065
+ label: opts.label,
15066
+ cloudUrl: opts.cloudUrl,
15067
+ teamName: opts.workspaceName,
15068
+ email: opts.email,
15069
+ displayName
15070
+ });
15071
+ return { created: true };
15072
+ } catch (e) {
15073
+ const msg = e.message || "";
15074
+ if (/already has (a team|users)/i.test(msg)) return { created: false };
15075
+ throw e;
15076
+ }
15077
+ }
14973
15078
  // ── Cloud team operations (dispatch on URL scheme) ──────────────────────
14974
15079
  // For HTTP cloud URLs (`http://lattice-server:port`), every operation
14975
15080
  // round-trips through the team server's authenticated REST API. For
@@ -15003,6 +15108,18 @@ var TeamsClient = class {
15003
15108
  );
15004
15109
  return r.members;
15005
15110
  }
15111
+ async listPendingInvitations(cloudUrl, token, teamId) {
15112
+ if (isPostgresUrl(cloudUrl)) {
15113
+ return listPendingInvitationsDirect(this.local, teamId);
15114
+ }
15115
+ const r = await this.fetchAuthed(
15116
+ cloudUrl,
15117
+ token,
15118
+ "GET",
15119
+ `/api/teams/${teamId}/invitations`
15120
+ );
15121
+ return r.invitations;
15122
+ }
15006
15123
  async invite(cloudUrl, token, teamId, inviteeEmail, expiresInHours, inviterUserId) {
15007
15124
  if (isPostgresUrl(cloudUrl)) {
15008
15125
  if (!inviterUserId) {
@@ -15907,6 +16024,15 @@ async function dispatchTeamSubroute(req, res, ctx, teamId, subpath) {
15907
16024
  sendJson(res, { members });
15908
16025
  return;
15909
16026
  }
16027
+ if (subpath === "invitations" && method === "GET") {
16028
+ const invitations = await ctx.client.listPendingInvitations(
16029
+ conn.cloud_url,
16030
+ conn.api_token,
16031
+ teamId
16032
+ );
16033
+ sendJson(res, { invitations });
16034
+ return;
16035
+ }
15910
16036
  if (subpath === "invitations" && method === "POST") {
15911
16037
  const body = await readJson(req);
15912
16038
  const inviteeEmail = requireString2(body, "invitee_email");
@@ -16268,7 +16394,8 @@ var NATIVE_ENTITY_DEFS = {
16268
16394
  notes: {
16269
16395
  // A generic knowledge object: a free-form note with a title and body.
16270
16396
  // Ordinary, user-editable rows; `source_file_id` optionally points back at
16271
- // an originating `files` row.
16397
+ // an originating `files` row. Retained as native (1.16.3) because the
16398
+ // reference/source-organizer store uses it as the fallback organizer target.
16272
16399
  columns: {
16273
16400
  id: "TEXT PRIMARY KEY",
16274
16401
  title: "TEXT",
@@ -16464,7 +16591,7 @@ async function getCreatorEmail(db) {
16464
16591
  }
16465
16592
  function computeState(type, teamEnabled, label, creatorEmail) {
16466
16593
  if (type === "sqlite") return "local";
16467
- if (!teamEnabled) return "cloud-connected";
16594
+ if (!teamEnabled) return "team-cloud-needs-invite";
16468
16595
  if (!label) {
16469
16596
  return "team-cloud-needs-invite";
16470
16597
  }
@@ -16744,6 +16871,19 @@ async function dispatchDbConfigRoute(req, res, ctx) {
16744
16871
  const sourceDbPath = parseConfigFile(ctx.configPath).dbPath;
16745
16872
  const backupPath = archiveLocalSqlite(sourceDbPath);
16746
16873
  saveDbCredential(parsed.label, url);
16874
+ try {
16875
+ const identity = readIdentity();
16876
+ if (identity.email) {
16877
+ await new TeamsClient(ctx.db).ensureCloudWorkspaceIdentity({
16878
+ label: parsed.label,
16879
+ cloudUrl: url,
16880
+ workspaceName: parsed.label,
16881
+ email: identity.email,
16882
+ displayName: identity.display_name
16883
+ });
16884
+ }
16885
+ } catch {
16886
+ }
16747
16887
  rewriteDbLine(ctx.configPath, "${LATTICE_DB:" + parsed.label + "}");
16748
16888
  await ctx.swap();
16749
16889
  sendJson(res, {
@@ -16789,6 +16929,18 @@ async function dispatchDbConfigRoute(req, res, ctx) {
16789
16929
  ...identity.email ? { email: identity.email } : {},
16790
16930
  ...identity.display_name ? { name: identity.display_name } : {}
16791
16931
  });
16932
+ if (!result.probe.teamEnabled && identity.email) {
16933
+ try {
16934
+ await client.ensureCloudWorkspaceIdentity({
16935
+ label: parsed.label,
16936
+ cloudUrl: url,
16937
+ workspaceName: parsed.label,
16938
+ email: identity.email,
16939
+ displayName: identity.display_name
16940
+ });
16941
+ } catch {
16942
+ }
16943
+ }
16792
16944
  rewriteDbLine(ctx.configPath, "${LATTICE_DB:" + parsed.label + "}");
16793
16945
  await ctx.swap();
16794
16946
  sendJson(res, {
@@ -16853,63 +17005,6 @@ async function dispatchDbConfigRoute(req, res, ctx) {
16853
17005
  });
16854
17006
  return true;
16855
17007
  }
16856
- if (pathname === "/api/dbconfig/upgrade-to-team" && method === "POST") {
16857
- await tryHandler(res, async () => {
16858
- const body = await readJson(req);
16859
- const teamName = typeof body.team_name === "string" && body.team_name.trim() ? body.team_name.trim() : "";
16860
- if (!teamName) {
16861
- sendJson(res, { error: "team_name is required" }, 400);
16862
- return;
16863
- }
16864
- const info = await describeCurrent(ctx.configPath, ctx.db);
16865
- if (info.type !== "postgres" || !info.label) {
16866
- sendJson(
16867
- res,
16868
- {
16869
- error: "upgrade-to-team requires the active project to be on a labeled cloud DB. Migrate to cloud first."
16870
- },
16871
- 400
16872
- );
16873
- return;
16874
- }
16875
- if (info.teamEnabled) {
16876
- sendJson(res, { error: "Cloud DB is already a team DB" }, 409);
16877
- return;
16878
- }
16879
- const cloudUrl = getDbCredential(info.label);
16880
- if (!cloudUrl) {
16881
- sendJson(res, { error: "No saved credential for " + info.label }, 500);
16882
- return;
16883
- }
16884
- const identity = readIdentity();
16885
- if (!identity.email || !identity.display_name) {
16886
- sendJson(
16887
- res,
16888
- {
16889
- error: "Set your display name + email in User Config \u2192 Identity before creating a team"
16890
- },
16891
- 400
16892
- );
16893
- return;
16894
- }
16895
- const client = new TeamsClient(ctx.db);
16896
- try {
16897
- const reg = await client.upgradeToTeamCloud({
16898
- label: info.label,
16899
- cloudUrl,
16900
- teamName,
16901
- email: identity.email,
16902
- displayName: identity.display_name
16903
- });
16904
- await ctx.swap();
16905
- sendJson(res, { ok: true, team: reg.team, user: reg.user });
16906
- } catch (e) {
16907
- const status = e.status ?? 500;
16908
- sendJson(res, { ok: false, error: e.message }, status);
16909
- }
16910
- });
16911
- return true;
16912
- }
16913
17008
  return false;
16914
17009
  }
16915
17010
 
@@ -17370,6 +17465,30 @@ async function openConfig(configPath, outputDir, autoRender = false) {
17370
17465
  } catch {
17371
17466
  teamEnabled = false;
17372
17467
  }
17468
+ if (!teamEnabled) {
17469
+ try {
17470
+ const rawDb = parseDocument2(readFileSync13(configPath, "utf8")).get("db");
17471
+ const dbLine = typeof rawDb === "string" ? rawDb.trim() : "";
17472
+ const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(dbLine);
17473
+ const label = labelMatch?.[1];
17474
+ const identity = readIdentity();
17475
+ if (label && identity.email) {
17476
+ await teamsClient.ensureCloudWorkspaceIdentity({
17477
+ label,
17478
+ cloudUrl: parsed.dbPath,
17479
+ workspaceName: label,
17480
+ email: identity.email,
17481
+ displayName: identity.display_name
17482
+ });
17483
+ teamEnabled = await db.get("__lattice_team_identity", "singleton") != null;
17484
+ }
17485
+ } catch (e) {
17486
+ console.warn(
17487
+ "[openConfig] could not auto-initialize cloud workspace:",
17488
+ e.message
17489
+ );
17490
+ }
17491
+ }
17373
17492
  if (teamEnabled) {
17374
17493
  await registerTeamCloudTables(db);
17375
17494
  try {
@@ -17483,19 +17602,12 @@ function saveConfigDoc(configPath, doc) {
17483
17602
  function createBlankConfig(activeConfigPath, dbName) {
17484
17603
  const dir = dirname8(activeConfigPath);
17485
17604
  const slug = dbName.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
17486
- if (!slug) throw new Error("Database name must contain at least one alphanumeric character");
17605
+ if (!slug) throw new Error("Workspace name must contain at least one alphanumeric character");
17487
17606
  const configPath = join16(dir, `${slug}.config.yml`);
17488
17607
  if (existsSync18(configPath)) throw new Error(`Config already exists: ${slug}.config.yml`);
17489
17608
  const yaml = `db: ./data/${slug}.db
17490
17609
 
17491
- entities:
17492
- items:
17493
- fields:
17494
- id: { type: uuid, primaryKey: true }
17495
- name: { type: text, required: true }
17496
- notes: { type: text }
17497
- deleted_at: { type: text }
17498
- outputFile: ITEMS.md
17610
+ entities: {}
17499
17611
  `;
17500
17612
  writeFileSync8(configPath, yaml, "utf8");
17501
17613
  mkdirSync8(join16(dir, "data"), { recursive: true });