@tensorix/metahub 0.1.10 → 0.1.11

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 (75) hide show
  1. package/README.md +19 -6
  2. package/README.zh-CN.md +7 -2
  3. package/dist/cli.js +1601 -595
  4. package/dist/cli.js.map +43 -38
  5. package/dist/core/compact.d.ts +42 -0
  6. package/dist/core/compact.d.ts.map +1 -0
  7. package/dist/core/config.d.ts +3 -3
  8. package/dist/core/config.d.ts.map +1 -1
  9. package/dist/core/context.d.ts +4 -4
  10. package/dist/core/context.d.ts.map +1 -1
  11. package/dist/core/crdt.d.ts +30 -7
  12. package/dist/core/crdt.d.ts.map +1 -1
  13. package/dist/core/databases.d.ts +22 -8
  14. package/dist/core/databases.d.ts.map +1 -1
  15. package/dist/core/db.d.ts +1 -27
  16. package/dist/core/db.d.ts.map +1 -1
  17. package/dist/core/documents.d.ts +28 -15
  18. package/dist/core/documents.d.ts.map +1 -1
  19. package/dist/core/driver.d.ts +25 -0
  20. package/dist/core/driver.d.ts.map +1 -0
  21. package/dist/core/history.d.ts +99 -11
  22. package/dist/core/history.d.ts.map +1 -1
  23. package/dist/core/hlc.d.ts +3 -3
  24. package/dist/core/hlc.d.ts.map +1 -1
  25. package/dist/core/index.d.ts +1 -0
  26. package/dist/core/index.d.ts.map +1 -1
  27. package/dist/core/indexing.d.ts +4 -4
  28. package/dist/core/indexing.d.ts.map +1 -1
  29. package/dist/core/integrity.d.ts +1 -1
  30. package/dist/core/integrity.d.ts.map +1 -1
  31. package/dist/core/node.d.ts +2 -2
  32. package/dist/core/node.d.ts.map +1 -1
  33. package/dist/core/properties.d.ts +10 -9
  34. package/dist/core/properties.d.ts.map +1 -1
  35. package/dist/core/records.d.ts +13 -8
  36. package/dist/core/records.d.ts.map +1 -1
  37. package/dist/core/resolve.d.ts +4 -4
  38. package/dist/core/resolve.d.ts.map +1 -1
  39. package/dist/core/schema-init.d.ts +37 -0
  40. package/dist/core/schema-init.d.ts.map +1 -0
  41. package/dist/core/schema.d.ts +1 -1
  42. package/dist/core/schema.d.ts.map +1 -1
  43. package/dist/core/search.d.ts +3 -3
  44. package/dist/core/search.d.ts.map +1 -1
  45. package/dist/core/sites-core.d.ts +60 -0
  46. package/dist/core/sites-core.d.ts.map +1 -0
  47. package/dist/core/sites.d.ts +11 -52
  48. package/dist/core/sites.d.ts.map +1 -1
  49. package/dist/core/sqlcols.d.ts +4 -0
  50. package/dist/core/sqlcols.d.ts.map +1 -0
  51. package/dist/core/sync/auth.d.ts +5 -3
  52. package/dist/core/sync/auth.d.ts.map +1 -1
  53. package/dist/core/sync/client.d.ts +11 -3
  54. package/dist/core/sync/client.d.ts.map +1 -1
  55. package/dist/core/sync/peers.d.ts +10 -10
  56. package/dist/core/sync/peers.d.ts.map +1 -1
  57. package/dist/core/sync/protocol.d.ts +5 -0
  58. package/dist/core/sync/protocol.d.ts.map +1 -1
  59. package/dist/core/sync/routes.d.ts.map +1 -1
  60. package/dist/core/sync/server.d.ts +10 -0
  61. package/dist/core/sync/server.d.ts.map +1 -1
  62. package/dist/db-worker.js +268 -0
  63. package/dist/index.js +1061 -340
  64. package/dist/index.js.map +31 -28
  65. package/dist/metahub-sdk.js +1 -0
  66. package/dist/mh-runtime.js +1 -0
  67. package/dist/sqlite3.wasm +0 -0
  68. package/dist/sw.js +1 -0
  69. package/dist/webui/server/assets.d.ts +8 -1
  70. package/dist/webui/server/assets.d.ts.map +1 -1
  71. package/dist/webui/server/icons.d.ts +4 -0
  72. package/dist/webui/server/icons.d.ts.map +1 -0
  73. package/dist/webui/server/routes.d.ts.map +1 -1
  74. package/dist/webui.js +57 -57
  75. package/package.json +3 -1
package/dist/index.js CHANGED
@@ -242,6 +242,19 @@ function lcs(a, b) {
242
242
  }
243
243
 
244
244
  // src/core/crdt.ts
245
+ function withChangeGroup(label, fn) {
246
+ if (currentTxn !== null)
247
+ return fn();
248
+ currentTxn = (label ? label + ":" : "") + randomSuffix(8);
249
+ try {
250
+ return fn();
251
+ } finally {
252
+ currentTxn = null;
253
+ }
254
+ }
255
+ function grouped(fn, label = null) {
256
+ return (...args) => withChangeGroup(label, () => fn(...args));
257
+ }
245
258
  function encodeScalar(val) {
246
259
  if (val === null || val === undefined)
247
260
  return null;
@@ -295,7 +308,7 @@ function materialize(db, dataset, rowId, col, valueJson) {
295
308
  }
296
309
  }
297
310
  function applyChange(db, c) {
298
- db.query("INSERT OR IGNORE INTO crdt_changes (hlc, node_id, dataset, row_id, col, value) VALUES (?, ?, ?, ?, ?, ?)").run(c.hlc, c.node_id, c.dataset, c.row_id, c.col, c.value);
311
+ db.query("INSERT OR IGNORE INTO crdt_changes (hlc, node_id, dataset, row_id, col, value, txn) VALUES (?, ?, ?, ?, ?, ?, ?)").run(c.hlc, c.node_id, c.dataset, c.row_id, c.col, c.value, c.txn ?? null);
299
312
  const cur = db.query("SELECT MAX(hlc) AS h FROM crdt_changes WHERE dataset = ? AND row_id = ? AND col = ?").get(c.dataset, c.row_id, c.col);
300
313
  if (cur.h !== c.hlc)
301
314
  return false;
@@ -310,7 +323,8 @@ function emit(db, dataset, rowId, col, value) {
310
323
  dataset,
311
324
  row_id: rowId,
312
325
  col,
313
- value: value === undefined ? null : JSON.stringify(value)
326
+ value: value === undefined ? null : JSON.stringify(value),
327
+ txn: currentTxn
314
328
  };
315
329
  applyChange(db, change);
316
330
  return change;
@@ -336,17 +350,29 @@ function ingest(db, changes) {
336
350
  return applied;
337
351
  }
338
352
  function changesSince(db, since) {
339
- return db.query("SELECT hlc, node_id, dataset, row_id, col, value FROM crdt_changes WHERE hlc > ? ORDER BY hlc").all(since);
340
- }
341
- function changesAfterSeq(db, seq) {
342
- const rows = db.query("SELECT rowid AS seq, hlc, node_id, dataset, row_id, col, value FROM crdt_changes WHERE rowid > ? ORDER BY rowid").all(seq);
343
- const last = rows[rows.length - 1];
344
- const cursor = last ? last.seq : seq;
353
+ return db.query(`SELECT ${CHANGE_SELECT} FROM crdt_changes WHERE hlc > ? ORDER BY hlc`).all(since);
354
+ }
355
+ function changesAfterSeq(db, seq, opts = {}) {
356
+ const exclude = opts.excludeDatasets ?? [];
357
+ const where = exclude.length ? `rowid > ? AND dataset NOT IN (${exclude.map(() => "?").join(", ")})` : "rowid > ?";
358
+ const limitSql = opts.limit != null && opts.limit > 0 ? ` LIMIT ${Math.floor(opts.limit)}` : "";
359
+ const rows = db.query(`SELECT rowid AS seq, ${CHANGE_SELECT} FROM crdt_changes WHERE ${where} ORDER BY rowid${limitSql}`).all(seq, ...exclude);
360
+ const exhausted = opts.limit == null || opts.limit <= 0 || rows.length < opts.limit;
361
+ let cursor;
362
+ if (exhausted) {
363
+ const top = db.query("SELECT MAX(rowid) AS m FROM crdt_changes").get();
364
+ cursor = Math.max(seq, top.m ?? seq);
365
+ } else {
366
+ cursor = rows[rows.length - 1].seq;
367
+ }
345
368
  return { changes: rows.map(({ seq: _seq, ...c }) => c), cursor };
346
369
  }
347
- var DOMAIN, RECORD_META;
370
+ var CHANGE_COLS, CHANGE_SELECT, currentTxn = null, DOMAIN, RECORD_META;
348
371
  var init_crdt = __esm(() => {
349
372
  init_node();
373
+ init_ids();
374
+ CHANGE_COLS = ["hlc", "node_id", "dataset", "row_id", "col", "value", "txn"];
375
+ CHANGE_SELECT = CHANGE_COLS.join(", ");
350
376
  DOMAIN = {
351
377
  databases: {
352
378
  table: "databases",
@@ -409,7 +435,7 @@ var init_cache = __esm(() => {
409
435
  init_paths();
410
436
  });
411
437
 
412
- // src/core/sites.ts
438
+ // src/core/sites-core.ts
413
439
  function inferContentType(path) {
414
440
  const ext = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
415
441
  return MIME[ext] ?? "application/octet-stream";
@@ -432,28 +458,8 @@ function normalizeSitePath(raw) {
432
458
  }
433
459
  return segs.join("/");
434
460
  }
435
- function isTextType(contentType) {
436
- const ct = contentType.toLowerCase();
437
- return ct.startsWith("text/") || ct.startsWith("application/json") || ct.startsWith("application/manifest+json") || ct.startsWith("application/xml") || ct.startsWith("text/javascript") || ct.startsWith("application/javascript") || ct.startsWith("image/svg+xml");
438
- }
439
- function toBytes(data) {
440
- if (typeof data === "string")
441
- return new TextEncoder().encode(data);
442
- return data instanceof Uint8Array ? data : new Uint8Array(data);
443
- }
444
- function createSite(db, opts) {
445
- const name = normalizeSiteName(opts.name);
446
- if (getSiteByName(db, name))
447
- throw new MhError("conflict", `site name already exists: ${name}`);
448
- const id = newId("site", name);
449
- const first = emit(db, "sites", id, "name", name);
450
- emit(db, "sites", id, "created_hlc", first.hlc);
451
- if (opts.title !== undefined)
452
- emit(db, "sites", id, "title", opts.title);
453
- return getSite(db, id);
454
- }
455
461
  function getSite(db, id) {
456
- return db.query("SELECT id, name, title, created_hlc FROM sites WHERE id = ? AND __deleted = 0").get(id);
462
+ return db.query(`SELECT ${SITE_SELECT} FROM sites WHERE id = ? AND __deleted = 0`).get(id);
457
463
  }
458
464
  function getSiteByName(db, name) {
459
465
  let slug;
@@ -462,10 +468,10 @@ function getSiteByName(db, name) {
462
468
  } catch {
463
469
  return null;
464
470
  }
465
- return db.query("SELECT id, name, title, created_hlc FROM sites WHERE name = ? AND __deleted = 0 ORDER BY created_hlc DESC LIMIT 1").get(slug);
471
+ return db.query(`SELECT ${SITE_SELECT} FROM sites WHERE name = ? AND __deleted = 0 ORDER BY created_hlc DESC LIMIT 1`).get(slug);
466
472
  }
467
473
  function listSites(db) {
468
- return db.query("SELECT id, name, title, created_hlc FROM sites WHERE __deleted = 0 ORDER BY created_hlc").all();
474
+ return db.query(`SELECT ${SITE_SELECT} FROM sites WHERE __deleted = 0 ORDER BY created_hlc`).all();
469
475
  }
470
476
  function resolveSite(db, ref) {
471
477
  const byId = ref.startsWith("site_") ? getSite(db, ref) : null;
@@ -474,28 +480,67 @@ function resolveSite(db, ref) {
474
480
  throw new MhError("not_found", `no such site: ${ref}`);
475
481
  return site;
476
482
  }
477
- function updateSite(db, id, opts) {
478
- if (!getSite(db, id))
479
- throw new MhError("not_found", `no such site: ${id}`);
480
- if (opts.name !== undefined) {
481
- const name = normalizeSiteName(opts.name);
482
- const dup = getSiteByName(db, name);
483
- if (dup && dup.id !== id)
484
- throw new MhError("conflict", `site name already exists: ${name}`);
485
- emit(db, "sites", id, "name", name);
486
- }
487
- if (opts.title !== undefined)
488
- emit(db, "sites", id, "title", opts.title);
489
- return getSite(db, id);
483
+ function getFileRow(db, siteId, path) {
484
+ const withIndex = path === "" || path.endsWith("/") ? `${path}index.html` : path;
485
+ const p = normalizeSitePath(withIndex);
486
+ if (!p)
487
+ return null;
488
+ return db.query("SELECT content_type, encoding, content FROM site_files WHERE site_id = ? AND path = ? AND __deleted = 0 ORDER BY created_hlc LIMIT 1").get(siteId, p);
490
489
  }
491
- function deleteSite(db, id) {
492
- if (!getSite(db, id))
493
- return false;
494
- const files = db.query("SELECT id FROM site_files WHERE site_id = ? AND __deleted = 0").all(id);
495
- for (const f of files)
496
- emit(db, "site_files", f.id, "__deleted", 1);
497
- emit(db, "sites", id, "__deleted", 1);
498
- return true;
490
+ function listFiles(db, siteId) {
491
+ return db.query("SELECT id, site_id, path, content_type, encoding FROM site_files WHERE site_id = ? AND __deleted = 0 ORDER BY path").all(siteId);
492
+ }
493
+ var SITE_COLS, SITE_SELECT, SITE_FILE_COLS, SITE_FILE_SELECT, MIME;
494
+ var init_sites_core = __esm(() => {
495
+ init_errors();
496
+ SITE_COLS = ["id", "name", "title", "created_hlc"];
497
+ SITE_SELECT = SITE_COLS.join(", ");
498
+ SITE_FILE_COLS = [
499
+ "id",
500
+ "site_id",
501
+ "path",
502
+ "content_type",
503
+ "encoding",
504
+ "content"
505
+ ];
506
+ SITE_FILE_SELECT = SITE_FILE_COLS.join(", ");
507
+ MIME = {
508
+ html: "text/html; charset=utf-8",
509
+ htm: "text/html; charset=utf-8",
510
+ css: "text/css; charset=utf-8",
511
+ js: "text/javascript; charset=utf-8",
512
+ mjs: "text/javascript; charset=utf-8",
513
+ json: "application/json; charset=utf-8",
514
+ svg: "image/svg+xml",
515
+ txt: "text/plain; charset=utf-8",
516
+ md: "text/markdown; charset=utf-8",
517
+ xml: "application/xml; charset=utf-8",
518
+ webmanifest: "application/manifest+json",
519
+ ico: "image/x-icon",
520
+ png: "image/png",
521
+ jpg: "image/jpeg",
522
+ jpeg: "image/jpeg",
523
+ gif: "image/gif",
524
+ webp: "image/webp",
525
+ avif: "image/avif",
526
+ woff: "font/woff",
527
+ woff2: "font/woff2",
528
+ ttf: "font/ttf",
529
+ otf: "font/otf",
530
+ pdf: "application/pdf",
531
+ wasm: "application/wasm"
532
+ };
533
+ });
534
+
535
+ // src/core/sites.ts
536
+ function isTextType(contentType) {
537
+ const ct = contentType.toLowerCase();
538
+ return ct.startsWith("text/") || ct.startsWith("application/json") || ct.startsWith("application/manifest+json") || ct.startsWith("application/xml") || ct.startsWith("text/javascript") || ct.startsWith("application/javascript") || ct.startsWith("image/svg+xml");
539
+ }
540
+ function toBytes(data) {
541
+ if (typeof data === "string")
542
+ return new TextEncoder().encode(data);
543
+ return data instanceof Uint8Array ? data : new Uint8Array(data);
499
544
  }
500
545
  function fileIdFor(db, siteId, path) {
501
546
  const row = db.query("SELECT id FROM site_files WHERE site_id = ? AND path = ? ORDER BY created_hlc LIMIT 1").get(siteId, path);
@@ -521,36 +566,21 @@ async function putFile(db, siteId, path, opts) {
521
566
  }
522
567
  const existing = fileIdFor(db, siteId, cleanPath);
523
568
  const id = existing ?? newId("sf", cleanPath);
524
- if (!existing) {
525
- const first = emit(db, "site_files", id, "site_id", siteId);
526
- emit(db, "site_files", id, "path", cleanPath);
527
- emit(db, "site_files", id, "created_hlc", first.hlc);
528
- }
529
- emit(db, "site_files", id, "content_type", contentType);
530
- emit(db, "site_files", id, "encoding", encoding);
531
- emit(db, "site_files", id, "content", content);
532
- emit(db, "site_files", id, "__deleted", 0);
533
- return db.query("SELECT id, site_id, path, content_type, encoding, content FROM site_files WHERE id = ?").get(id);
534
- }
535
- function listFiles(db, siteId) {
536
- return db.query("SELECT id, site_id, path, content_type, encoding FROM site_files WHERE site_id = ? AND __deleted = 0 ORDER BY path").all(siteId);
537
- }
538
- function deleteFile(db, siteId, path) {
539
- const id = fileIdFor(db, siteId, normalizeSitePath(path));
540
- if (!id)
541
- return false;
542
- const live = db.query("SELECT __deleted AS d FROM site_files WHERE id = ?").get(id);
543
- if (!live || live.d)
544
- return false;
545
- emit(db, "site_files", id, "__deleted", 1);
546
- return true;
569
+ withChangeGroup(null, () => {
570
+ if (!existing) {
571
+ const first = emit(db, "site_files", id, "site_id", siteId);
572
+ emit(db, "site_files", id, "path", cleanPath);
573
+ emit(db, "site_files", id, "created_hlc", first.hlc);
574
+ }
575
+ emit(db, "site_files", id, "content_type", contentType);
576
+ emit(db, "site_files", id, "encoding", encoding);
577
+ emit(db, "site_files", id, "content", content);
578
+ emit(db, "site_files", id, "__deleted", 0);
579
+ });
580
+ return db.query(`SELECT ${SITE_FILE_SELECT} FROM site_files WHERE id = ?`).get(id);
547
581
  }
548
582
  async function getFileForServe(db, siteId, path) {
549
- const withIndex = path === "" || path.endsWith("/") ? `${path}index.html` : path;
550
- const p = normalizeSitePath(withIndex);
551
- if (!p)
552
- return null;
553
- const row = db.query("SELECT content_type, encoding, content FROM site_files WHERE site_id = ? AND path = ? AND __deleted = 0 ORDER BY created_hlc LIMIT 1").get(siteId, p);
583
+ const row = getFileRow(db, siteId, path);
554
584
  if (!row)
555
585
  return null;
556
586
  const content = row.content ?? "";
@@ -565,39 +595,59 @@ async function getFileForServe(db, siteId, path) {
565
595
  return null;
566
596
  return { contentType: row.content_type, bytes };
567
597
  }
568
- var INLINE_LIMIT, MIME;
598
+ var INLINE_LIMIT, createSite, updateSite, deleteSite, deleteFile;
569
599
  var init_sites = __esm(() => {
570
600
  init_ids();
571
601
  init_crdt();
572
602
  init_cache();
573
603
  init_errors();
604
+ init_sites_core();
605
+ init_sites_core();
574
606
  INLINE_LIMIT = 256 * 1024;
575
- MIME = {
576
- html: "text/html; charset=utf-8",
577
- htm: "text/html; charset=utf-8",
578
- css: "text/css; charset=utf-8",
579
- js: "text/javascript; charset=utf-8",
580
- mjs: "text/javascript; charset=utf-8",
581
- json: "application/json; charset=utf-8",
582
- svg: "image/svg+xml",
583
- txt: "text/plain; charset=utf-8",
584
- md: "text/markdown; charset=utf-8",
585
- xml: "application/xml; charset=utf-8",
586
- webmanifest: "application/manifest+json",
587
- ico: "image/x-icon",
588
- png: "image/png",
589
- jpg: "image/jpeg",
590
- jpeg: "image/jpeg",
591
- gif: "image/gif",
592
- webp: "image/webp",
593
- avif: "image/avif",
594
- woff: "font/woff",
595
- woff2: "font/woff2",
596
- ttf: "font/ttf",
597
- otf: "font/otf",
598
- pdf: "application/pdf",
599
- wasm: "application/wasm"
600
- };
607
+ createSite = grouped(function createSite2(db, opts) {
608
+ const name = normalizeSiteName(opts.name);
609
+ if (getSiteByName(db, name))
610
+ throw new MhError("conflict", `site name already exists: ${name}`);
611
+ const id = newId("site", name);
612
+ const first = emit(db, "sites", id, "name", name);
613
+ emit(db, "sites", id, "created_hlc", first.hlc);
614
+ if (opts.title !== undefined)
615
+ emit(db, "sites", id, "title", opts.title);
616
+ return getSite(db, id);
617
+ });
618
+ updateSite = grouped(function updateSite2(db, id, opts) {
619
+ if (!getSite(db, id))
620
+ throw new MhError("not_found", `no such site: ${id}`);
621
+ if (opts.name !== undefined) {
622
+ const name = normalizeSiteName(opts.name);
623
+ const dup = getSiteByName(db, name);
624
+ if (dup && dup.id !== id)
625
+ throw new MhError("conflict", `site name already exists: ${name}`);
626
+ emit(db, "sites", id, "name", name);
627
+ }
628
+ if (opts.title !== undefined)
629
+ emit(db, "sites", id, "title", opts.title);
630
+ return getSite(db, id);
631
+ });
632
+ deleteSite = grouped(function deleteSite2(db, id) {
633
+ if (!getSite(db, id))
634
+ return false;
635
+ const files = db.query("SELECT id FROM site_files WHERE site_id = ? AND __deleted = 0").all(id);
636
+ for (const f of files)
637
+ emit(db, "site_files", f.id, "__deleted", 1);
638
+ emit(db, "sites", id, "__deleted", 1);
639
+ return true;
640
+ });
641
+ deleteFile = grouped(function deleteFile2(db, siteId, path) {
642
+ const id = fileIdFor(db, siteId, normalizeSitePath(path));
643
+ if (!id)
644
+ return false;
645
+ const live = db.query("SELECT __deleted AS d FROM site_files WHERE id = ?").get(id);
646
+ if (!live || live.d)
647
+ return false;
648
+ emit(db, "site_files", id, "__deleted", 1);
649
+ return true;
650
+ });
601
651
  });
602
652
 
603
653
  // src/core/sync/sites-serve.ts
@@ -647,6 +697,8 @@ CREATE TABLE IF NOT EXISTS meta (
647
697
  );
648
698
 
649
699
  -- CRDT oplog: one row per field assignment. Source of truth for sync.
700
+ -- txn groups the changes of one logical mutation for history rendering
701
+ -- (nullable: legacy rows and changes from pre-txn peers have none).
650
702
  CREATE TABLE IF NOT EXISTS crdt_changes (
651
703
  hlc TEXT NOT NULL,
652
704
  node_id TEXT NOT NULL,
@@ -654,9 +706,14 @@ CREATE TABLE IF NOT EXISTS crdt_changes (
654
706
  row_id TEXT NOT NULL,
655
707
  col TEXT NOT NULL,
656
708
  value TEXT,
709
+ txn TEXT,
657
710
  PRIMARY KEY (dataset, row_id, col, hlc)
658
711
  );
659
712
  CREATE INDEX IF NOT EXISTS idx_changes_hlc ON crdt_changes(hlc);
713
+ -- Serves history's "every block ever attached to this doc" lookup. Partial so
714
+ -- it never indexes the large values other datasets carry (e.g. site file bodies).
715
+ CREATE INDEX IF NOT EXISTS idx_changes_docref ON crdt_changes(value)
716
+ WHERE dataset = 'doc_blocks' AND col = 'doc_id';
660
717
 
661
718
  -- Per-peer replication cursors (SQLite rowids: monotonic in insertion order,
662
719
  -- so no change is ever skipped regardless of HLC/clock skew). Outbound side of
@@ -796,47 +853,6 @@ init_crdt();
796
853
  init_ids();
797
854
  init_crdt();
798
855
  init_errors();
799
- function createDatabase(db, opts) {
800
- const id = newId("db", opts.name);
801
- const first = emit(db, "databases", id, "name", opts.name);
802
- emit(db, "databases", id, "created_hlc", first.hlc);
803
- if (opts.icon !== undefined)
804
- emit(db, "databases", id, "icon", opts.icon);
805
- return getDatabase(db, id);
806
- }
807
- function updateDatabase(db, id, fields) {
808
- if (!getDatabase(db, id))
809
- throw new MhError("not_found", `no such database: ${id}`);
810
- if (fields.name !== undefined)
811
- emit(db, "databases", id, "name", fields.name);
812
- if (fields.icon !== undefined)
813
- emit(db, "databases", id, "icon", fields.icon);
814
- return getDatabase(db, id);
815
- }
816
- function getDatabase(db, id) {
817
- return db.query("SELECT id, name, icon, created_hlc FROM databases WHERE id = ? AND __deleted = 0").get(id);
818
- }
819
- function listDatabases(db) {
820
- return db.query("SELECT id, name, icon, created_hlc FROM databases WHERE __deleted = 0 ORDER BY created_hlc").all();
821
- }
822
- function deleteDatabase(db, id) {
823
- if (!getDatabase(db, id))
824
- return false;
825
- emit(db, "databases", id, "__deleted", 1);
826
- const tombstone = (table) => {
827
- for (const r of liveChildren(db, table, "database_id", id))
828
- emit(db, table, r, "__deleted", 1);
829
- };
830
- tombstone("properties");
831
- tombstone("records");
832
- for (const d of liveChildren(db, "documents", "database_id", id))
833
- emit(db, "documents", d, "database_id", null);
834
- db.query("DELETE FROM meta WHERE key = 'current_db' AND value = ?").run(id);
835
- return true;
836
- }
837
- function liveChildren(db, table, col, parentId) {
838
- return db.query(`SELECT id FROM ${table} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r) => r.id);
839
- }
840
856
 
841
857
  // src/core/properties.ts
842
858
  init_ids();
@@ -884,6 +900,8 @@ var PROP_TYPES = new Set([
884
900
  "relation",
885
901
  "url"
886
902
  ]);
903
+ var PROPERTY_COLS = ["id", "database_id", "name", "type", "config", "position"];
904
+ var PROPERTY_SELECT = PROPERTY_COLS.join(", ");
887
905
  function validateConfig(type, config) {
888
906
  if (type === "select" || type === "multi_select") {
889
907
  const opts = config?.options;
@@ -899,7 +917,7 @@ function nextPosition(db, databaseId) {
899
917
  const row = db.query("SELECT MAX(position) AS m FROM properties WHERE database_id = ? AND __deleted = 0").get(databaseId);
900
918
  return (row.m ?? 0) + 1;
901
919
  }
902
- function addProperty(db, databaseId, opts) {
920
+ var addProperty = grouped(function addProperty2(db, databaseId, opts) {
903
921
  if (!getDatabase(db, databaseId))
904
922
  throw new MhError("not_found", `no such database: ${databaseId}`);
905
923
  if (!PROP_TYPES.has(opts.type))
@@ -915,21 +933,21 @@ function addProperty(db, databaseId, opts) {
915
933
  if (opts.type === "relation" || opts.indexed === true)
916
934
  ensurePropIndex(db, databaseId, id);
917
935
  return getProperty(db, id);
918
- }
936
+ });
919
937
  function getProperty(db, id) {
920
- const row = db.query("SELECT id, database_id, name, type, config, position FROM properties WHERE id = ? AND __deleted = 0").get(id);
938
+ const row = db.query(`SELECT ${PROPERTY_SELECT} FROM properties WHERE id = ? AND __deleted = 0`).get(id);
921
939
  if (!row)
922
940
  return null;
923
941
  return { ...row, config: row.config ? JSON.parse(row.config) : null };
924
942
  }
925
943
  function listProperties(db, databaseId) {
926
- const rows = db.query("SELECT id, database_id, name, type, config, position FROM properties WHERE database_id = ? AND __deleted = 0 ORDER BY position").all(databaseId);
944
+ const rows = db.query(`SELECT ${PROPERTY_SELECT} FROM properties WHERE database_id = ? AND __deleted = 0 ORDER BY position`).all(databaseId);
927
945
  return rows.map((r) => ({
928
946
  ...r,
929
947
  config: r.config ? JSON.parse(r.config) : null
930
948
  }));
931
949
  }
932
- function updateProperty(db, id, fields) {
950
+ var updateProperty = grouped(function updateProperty2(db, id, fields) {
933
951
  const cur = getProperty(db, id);
934
952
  if (!cur)
935
953
  throw new MhError("not_found", `no such property: ${id}`);
@@ -955,7 +973,7 @@ function updateProperty(db, id, fields) {
955
973
  emit(db, "records", r.id, id, null);
956
974
  }
957
975
  return getProperty(db, id);
958
- }
976
+ });
959
977
  function setPropertyWidth(db, id, width) {
960
978
  const cur = getProperty(db, id);
961
979
  if (!cur)
@@ -965,7 +983,7 @@ function setPropertyWidth(db, id, width) {
965
983
  const w = Math.max(80, Math.min(2000, Math.round(width)));
966
984
  return updateProperty(db, id, { config: { ...cur.config ?? {}, width: w } });
967
985
  }
968
- function removeProperty(db, id) {
986
+ var removeProperty = grouped(function removeProperty2(db, id) {
969
987
  const prop = getProperty(db, id);
970
988
  if (!prop)
971
989
  return false;
@@ -974,6 +992,76 @@ function removeProperty(db, id) {
974
992
  for (const r of rows)
975
993
  emit(db, "records", r.id, id, undefined);
976
994
  return true;
995
+ });
996
+
997
+ // src/core/databases.ts
998
+ var DATABASE_COLS = ["id", "name", "icon", "created_hlc"];
999
+ var DATABASE_SELECT = DATABASE_COLS.join(", ");
1000
+ var createDatabase = grouped(function createDatabase2(db, opts) {
1001
+ const id = newId("db", opts.name);
1002
+ const first = emit(db, "databases", id, "name", opts.name);
1003
+ emit(db, "databases", id, "created_hlc", first.hlc);
1004
+ if (opts.icon !== undefined)
1005
+ emit(db, "databases", id, "icon", opts.icon);
1006
+ return getDatabase(db, id);
1007
+ });
1008
+ var updateDatabase = grouped(function updateDatabase2(db, id, fields) {
1009
+ if (!getDatabase(db, id))
1010
+ throw new MhError("not_found", `no such database: ${id}`);
1011
+ if (fields.name !== undefined)
1012
+ emit(db, "databases", id, "name", fields.name);
1013
+ if (fields.icon !== undefined)
1014
+ emit(db, "databases", id, "icon", fields.icon);
1015
+ return getDatabase(db, id);
1016
+ });
1017
+ var duplicateDatabase = grouped(function duplicateDatabase2(db, id, opts = {}) {
1018
+ const src = getDatabase(db, id);
1019
+ if (!src)
1020
+ throw new MhError("not_found", `no such database: ${id}`);
1021
+ const dup = createDatabase(db, {
1022
+ name: opts.name ?? src.name,
1023
+ icon: opts.icon ?? src.icon ?? undefined
1024
+ });
1025
+ const propIdMap = new Map;
1026
+ for (const p of listProperties(db, id)) {
1027
+ const config = p.config?.database === id ? { ...p.config, database: dup.id } : p.config ?? undefined;
1028
+ const np = addProperty(db, dup.id, { name: p.name, type: p.type, config, position: p.position });
1029
+ propIdMap.set(p.id, np.id);
1030
+ }
1031
+ for (const r of listRecords(db, id)) {
1032
+ const data = {};
1033
+ for (const [pid, v] of Object.entries(r.cells)) {
1034
+ const nid = propIdMap.get(pid);
1035
+ if (nid)
1036
+ data[nid] = v;
1037
+ }
1038
+ createRecord(db, dup.id, data);
1039
+ }
1040
+ return getDatabase(db, dup.id);
1041
+ });
1042
+ function getDatabase(db, id) {
1043
+ return db.query(`SELECT ${DATABASE_SELECT} FROM databases WHERE id = ? AND __deleted = 0`).get(id);
1044
+ }
1045
+ function listDatabases(db) {
1046
+ return db.query(`SELECT ${DATABASE_SELECT} FROM databases WHERE __deleted = 0 ORDER BY created_hlc`).all();
1047
+ }
1048
+ var deleteDatabase = grouped(function deleteDatabase2(db, id) {
1049
+ if (!getDatabase(db, id))
1050
+ return false;
1051
+ emit(db, "databases", id, "__deleted", 1);
1052
+ const tombstone = (table) => {
1053
+ for (const r of liveChildren(db, table, "database_id", id))
1054
+ emit(db, table, r, "__deleted", 1);
1055
+ };
1056
+ tombstone("properties");
1057
+ tombstone("records");
1058
+ for (const d of liveChildren(db, "documents", "database_id", id))
1059
+ emit(db, "documents", d, "database_id", null);
1060
+ db.query("DELETE FROM meta WHERE key = 'current_db' AND value = ?").run(id);
1061
+ return true;
1062
+ });
1063
+ function liveChildren(db, table, col, parentId) {
1064
+ return db.query(`SELECT id FROM ${table} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r) => r.id);
977
1065
  }
978
1066
 
979
1067
  // src/core/resolve.ts
@@ -1191,12 +1279,30 @@ function coerce(db, prop, value) {
1191
1279
  }
1192
1280
  }
1193
1281
  }
1282
+ function propsByName(props) {
1283
+ const byName = new Map;
1284
+ for (const p of props) {
1285
+ const k = p.name.toLowerCase();
1286
+ const list = byName.get(k);
1287
+ if (list)
1288
+ list.push(p);
1289
+ else
1290
+ byName.set(k, [p]);
1291
+ }
1292
+ return byName;
1293
+ }
1194
1294
  function resolveData(props, data) {
1195
1295
  const byId = new Map(props.map((p) => [p.id, p]));
1196
- const byName = new Map(props.map((p) => [p.name.toLowerCase(), p]));
1296
+ const byName = propsByName(props);
1197
1297
  const out = [];
1198
1298
  for (const [key, value] of Object.entries(data)) {
1199
- const prop = byId.get(key) ?? byName.get(key.toLowerCase());
1299
+ let prop = byId.get(key);
1300
+ if (!prop) {
1301
+ const matches = byName.get(key.toLowerCase()) ?? [];
1302
+ if (matches.length > 1)
1303
+ throw new MhError("ambiguous", `property name "${key}" matches ${matches.length} properties; use a property id`);
1304
+ prop = matches[0];
1305
+ }
1200
1306
  if (!prop)
1201
1307
  throw new MhError("not_found", `unknown property: ${key}`);
1202
1308
  out.push({ prop, value });
@@ -1207,12 +1313,15 @@ function rowToRecord(row, props) {
1207
1313
  const raw = JSON.parse(row.data || "{}");
1208
1314
  const byId = new Map(props.map((p) => [p.id, p]));
1209
1315
  const values = {};
1316
+ const cells = {};
1210
1317
  for (const [propId, v] of Object.entries(raw)) {
1211
1318
  const p = byId.get(propId);
1212
- if (p)
1213
- values[p.name] = v;
1319
+ if (!p)
1320
+ continue;
1321
+ values[p.name] = v;
1322
+ cells[p.id] = v;
1214
1323
  }
1215
- return { id: row.id, database_id: row.database_id, values };
1324
+ return { id: row.id, database_id: row.database_id, values, cells };
1216
1325
  }
1217
1326
  function deriveTitle(props, resolved, fallbackBase) {
1218
1327
  const text = resolved.find((r) => r.prop.type === "text" && typeof r.value === "string" && r.value);
@@ -1249,7 +1358,7 @@ function rebalanceRecordOrderKeys(db, databaseId) {
1249
1358
  emit(db, "records", row.id, "order_key", keys[i]);
1250
1359
  });
1251
1360
  }
1252
- function backfillRecordOrderKeys(db, databaseId) {
1361
+ var backfillRecordOrderKeys = grouped(function backfillRecordOrderKeys2(db, databaseId) {
1253
1362
  const emitChange = canEmitOrderKeys(db);
1254
1363
  const dbRows = databaseId ? [{ database_id: databaseId }] : db.query(`SELECT DISTINCT database_id FROM records
1255
1364
  WHERE database_id IS NOT NULL AND __deleted = 0 AND order_key IS NULL`).all();
@@ -1263,8 +1372,8 @@ function backfillRecordOrderKeys(db, databaseId) {
1263
1372
  const keys = keysBetween(start, null, missing.length);
1264
1373
  missing.forEach((row, i) => writeRecordOrderKey(db, row.id, keys[i], emitChange));
1265
1374
  }
1266
- }
1267
- function createRecord(db, databaseId, data) {
1375
+ });
1376
+ var createRecord = grouped(function createRecord2(db, databaseId, data) {
1268
1377
  const dbRow = getDatabase(db, databaseId);
1269
1378
  if (!dbRow)
1270
1379
  throw new MhError("not_found", `no such database: ${databaseId}`);
@@ -1278,7 +1387,7 @@ function createRecord(db, databaseId, data) {
1278
1387
  for (const { prop, value } of resolved)
1279
1388
  emit(db, "records", id, prop.id, coerce(db, prop, value));
1280
1389
  return getRecord(db, id);
1281
- }
1390
+ });
1282
1391
  function getRecord(db, id) {
1283
1392
  const row = db.query("SELECT id, database_id, data FROM records WHERE id = ? AND __deleted = 0").get(id);
1284
1393
  if (!row)
@@ -1305,7 +1414,13 @@ function listRecords(db, databaseId, opts = {}) {
1305
1414
  key = key.slice(1);
1306
1415
  }
1307
1416
  if (key !== "created" && key !== "created_hlc") {
1308
- const p = props.find((p2) => p2.name.toLowerCase() === key.toLowerCase() || p2.id === key);
1417
+ let p = props.find((p2) => p2.id === key);
1418
+ if (!p) {
1419
+ const matches = props.filter((q) => q.name.toLowerCase() === key.toLowerCase());
1420
+ if (matches.length > 1)
1421
+ throw new MhError("ambiguous", `sort field "${key}" matches ${matches.length} properties; use a property id`);
1422
+ p = matches[0];
1423
+ }
1309
1424
  if (!p)
1310
1425
  throw new MhError("not_found", `unknown sort field: ${key}`);
1311
1426
  sortProp = p;
@@ -1331,13 +1446,13 @@ function listRecords(db, databaseId, opts = {}) {
1331
1446
  }
1332
1447
  let rows = db.query(sql).all(...args).map((r) => rowToRecord(r, props));
1333
1448
  if (jsFilters.length) {
1334
- rows = rows.filter((r) => jsFilters.every((f) => JSON.stringify(r.values[f.prop.name] ?? null) === JSON.stringify(f.value)));
1449
+ rows = rows.filter((r) => jsFilters.every((f) => JSON.stringify(r.cells[f.prop.id] ?? null) === JSON.stringify(f.value)));
1335
1450
  if (opts.limit != null)
1336
1451
  rows = rows.slice(0, opts.limit);
1337
1452
  }
1338
1453
  return rows;
1339
1454
  }
1340
- function moveRecord(db, id, targetId, where) {
1455
+ var moveRecord = grouped(function moveRecord2(db, id, targetId, where) {
1341
1456
  if (where !== "before" && where !== "after")
1342
1457
  throw new MhError("invalid_input", `unknown move position: ${where}`);
1343
1458
  const src = db.query("SELECT id, database_id FROM records WHERE id = ? AND __deleted = 0").get(id);
@@ -1366,8 +1481,8 @@ function moveRecord(db, id, targetId, where) {
1366
1481
  }
1367
1482
  emit(db, "records", id, "order_key", keyBetween(left, right));
1368
1483
  return getRecord(db, id);
1369
- }
1370
- function updateRecord(db, id, data) {
1484
+ });
1485
+ var updateRecord = grouped(function updateRecord2(db, id, data) {
1371
1486
  const rec = db.query("SELECT id, database_id FROM records WHERE id = ? AND __deleted = 0").get(id);
1372
1487
  if (!rec)
1373
1488
  throw new MhError("not_found", `no such record: ${id}`);
@@ -1375,19 +1490,38 @@ function updateRecord(db, id, data) {
1375
1490
  for (const { prop, value } of resolved)
1376
1491
  emit(db, "records", id, prop.id, coerce(db, prop, value));
1377
1492
  return getRecord(db, id);
1378
- }
1379
- function deleteRecord(db, id) {
1493
+ });
1494
+ var deleteRecord = grouped(function deleteRecord2(db, id) {
1380
1495
  const rec = db.query("SELECT id FROM records WHERE id = ? AND __deleted = 0").get(id);
1381
1496
  if (!rec)
1382
1497
  return false;
1383
1498
  emit(db, "records", id, "__deleted", 1);
1384
1499
  return true;
1385
- }
1500
+ });
1386
1501
 
1387
1502
  // src/core/documents.ts
1388
1503
  init_ids();
1389
1504
  init_crdt();
1390
1505
  init_errors();
1506
+ var DOCUMENT_COLS = [
1507
+ "id",
1508
+ "title",
1509
+ "body",
1510
+ "database_id",
1511
+ "parent_id",
1512
+ "created_hlc",
1513
+ "order_key"
1514
+ ];
1515
+ var DOCUMENT_SELECT = DOCUMENT_COLS.join(", ");
1516
+ var DOC_SUMMARY_COLS = [
1517
+ "id",
1518
+ "title",
1519
+ "database_id",
1520
+ "parent_id",
1521
+ "created_hlc",
1522
+ "order_key"
1523
+ ];
1524
+ var DOC_SUMMARY_SELECT = DOC_SUMMARY_COLS.join(", ");
1391
1525
  function liveBlocks(db, docId) {
1392
1526
  return db.query("SELECT id, text, order_key, blank_after FROM doc_blocks WHERE doc_id = ? AND __deleted = 0 ORDER BY order_key, id").all(docId);
1393
1527
  }
@@ -1448,7 +1582,7 @@ function documentVersion(db, id) {
1448
1582
  OR (dataset = 'doc_blocks' AND row_id IN (SELECT id FROM doc_blocks WHERE doc_id = ?))`).get(id, id);
1449
1583
  return row.h ?? "";
1450
1584
  }
1451
- function createDocument(db, opts) {
1585
+ var createDocument = grouped(function createDocument2(db, opts) {
1452
1586
  const id = newId("doc", opts.title);
1453
1587
  const first = emit(db, "documents", id, "title", opts.title);
1454
1588
  emit(db, "documents", id, "created_hlc", first.hlc);
@@ -1463,13 +1597,13 @@ function createDocument(db, opts) {
1463
1597
  insertBlocks(db, id, blocks, null, null);
1464
1598
  }
1465
1599
  return getDocument(db, id);
1466
- }
1600
+ });
1467
1601
  function getDocument(db, id) {
1468
- return db.query("SELECT id, title, body, database_id, parent_id, created_hlc, order_key FROM documents WHERE id = ? AND __deleted = 0").get(id);
1602
+ return db.query(`SELECT ${DOCUMENT_SELECT} FROM documents WHERE id = ? AND __deleted = 0`).get(id);
1469
1603
  }
1470
1604
  var ORDER_BY = "ORDER BY order_key IS NULL, order_key, created_hlc, id";
1471
1605
  function listDocuments(db, opts = {}) {
1472
- const cols = "SELECT id, title, database_id, parent_id, created_hlc, order_key FROM documents WHERE __deleted = 0";
1606
+ const cols = `SELECT ${DOC_SUMMARY_SELECT} FROM documents WHERE __deleted = 0`;
1473
1607
  if (opts.parent_id !== undefined)
1474
1608
  return db.query(`${cols} AND parent_id = ? ${ORDER_BY}`).all(opts.parent_id);
1475
1609
  if (opts.database_id)
@@ -1520,11 +1654,11 @@ function backfillSiblings(db, parentId) {
1520
1654
  db.query("UPDATE documents SET order_key = ? WHERE id = ?").run(keys[i], row.id);
1521
1655
  });
1522
1656
  }
1523
- function backfillDocumentOrderKeys(db) {
1657
+ var backfillDocumentOrderKeys = grouped(function backfillDocumentOrderKeys2(db) {
1524
1658
  const parents = db.query("SELECT DISTINCT parent_id FROM documents WHERE __deleted = 0 AND order_key IS NULL").all();
1525
1659
  for (const p of parents)
1526
1660
  backfillSiblings(db, p.parent_id);
1527
- }
1661
+ });
1528
1662
  function placeInSiblings(db, id, parentId, anchor) {
1529
1663
  let cur = parentId;
1530
1664
  while (cur) {
@@ -1556,7 +1690,7 @@ function placeInSiblings(db, id, parentId, anchor) {
1556
1690
  }
1557
1691
  emit(db, "documents", id, "order_key", key);
1558
1692
  }
1559
- function moveDocument(db, id, targetId, where) {
1693
+ var moveDocument = grouped(function moveDocument2(db, id, targetId, where) {
1560
1694
  const src = getDocument(db, id);
1561
1695
  if (!src)
1562
1696
  throw new MhError("not_found", `no such document: ${id}`);
@@ -1570,8 +1704,26 @@ function moveDocument(db, id, targetId, where) {
1570
1704
  else
1571
1705
  placeInSiblings(db, id, target.parent_id, { targetId, where });
1572
1706
  return getDocument(db, id);
1573
- }
1574
- function updateDocument(db, id, fields, opts = {}) {
1707
+ });
1708
+ var duplicateDocument = grouped(function duplicateDocument2(db, id, opts = {}) {
1709
+ const src = getDocument(db, id);
1710
+ if (!src)
1711
+ throw new MhError("not_found", `no such document: ${id}`);
1712
+ ensureBlocks(db, id);
1713
+ const blocks = liveBlocks(db, id);
1714
+ const parentId = opts.parentId !== undefined ? opts.parentId : src.parent_id;
1715
+ const dup = createDocument(db, {
1716
+ title: opts.title ?? src.title,
1717
+ database_id: src.database_id ?? undefined,
1718
+ parent_id: parentId ?? undefined
1719
+ });
1720
+ if (blocks.length)
1721
+ insertBlocks(db, dup.id, blocks.map((b) => ({ text: b.text, blankAfter: b.blank_after })), null, null);
1722
+ if (parentId === src.parent_id)
1723
+ moveDocument(db, dup.id, src.id, "after");
1724
+ return getDocument(db, dup.id);
1725
+ });
1726
+ var updateDocument = grouped(function updateDocument2(db, id, fields, opts = {}) {
1575
1727
  if (!getDocument(db, id))
1576
1728
  throw new MhError("not_found", `no such document: ${id}`);
1577
1729
  if (opts.ifMatch !== undefined && documentVersion(db, id) !== opts.ifMatch)
@@ -1590,8 +1742,8 @@ function updateDocument(db, id, fields, opts = {}) {
1590
1742
  reconcileBody(db, id, fields.body);
1591
1743
  }
1592
1744
  return getDocument(db, id);
1593
- }
1594
- function editDocument(db, id, opts) {
1745
+ });
1746
+ var editDocument = grouped(function editDocument2(db, id, opts) {
1595
1747
  if (!getDocument(db, id))
1596
1748
  throw new MhError("not_found", `no such document: ${id}`);
1597
1749
  if (opts.ifMatch !== undefined && documentVersion(db, id) !== opts.ifMatch)
@@ -1622,8 +1774,8 @@ function editDocument(db, id, opts) {
1622
1774
  const newBody = opts.replaceAll ? body.replaceAll(opts.old, opts.new) : body.replace(opts.old, opts.new);
1623
1775
  reconcileBody(db, id, newBody);
1624
1776
  return { id, changed: newBody !== body, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
1625
- }
1626
- function appendDocument(db, id, body) {
1777
+ });
1778
+ var appendDocument = grouped(function appendDocument2(db, id, body) {
1627
1779
  if (!getDocument(db, id))
1628
1780
  throw new MhError("not_found", `no such document: ${id}`);
1629
1781
  ensureBlocks(db, id);
@@ -1634,8 +1786,8 @@ function appendDocument(db, id, body) {
1634
1786
  insertBlocks(db, id, next, last, null);
1635
1787
  }
1636
1788
  return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
1637
- }
1638
- function prependDocument(db, id, body) {
1789
+ });
1790
+ var prependDocument = grouped(function prependDocument2(db, id, body) {
1639
1791
  if (!getDocument(db, id))
1640
1792
  throw new MhError("not_found", `no such document: ${id}`);
1641
1793
  ensureBlocks(db, id);
@@ -1646,8 +1798,8 @@ function prependDocument(db, id, body) {
1646
1798
  insertBlocks(db, id, next, null, first);
1647
1799
  }
1648
1800
  return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
1649
- }
1650
- function deleteDocument(db, id) {
1801
+ });
1802
+ var deleteDocument = grouped(function deleteDocument2(db, id) {
1651
1803
  if (!getDocument(db, id))
1652
1804
  return false;
1653
1805
  emit(db, "documents", id, "__deleted", 1);
@@ -1656,7 +1808,7 @@ function deleteDocument(db, id) {
1656
1808
  for (const c of liveChildIds(db, "documents", "parent_id", id))
1657
1809
  emit(db, "documents", c, "parent_id", null);
1658
1810
  return true;
1659
- }
1811
+ });
1660
1812
  function liveChildIds(db, table, col, parentId) {
1661
1813
  return db.query(`SELECT id FROM ${table} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r) => r.id);
1662
1814
  }
@@ -1672,11 +1824,7 @@ function countOccurrences(haystack, needle) {
1672
1824
  return n;
1673
1825
  }
1674
1826
 
1675
- // src/core/db.ts
1676
- function ensureDirs() {
1677
- mkdirSync(metahubHome(), { recursive: true });
1678
- mkdirSync(cacheDir(), { recursive: true });
1679
- }
1827
+ // src/core/schema-init.ts
1680
1828
  function runSchema(db) {
1681
1829
  db.exec(CORE_SCHEMA);
1682
1830
  try {
@@ -1739,15 +1887,29 @@ function migrateDocuments(db) {
1739
1887
  db.exec("ALTER TABLE documents ADD COLUMN order_key TEXT");
1740
1888
  backfillDocumentOrderKeys(db);
1741
1889
  }
1742
- function openMetahub() {
1743
- ensureDirs();
1744
- const db = new Database(dbPath(), { create: true });
1745
- db.exec("PRAGMA journal_mode = WAL;");
1890
+ function migrateOplog(db) {
1891
+ if (!hasColumn(db, "crdt_changes", "txn"))
1892
+ db.exec("ALTER TABLE crdt_changes ADD COLUMN txn TEXT");
1893
+ }
1894
+ function initSchema(db) {
1746
1895
  runSchema(db);
1896
+ migrateOplog(db);
1747
1897
  migrateRecords(db);
1748
1898
  migratePeers(db);
1749
1899
  migrateDocBlocks(db);
1750
1900
  migrateDocuments(db);
1901
+ }
1902
+
1903
+ // src/core/db.ts
1904
+ function ensureDirs() {
1905
+ mkdirSync(metahubHome(), { recursive: true });
1906
+ mkdirSync(cacheDir(), { recursive: true });
1907
+ }
1908
+ function openMetahub() {
1909
+ ensureDirs();
1910
+ const db = new Database(dbPath(), { create: true });
1911
+ db.exec("PRAGMA journal_mode = WAL;");
1912
+ initSchema(db);
1751
1913
  return db;
1752
1914
  }
1753
1915
 
@@ -16231,12 +16393,15 @@ var ChangeSchema = exports_external.object({
16231
16393
  dataset: exports_external.string().describe("CRDT dataset, e.g. databases / records / documents"),
16232
16394
  row_id: exports_external.string().describe("Row identifier within the dataset"),
16233
16395
  col: exports_external.string().describe("Column / property identifier"),
16234
- value: exports_external.string().nullable().describe("JSON-encoded field value, or null")
16396
+ value: exports_external.string().nullable().describe("JSON-encoded field value, or null"),
16397
+ txn: exports_external.string().nullable().optional().describe("Change-group id (one logical mutation)")
16235
16398
  });
16236
16399
  var SyncRequestSchema = exports_external.object({
16237
16400
  node_id: exports_external.string().describe("Calling client's node id"),
16238
16401
  since: exports_external.number().describe("Server cursor the client last pulled"),
16239
- changes: exports_external.array(ChangeSchema).describe("Client's changes to push")
16402
+ changes: exports_external.array(ChangeSchema).describe("Client's changes to push"),
16403
+ limit: exports_external.number().optional().describe("Max changes to return (pagination for hydration)"),
16404
+ exclude_datasets: exports_external.array(exports_external.string()).optional().describe("Datasets to omit from the pull (partial replica)")
16240
16405
  });
16241
16406
  var SyncResponseSchema = exports_external.object({
16242
16407
  node_id: exports_external.string().describe("Server's node id"),
@@ -16272,10 +16437,11 @@ function getPeer(db, url2) {
16272
16437
  function setPeer(db, url2, c) {
16273
16438
  db.query("INSERT INTO peers (url, pull_cursor, push_cursor) VALUES (?, ?, ?) ON CONFLICT(url) DO UPDATE SET pull_cursor = excluded.pull_cursor, push_cursor = excluded.push_cursor").run(url2, c.pull_cursor, c.push_cursor);
16274
16439
  }
16275
- async function syncWithPeer(db, url2, token) {
16440
+ async function syncWithPeer(db, url2, opts = {}) {
16441
+ const o = typeof opts === "string" ? { token: opts } : opts;
16276
16442
  const node = getNodeId(db);
16277
16443
  const peer = getPeer(db, url2);
16278
- const bearer = token ?? peer.token ?? undefined;
16444
+ const bearer = o.token ?? peer.token ?? undefined;
16279
16445
  const toPush = changesAfterSeq(db, peer.push_cursor);
16280
16446
  const headers = { "content-type": "application/json" };
16281
16447
  if (bearer)
@@ -16283,7 +16449,13 @@ async function syncWithPeer(db, url2, token) {
16283
16449
  const res = await fetch(new URL(SYNC_PATH, url2), {
16284
16450
  method: "POST",
16285
16451
  headers,
16286
- body: JSON.stringify({ node_id: node, since: peer.pull_cursor, changes: toPush.changes })
16452
+ body: JSON.stringify({
16453
+ node_id: node,
16454
+ since: peer.pull_cursor,
16455
+ changes: toPush.changes,
16456
+ ...o.pullLimit != null ? { limit: o.pullLimit } : {},
16457
+ ...o.excludeDatasets?.length ? { exclude_datasets: o.excludeDatasets } : {}
16458
+ })
16287
16459
  });
16288
16460
  if (!res.ok)
16289
16461
  throw new MhError(res.status === 401 || res.status === 403 ? "auth" : "network", `sync failed: ${res.status} ${await res.text()}`);
@@ -16397,7 +16569,7 @@ function getServerConfig(db) {
16397
16569
  // package.json
16398
16570
  var package_default = {
16399
16571
  name: "@tensorix/metahub",
16400
- version: "0.1.10",
16572
+ version: "0.1.11",
16401
16573
  description: "Metahub core library and CLI",
16402
16574
  license: "AGPL-3.0-only",
16403
16575
  type: "module",
@@ -16431,6 +16603,7 @@ var package_default = {
16431
16603
  },
16432
16604
  dependencies: {
16433
16605
  "@clack/prompts": "^1.5.0",
16606
+ "@sqlite.org/sqlite-wasm": "^3.53.0-build1",
16434
16607
  citty: "^0.1.6",
16435
16608
  "highlight.js": "^11.11.1",
16436
16609
  preact: "^10.29.2",
@@ -16439,6 +16612,7 @@ var package_default = {
16439
16612
  zod: "^4.4.3"
16440
16613
  },
16441
16614
  devDependencies: {
16615
+ "@happy-dom/global-registrator": "^20.10.2",
16442
16616
  "@types/bun": "latest",
16443
16617
  "@types/turndown": "^5.0.6",
16444
16618
  typescript: "^5"
@@ -16830,7 +17004,10 @@ var syncRoutes = [
16830
17004
  async handler(req, { db, node }) {
16831
17005
  const body = await req.json();
16832
17006
  ingest(db, body.changes ?? []);
16833
- const batch = changesAfterSeq(db, body.since ?? 0);
17007
+ const batch = changesAfterSeq(db, body.since ?? 0, {
17008
+ limit: body.limit,
17009
+ excludeDatasets: body.exclude_datasets
17010
+ });
16834
17011
  return Response.json({ node_id: node, changes: batch.changes, cursor: batch.cursor });
16835
17012
  }
16836
17013
  },
@@ -16995,50 +17172,14 @@ function unlockPage() {
16995
17172
  </script>
16996
17173
  </body></html>`;
16997
17174
  }
16998
- var SHIM = `<script>(function(){
16999
- var KEY = "mh_token";
17000
- function save(t){
17001
- try { localStorage.setItem(KEY, t); } catch (e) {}
17002
- document.cookie = KEY + "=" + encodeURIComponent(t) + "; path=/; SameSite=Strict; Max-Age=31536000";
17003
- }
17004
- var orig = window.fetch.bind(window);
17005
- window.fetch = function(input, init){
17006
- init = init || {};
17007
- var t = null; try { t = localStorage.getItem(KEY); } catch (e) {}
17008
- var url;
17009
- try { url = new URL((typeof input === "string" ? input : input.url), location.href); } catch (e) { url = null; }
17010
- var same = url && url.origin === location.origin;
17011
- if (same && t) {
17012
- var h = new Headers(init.headers || (typeof input !== "string" && input.headers) || {});
17013
- if (!h.has("authorization")) h.set("authorization", "Bearer " + t);
17014
- init.headers = h;
17015
- }
17016
- return orig(input, init).then(function(res){
17017
- if (res.status !== 401 || !same || !t || init.__mhRetried) return res;
17018
- return orig("${RENEW_PATH}", { headers: { authorization: "Bearer " + t } }).then(function(r){
17019
- if (!r.ok) return res;
17020
- return r.json().then(function(d){
17021
- if (!d || !d.token) return res;
17022
- save(d.token);
17023
- var h2 = new Headers(init.headers || {});
17024
- h2.set("authorization", "Bearer " + d.token);
17025
- init.headers = h2;
17026
- init.__mhRetried = true;
17027
- return orig(input, init);
17028
- });
17029
- }).catch(function(){ return res; });
17030
- });
17031
- };
17032
- })();</script>`;
17175
+ var RUNTIME_TAG = `<script src="/mh-runtime.js"></script>`;
17033
17176
  function injectShim(html) {
17034
17177
  const i = html.toLowerCase().indexOf("<head>");
17035
17178
  if (i >= 0)
17036
- return html.slice(0, i + 6) + SHIM + html.slice(i + 6);
17037
- return SHIM + html;
17179
+ return html.slice(0, i + 6) + RUNTIME_TAG + html.slice(i + 6);
17180
+ return RUNTIME_TAG + html;
17038
17181
  }
17039
- async function withShim(res, cfg) {
17040
- if (!authActive(cfg))
17041
- return res;
17182
+ async function withShim(res, _cfg) {
17042
17183
  const ct = res.headers.get("content-type") ?? "";
17043
17184
  if (!ct.includes("text/html"))
17044
17185
  return res;
@@ -17091,15 +17232,16 @@ function startServer(opts = {}) {
17091
17232
  server = Bun.serve({
17092
17233
  port,
17093
17234
  hostname: host,
17235
+ tls: opts.tls ? { cert: Bun.file(opts.tls.certPath), key: Bun.file(opts.tls.keyPath) } : undefined,
17094
17236
  async fetch(req) {
17095
17237
  const url2 = new URL(req.url);
17096
17238
  if (url2.pathname === SYNC_PATH) {
17097
17239
  if (!acceptsSyncToken(req, url2, auth, db))
17098
17240
  return unauthorized();
17099
17241
  } else {
17100
- const exempt = url2.pathname === HEALTH_PATH || url2.pathname === RENEW_PATH || url2.pathname === PAIR_PATH;
17242
+ const exempt = url2.pathname === HEALTH_PATH || url2.pathname === RENEW_PATH || url2.pathname === PAIR_PATH || url2.pathname === "/manifest.webmanifest" || url2.pathname === "/sw.js" || url2.pathname.startsWith("/icons/");
17101
17243
  if (!exempt && !hasValidToken(req, url2, auth)) {
17102
- return wantsHtml(req) ? new Response(unlockPage(), { headers: HTML_HEADERS }) : unauthorized();
17244
+ return wantsHtml(req) ? new Response(unlockPage(), { headers: { ...HTML_HEADERS, "x-mh-unlock": "1" } }) : unauthorized();
17103
17245
  }
17104
17246
  }
17105
17247
  if (url2.pathname === RENEW_PATH) {
@@ -17156,6 +17298,7 @@ function startServer(opts = {}) {
17156
17298
  token: active?.token ?? null,
17157
17299
  exp: active?.exp ?? null,
17158
17300
  authMode,
17301
+ tls: Boolean(opts.tls),
17159
17302
  autoSync: Boolean(autoSync && syncIntervalMs > 0),
17160
17303
  syncIntervalMs,
17161
17304
  stop() {
@@ -17168,7 +17311,7 @@ function startServer(opts = {}) {
17168
17311
  // src/webui/server/assets.ts
17169
17312
  import { fileURLToPath } from "url";
17170
17313
  import { readdirSync, statSync } from "fs";
17171
- import { join as join3 } from "path";
17314
+ import { join as join3, dirname } from "path";
17172
17315
 
17173
17316
  // src/webui/styles.css
17174
17317
  var styles_default = ` :root {
@@ -17253,7 +17396,26 @@ var styles_default = ` :root {
17253
17396
  /* sidebar */
17254
17397
  .sidebar { width:268px; flex:none; background:var(--sidebar); border-right:1px solid var(--line);
17255
17398
  display:flex; flex-direction:column; position:relative; transition:margin-left .22s cubic-bezier(.4,0,.2,1); }
17256
- .sb-resizer { position:absolute; top:0; right:-3px; width:6px; height:100%; cursor:col-resize; z-index:5; }
17399
+ /* right:-4px (not -3): the 7px handle then straddles the 1px border line
17400
+ symmetrically (3.5px each side of the border's center), so the centered
17401
+ ::before grip line lands exactly on the border instead of 1px left of it. */
17402
+ .sb-resizer { position:absolute; top:0; right:-4px; width:7px; height:100%; cursor:col-resize; z-index:5; }
17403
+ /* Unified resize-handle visual (sidebar + datasheet/doc-table column resizers):
17404
+ the handle stays an invisible hit area; a centered 3px round-capped line
17405
+ fades/expands in on hover and solidifies with a soft halo while dragging.
17406
+ --rz-guide-h (written by startColumnResize) stretches the dragging line to
17407
+ the full table height so the new column edge is visible across all rows. */
17408
+ .sb-resizer::before, .col-resizer::before, .doc-col-resizer::before {
17409
+ content:""; position:absolute; left:50%; top:0; height:100%; width:3px;
17410
+ transform:translateX(-50%) scaleX(.4); border-radius:999px;
17411
+ background:var(--accent); opacity:0; pointer-events:none;
17412
+ transition:opacity .15s cubic-bezier(.4,0,.2,1), transform .15s cubic-bezier(.4,0,.2,1), box-shadow .15s; }
17413
+ .sb-resizer:hover::before, .col-resizer:hover::before, .doc-col-resizer:hover::before {
17414
+ opacity:.6; transform:translateX(-50%) scaleX(1); }
17415
+ .sb-resizer.dragging::before, .col-resizer.dragging::before, .doc-col-resizer.dragging::before {
17416
+ opacity:1; transform:translateX(-50%) scaleX(1);
17417
+ box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
17418
+ height:var(--rz-guide-h, 100%); }
17257
17419
  .sb-head { display:flex; align-items:center; gap:8px; padding:14px 14px 10px; }
17258
17420
  .brand { display:flex; align-items:center; gap:8px; flex:1; font-weight:700; font-size:15px; letter-spacing:-.01em; }
17259
17421
  .brand .mark { width:24px; height:24px; border-radius:7px;
@@ -17385,18 +17547,19 @@ var styles_default = ` :root {
17385
17547
  .btn-primary:hover { filter:brightness(1.07); }
17386
17548
  .btn-danger { background:var(--danger); color:#fff; } .btn-danger:hover { filter:brightness(1.05); }
17387
17549
 
17388
- /* desktop app (frameless): drop the sidebar logo; on macOS reserve a draggable
17389
- strip for the inset traffic lights so they never overlap our controls */
17390
- body.desktop .brand { display:none; }
17550
+ /* macOS desktop (hiddenInset frameless): drop the sidebar logo and reserve a
17551
+ draggable strip so the inset traffic lights never overlap our controls.
17552
+ Windows/Linux keep the native frame, so the logo stays. */
17553
+ body.desktop-mac .brand { display:none; }
17391
17554
  /* logo gone \u2192 keep the collapse button on the right (where .brand's flex used to push it) */
17392
- body.desktop .sb-head .iconbtn { margin-left:auto; }
17555
+ body.desktop-mac .sb-head .iconbtn { margin-left:auto; }
17393
17556
  body.desktop-mac .sb-head { -webkit-app-region:drag; }
17394
17557
  body.desktop-mac .sb-head .iconbtn { -webkit-app-region:no-drag; }
17395
17558
  body.desktop-mac .topbar { -webkit-app-region:drag; }
17396
17559
  body.desktop-mac .topbar .iconbtn, body.desktop-mac .topbar .btn { -webkit-app-region:no-drag; }
17397
17560
  /* peek drawer header overlaps the topbar drag region \u2192 clear its buttons so they stay clickable */
17398
17561
  body.desktop-mac .peek-head { -webkit-app-region:drag; }
17399
- body.desktop-mac .peek-head .iconbtn, body.desktop-mac .peek-head .btn { -webkit-app-region:no-drag; }
17562
+ body.desktop-mac .peek-head .iconbtn, body.desktop-mac .peek-head .btn, body.desktop-mac .peek-head .hist-toggle { -webkit-app-region:no-drag; }
17400
17563
  /* sidebar collapsed \u2192 traffic lights sit over the main header; clear the hamburger */
17401
17564
  body.desktop-mac .sidebar.collapsed ~ .main .topbar { padding-left:80px; }
17402
17565
 
@@ -17561,8 +17724,7 @@ var styles_default = ` :root {
17561
17724
  color:var(--muted); border-radius:4px; opacity:0; transition:opacity .12s,background .12s; }
17562
17725
  .doc-th:hover .doc-col-menu { opacity:1; }
17563
17726
  .doc-col-menu:hover { background:var(--hover-2); color:var(--fg); }
17564
- .doc-col-resizer { position:absolute; top:0; right:-3px; width:7px; height:100%; cursor:col-resize; z-index:3; touch-action:none; }
17565
- .doc-col-resizer:hover, .doc-col-resizer.dragging { background:var(--accent); opacity:.35; }
17727
+ .doc-col-resizer { position:absolute; top:0; right:-4px; width:7px; height:100%; cursor:col-resize; z-index:3; touch-action:none; }
17566
17728
  /* per-row delete: small handle floating at the first cell on row hover */
17567
17729
  .doc-row-del { position:absolute; top:3px; left:2px; width:18px; height:18px; display:grid; place-items:center;
17568
17730
  color:var(--muted); background:var(--bg); border:1px solid var(--line); border-radius:5px; box-shadow:var(--shadow-sm);
@@ -17607,15 +17769,15 @@ var styles_default = ` :root {
17607
17769
  .colhead:hover { background:var(--hover); }
17608
17770
  .colhead .ti { color:var(--muted); } .colhead .ti svg { width:14px; height:14px; }
17609
17771
  .colhead .nm { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
17610
- .col-resizer { position:absolute; top:0; right:-3px; width:7px; height:100%; cursor:col-resize; z-index:2; touch-action:none; }
17611
- .col-resizer:hover, .col-resizer.dragging { background:var(--accent); opacity:.35; }
17772
+ .col-resizer { position:absolute; top:0; right:-4px; width:7px; height:100%; cursor:col-resize; z-index:2; touch-action:none; }
17612
17773
  .addcol { min-width:46px; } .addcol .colhead { justify-content:flex-start; color:var(--muted); }
17613
17774
  /* trailing filler column: empty canvas to the right of the data, not a real cell */
17614
17775
  table.grid td.filler { border-right:0; border-bottom:0; background:transparent; }
17615
17776
  body.col-resizing { user-select:none; cursor:col-resize; }
17616
17777
  table.grid .selcell { width:38px; min-width:38px; max-width:38px; text-align:center; }
17617
17778
  table.grid .gripcol, table.grid .rowgrip { width:26px; min-width:26px; max-width:26px; text-align:center; }
17618
- td.cell-td { vertical-align:top; }
17779
+ /* relative: anchors the .celledit overlay editor to the cell */
17780
+ td.cell-td { position:relative; vertical-align:top; }
17619
17781
  .cell { padding:8px 11px; min-height:37px; cursor:text; word-break:break-word; display:flex; flex-wrap:wrap; gap:3px; align-items:center; }
17620
17782
  /* hover/active tint goes on the <td> (always full row height) not the inner
17621
17783
  .cell (only as tall as its content), so a taller sibling row never leaves a
@@ -17667,6 +17829,14 @@ var styles_default = ` :root {
17667
17829
  .addrow:hover { background:var(--hover); color:var(--fg-soft); }
17668
17830
  .gridfoot { display:flex; gap:18px; color:var(--muted); font-size:12px; padding:8px 2px; }
17669
17831
  input.inlineedit { width:100%; border:0; outline:2px solid var(--accent); border-radius:4px; padding:7px 9px; background:var(--bg); color:var(--fg); font:inherit; }
17832
+ /* Overlay cell editor: floats over the td; the display content underneath
17833
+ stays in flow so the row height never changes while editing. Padding on the
17834
+ input matches .cell (8px 11px) so the text doesn't shift on entering edit. */
17835
+ .celledit { position:absolute; top:0; left:0; width:100%; min-height:100%; z-index:10;
17836
+ display:flex; align-items:stretch; background:var(--bg); border-radius:var(--radius-sm);
17837
+ box-shadow:0 0 0 2px var(--accent), var(--shadow-md); }
17838
+ .celledit input.inlineedit { width:100%; min-width:0; border:0; outline:0; border-radius:inherit;
17839
+ padding:8px 11px; background:transparent; }
17670
17840
 
17671
17841
  /* ---- sites (\u7AD9\u70B9\u7BA1\u7406) ---- */
17672
17842
  .sites-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(264px, 1fr)); gap:14px; margin-top:6px; }
@@ -17770,6 +17940,53 @@ var styles_default = ` :root {
17770
17940
  .proprow .v:hover { background:var(--hover); }
17771
17941
  .peek-divider { height:1px; background:var(--line); margin:22px 0 18px; }
17772
17942
 
17943
+ /* version history (doc drawer + record peek view) */
17944
+ .hist-peek { width:760px; }
17945
+ .hist-title { display:flex; align-items:center; gap:6px; font-weight:600; font-size:var(--fs-ui); }
17946
+ .hist-toggle { display:flex; align-items:center; gap:5px; font-size:var(--fs-sm); color:var(--muted); cursor:pointer; user-select:none; white-space:nowrap; }
17947
+ .hist-toggle input { accent-color:var(--accent); }
17948
+ .hist-split { display:flex; flex:1; min-height:0; }
17949
+ .hist-list { width:252px; flex:none; overflow:auto; border-right:1px solid var(--line); padding:8px; }
17950
+ .hist-item { padding:8px 10px; border-radius:var(--radius-sm); cursor:pointer; }
17951
+ .hist-item:hover { background:var(--hover); }
17952
+ .hist-item.sel { background:var(--accent-soft); }
17953
+ .hist-item.static { cursor:default; border-bottom:1px solid var(--line); border-radius:0; padding:10px 4px; }
17954
+ .hist-item.static:hover { background:none; }
17955
+ .hist-item .row1 { display:flex; align-items:center; gap:7px; }
17956
+ .hist-item .row1 .when { font-weight:600; font-size:var(--fs-ui); }
17957
+ .hist-item .row2 { display:flex; gap:8px; margin-top:2px; font-size:var(--fs-sm); color:var(--muted); min-width:0; }
17958
+ .hist-item .row2 .what { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
17959
+ .hist-kind { font-size:var(--fs-xs); padding:1px 6px; border-radius:99px; background:var(--surface-2); color:var(--muted); }
17960
+ .hist-kind.revert { background:var(--accent-soft); color:var(--accent); }
17961
+ .hist-kind.repair { background:var(--danger-soft); color:var(--danger); }
17962
+ .hist-now { font-size:var(--fs-xs); padding:1px 6px; border-radius:99px; background:var(--accent); color:var(--accent-fg); }
17963
+ .hist-preview { flex:1; overflow:auto; padding:20px 24px; }
17964
+ .hist-doc-title { font-size:21px; font-weight:700; letter-spacing:-.02em; margin-bottom:14px; }
17965
+ .hist-block { white-space:pre-wrap; font-size:var(--fs-body); line-height:1.65; padding:2px 6px; border-radius:4px; margin:0 -6px 12px; }
17966
+ /* line-level diff rows (git-like): pale full-line tint, darker tint on the
17967
+ intra-line changed segment; strikethrough only on the deleted segment. */
17968
+ .hist-line { white-space:pre-wrap; font-size:var(--fs-body); line-height:1.6; padding:1px 6px; margin:0 -6px; border-radius:3px; }
17969
+ .hist-line.add { background:color-mix(in srgb, var(--success) 13%, transparent); }
17970
+ .hist-line.del { background:var(--danger-soft); }
17971
+ .hist-line.add .seg { background:color-mix(in srgb, var(--success) 32%, transparent); border-radius:2px; }
17972
+ .hist-line.del .seg { background:color-mix(in srgb, var(--danger) 28%, transparent); border-radius:2px; text-decoration:line-through; }
17973
+ .hist-foot { display:flex; justify-content:flex-end; padding:12px 16px; border-top:1px solid var(--line); }
17974
+ .pad { padding:12px; }
17975
+ .hist-rec-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:10px; }
17976
+ .hist-feed { padding:12px 24px; }
17977
+ .hist-recname { font-weight:600; font-size:var(--fs-sm); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
17978
+ .hist-status { font-size:var(--fs-xs); color:var(--muted); flex:none; }
17979
+ .hist-field .old, .hist-field .new { max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
17980
+ .hist-more { border:0; background:none; color:var(--muted); font-size:var(--fs-sm); cursor:pointer; padding:2px 0; text-align:left; }
17981
+ .hist-more:hover { color:var(--fg); }
17982
+ .hist-fields { padding:8px 4px 4px; display:flex; flex-direction:column; gap:6px; }
17983
+ .hist-field { display:flex; gap:8px; font-size:var(--fs-sm); align-items:baseline; flex-wrap:wrap; }
17984
+ .hist-field .fname { color:var(--muted); width:120px; flex:none; }
17985
+ .hist-field .old { text-decoration:line-through; color:var(--muted); }
17986
+ .hist-field .arr { color:var(--muted); }
17987
+ .hist-field .new { font-weight:500; }
17988
+ .hist-restore { margin-top:8px; align-self:flex-start; }
17989
+
17773
17990
  /* menu / popover */
17774
17991
  .menu-layer { position:fixed; inset:0; z-index:90; }
17775
17992
  .pop { position:fixed; background:var(--bg); border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow-lg);
@@ -17898,12 +18115,51 @@ var styles_default = ` :root {
17898
18115
  .kg-particle { opacity:0 !important; }
17899
18116
  }
17900
18117
  .error-bar { margin:12px 24px; padding:10px 14px; background:var(--danger-soft); color:var(--danger); border-radius:var(--radius-sm); font-size:13px; cursor:pointer; }
18118
+ .offline-bar { margin:12px 24px 0; padding:8px 14px; background:var(--surface-2); color:var(--fg-soft); border-radius:var(--radius-sm); font-size:12.5px; }
17901
18119
  .search-hit { margin:0 0 8px; padding:10px 14px; background:var(--surface); border-radius:var(--radius-sm); font-size:13px; cursor:pointer; }
17902
18120
  .search-hit:hover { background:var(--surface-2); }
17903
18121
  .search-hit mark { background:transparent; color:var(--accent); font-weight:600; }
17904
18122
  .view-placeholder { padding:40px; text-align:center; border:1px dashed var(--line-strong); border-radius:var(--radius); color:var(--muted); margin-top:8px; }
17905
18123
  .ico.flip { transform:scaleX(-1); }
17906
18124
 
18125
+ /* ---- find in document (Ctrl-F) ---- */
18126
+ /* CSS Custom Highlight overlays \u2014 no DOM mutation, safe over contentEditable. */
18127
+ ::highlight(mh-find-all) { background:color-mix(in srgb, var(--accent) 26%, transparent); border-radius:2px; }
18128
+ ::highlight(mh-find-current) { background:var(--accent); color:var(--accent-fg); }
18129
+ .find-bar {
18130
+ position:fixed; top:58px; right:18px; z-index:40; /* below the ~50px topbar */
18131
+ display:flex; align-items:center; gap:4px;
18132
+ padding:5px 6px; background:var(--bg); border:1px solid var(--line-strong);
18133
+ border-radius:var(--radius); box-shadow:var(--shadow-lg);
18134
+ }
18135
+ .find-bar .find-ico { color:var(--muted); margin:0 2px; flex:none; }
18136
+ .find-bar input {
18137
+ width:200px; max-width:42vw; border:0; outline:0; background:transparent;
18138
+ color:var(--fg); font-size:13px; padding:3px 2px;
18139
+ }
18140
+ .find-bar input::placeholder { color:var(--muted); }
18141
+ .find-count { font-size:12px; color:var(--muted); min-width:48px; text-align:right; padding:0 2px; font-variant-numeric:tabular-nums; }
18142
+ .find-opt {
18143
+ border:1px solid var(--line-strong); background:transparent; color:var(--fg-soft);
18144
+ border-radius:var(--radius-sm); font-size:12px; line-height:1; padding:4px 6px; cursor:pointer;
18145
+ transition:background .12s, color .12s, border-color .12s;
18146
+ }
18147
+ .find-opt:hover { background:var(--surface-2); }
18148
+ .find-opt.on { background:var(--accent-soft); color:var(--accent); border-color:color-mix(in srgb, var(--accent) 45%, transparent); }
18149
+ .find-nav {
18150
+ display:grid; place-items:center; width:26px; height:26px; flex:none;
18151
+ border:0; background:transparent; color:var(--fg-soft); border-radius:var(--radius-sm); cursor:pointer;
18152
+ transition:background .12s, color .12s;
18153
+ }
18154
+ .find-nav:hover:not(:disabled) { background:var(--surface-2); color:var(--fg); }
18155
+ .find-nav:disabled { opacity:.4; cursor:default; }
18156
+ .find-nav .find-prev { transform:rotate(180deg); }
18157
+ .find-close { color:var(--muted); }
18158
+ @media (max-width:560px) {
18159
+ .find-bar { left:12px; right:12px; }
18160
+ .find-bar input { width:auto; flex:1; max-width:none; }
18161
+ }
18162
+
17907
18163
  /* ---- board (kanban) view ---- */
17908
18164
  .board-wrap { margin-top:4px; }
17909
18165
  .board-bar { display:flex; align-items:center; gap:10px; padding:2px 0 12px; }
@@ -17979,9 +18235,16 @@ var styles_default = ` :root {
17979
18235
  display:flex; align-items:center; gap:2px; padding:0 4px; cursor:grab; touch-action:none; box-shadow:var(--shadow-sm); overflow:hidden; }
17980
18236
  .tl-item .tl-label { flex:1; font-size:12px; font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; pointer-events:none; }
17981
18237
  .tl-row:hover .tl-item { box-shadow:0 0 0 1px var(--accent), var(--shadow-sm); }
17982
- .tl-item .tl-h { width:8px; align-self:stretch; flex:none; cursor:col-resize; border-radius:6px; opacity:0; transition:opacity .1s; }
17983
- .tl-item:hover .tl-h { opacity:1; background:rgba(255,255,255,.35); }
17984
- .tl-item .tl-h:hover { background:rgba(255,255,255,.6); }
18238
+ /* Edge handles: a small centered round-capped grip inside the colored bar.
18239
+ --accent-fg (not white) keeps contrast right in both themes \u2014 dark grip on
18240
+ the light-purple dark-mode bar, white grip on the blue light-mode bar. */
18241
+ .tl-item .tl-h { position:relative; width:9px; align-self:stretch; flex:none; cursor:col-resize; opacity:0; transition:opacity .15s; touch-action:none; }
18242
+ .tl-item .tl-h::before { content:""; position:absolute; left:50%; top:50%; width:3px; height:10px;
18243
+ transform:translate(-50%,-50%); border-radius:999px;
18244
+ background:color-mix(in srgb, var(--accent-fg) 70%, transparent);
18245
+ transition:height .15s cubic-bezier(.4,0,.2,1), background .15s; }
18246
+ .tl-item:hover .tl-h, .tl-item .tl-h.dragging { opacity:1; }
18247
+ .tl-item .tl-h:hover::before, .tl-item .tl-h.dragging::before { height:14px; background:var(--accent-fg); }
17985
18248
  .tl-milestone { position:absolute; top:9px; width:18px; height:18px; transform:rotate(45deg); background:var(--accent); border-radius:3px; cursor:grab; touch-action:none; box-shadow:var(--shadow-sm); margin-left:-9px; }
17986
18249
  .tl-milestone:hover { box-shadow:0 0 0 2px var(--accent-soft), var(--shadow-sm); }
17987
18250
  .tl-track-empty { position:absolute; top:0; right:0; height:100%; cursor:copy; }
@@ -18075,8 +18338,13 @@ var styles_default = ` :root {
18075
18338
  /* Anchor/TOC jumps: clear the mobile topbar (49px min + notch inset);
18076
18339
  the desktop 60px estimate leaves blocks half-hidden behind it. */
18077
18340
  .block { scroll-margin-top:calc(49px + env(safe-area-inset-top) + 8px); }
18341
+ /* Find bar clears the taller mobile topbar (49px min + notch inset). */
18342
+ .find-bar { top:calc(49px + env(safe-area-inset-top) + 8px); }
18078
18343
  .db { padding:16px 14px 90px; }
18079
18344
  .peek { width:100%; }
18345
+ /* history drawer: the side-by-side list/preview doesn't fit a phone \u2014 stack. */
18346
+ .hist-split { flex-direction:column; }
18347
+ .hist-list { width:auto; max-height:38%; border-right:0; border-bottom:1px solid var(--line); }
18080
18348
 
18081
18349
  /* Safe areas (viewport-fit=cover): keep chrome clear of the notch / home bar. */
18082
18350
  .sb-head { padding-top:calc(14px + env(safe-area-inset-top)); }
@@ -18126,7 +18394,7 @@ var styles_default = ` :root {
18126
18394
  table.doc-table .doc-row-del { opacity:1; }
18127
18395
  .selcell input { opacity:1; }
18128
18396
  .rowgrip { color:var(--muted); }
18129
- .tl-item .tl-h { opacity:1; background:rgba(255,255,255,.35); }
18397
+ .tl-item .tl-h { opacity:1; }
18130
18398
  /* Neutralize the stickiest tap-residue tints (a tap "hovers" until the
18131
18399
  next tap elsewhere); transient menus/buttons are left alone. */
18132
18400
  .navitem:hover { background:none; }
@@ -18142,15 +18410,20 @@ var styles_default = ` :root {
18142
18410
  macOS vibrancy blur show through; a translucent panel keeps text readable.
18143
18411
  The top bar is the window drag region; its buttons opt back out. */
18144
18412
  body.quicknote { background:transparent; }
18413
+ /* html carries an opaque bg for the iOS 26 Safari toolbar tint; the vibrancy
18414
+ window needs the root transparent again or the blur is walled off. */
18415
+ html:has(body.quicknote) { background:transparent; }
18145
18416
  /* The main app lays #app out as a flex row (sidebar + main); the quick-note
18146
18417
  route has a single child, so override to a full-bleed block or the panel
18147
18418
  shrinks to its content width and the window's vibrancy shows through. */
18148
18419
  body.quicknote #app { display:block; width:100vw; height:100vh; height:100dvh; }
18149
18420
  .qn { display:flex; flex-direction:column; width:100%; height:100vh; height:100dvh; overflow:hidden;
18150
18421
  background:color-mix(in srgb, var(--bg) 20%, transparent); }
18151
- .qn-bar { display:flex; align-items:center; gap:6px; padding:4px 10px 4px 76px;
18422
+ .qn-bar { display:flex; align-items:center; gap:6px; padding:4px 10px;
18152
18423
  user-select:none; -webkit-app-region:drag;
18153
18424
  background:color-mix(in srgb, var(--surface) 30%, transparent); }
18425
+ /* macOS only: clear the inset traffic lights; Win/Linux frameless has none */
18426
+ body.desktop-mac .qn-bar { padding-left:76px; }
18154
18427
  .qn-brand { flex:1; min-width:0; font-size:12px; font-weight:600; color:var(--fg-soft);
18155
18428
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
18156
18429
  .qn-actions { display:flex; gap:2px; -webkit-app-region:no-drag; }
@@ -18160,6 +18433,10 @@ var styles_default = ` :root {
18160
18433
  .qn-body .doc { max-width:none; margin:0; padding:14px 22px 30vh; }
18161
18434
  .qn-body .doc-title { font-size:24px; margin-bottom:2px; }
18162
18435
  .qn-body .doc-meta { display:none; }
18436
+ /* The narrow-width gutter hide is now gated on (pointer:coarse), which this
18437
+ desktop window never matches \u2014 re-hide explicitly: quick notes offer no
18438
+ block insert/drag handles (Enter / slash commands cover insertion). */
18439
+ body.quicknote .block .gutter { display:none; }
18163
18440
  /* A native checkbox paints an opaque white box when unchecked, which breaks
18164
18441
  the window's vibrancy blur. Draw a custom box so the unchecked background
18165
18442
  stays transparent; checked falls back to the accent fill + checkmark. */
@@ -18193,6 +18470,11 @@ var styles_default = ` :root {
18193
18470
  .qn-shortcut.capturing { outline:2px solid var(--accent); color:var(--accent); }
18194
18471
  `;
18195
18472
 
18473
+ // src/webui/server/icons.ts
18474
+ var ICON_192 = "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";
18475
+ var ICON_512 = "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";
18476
+ var ICON_180 = "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";
18477
+
18196
18478
  // src/webui/server/assets.ts
18197
18479
  var RUNNING_FROM_SOURCE = import.meta.url.includes("/src/webui/");
18198
18480
  var HTML = `<!doctype html>
@@ -18220,6 +18502,8 @@ var HTML = `<!doctype html>
18220
18502
  alone, so the dark palette exists exactly once \u2014 see styles.css. Dark also
18221
18503
  pre-tints the status bar (hex mirrors --bg) so the first frame isn't white. -->
18222
18504
  <script>try{var t=localStorage.getItem('mh-theme'),d=t==='dark'||(t!=='light'&&matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.dataset.resolved=d?'dark':'light';if(d)document.getElementById('theme-color-meta').content='#1a1a1c'}catch(e){}</script>
18505
+ <link rel="manifest" href="/manifest.webmanifest">
18506
+ <link rel="apple-touch-icon" href="/icons/icon-180.png">
18223
18507
  <link rel="stylesheet" href="/webui.css">
18224
18508
  </head>
18225
18509
  <body>
@@ -18227,12 +18511,23 @@ var HTML = `<!doctype html>
18227
18511
  <script type="module" src="/webui.js"></script>
18228
18512
  </body>
18229
18513
  </html>`;
18230
- var injectedJs = null;
18231
- function setWebuiBundle(js) {
18232
- injectedJs = js;
18514
+ var injected = {
18515
+ js: null,
18516
+ sw: null,
18517
+ dbWorker: null,
18518
+ runtime: null,
18519
+ sdk: null
18520
+ };
18521
+ var injectedWasm = null;
18522
+ function setWebuiBundle(bundle) {
18523
+ injected.js = bundle.js;
18524
+ injected.sw = bundle.sw;
18525
+ injected.dbWorker = bundle.dbWorker;
18526
+ injected.runtime = bundle.runtime;
18527
+ injected.sdk = bundle.sdk;
18528
+ const w = bundle.wasm;
18529
+ injectedWasm = w.byteOffset === 0 && w.byteLength === w.buffer.byteLength ? w.buffer : w.slice().buffer;
18233
18530
  }
18234
- var cachedJs = null;
18235
- var cachedJsMtime = 0;
18236
18531
  function newestSourceMtime(dir) {
18237
18532
  let newest = 0;
18238
18533
  for (const e of readdirSync(dir, { withFileTypes: true })) {
@@ -18246,37 +18541,83 @@ function newestSourceMtime(dir) {
18246
18541
  }
18247
18542
  return newest;
18248
18543
  }
18249
- async function getJs() {
18250
- if (injectedJs != null)
18251
- return injectedJs;
18252
- if (RUNNING_FROM_SOURCE) {
18253
- const srcDir = fileURLToPath(new URL("..", import.meta.url));
18254
- const newest = newestSourceMtime(srcDir);
18255
- if (cachedJs == null || newest > cachedJsMtime) {
18256
- const entry = fileURLToPath(new URL("../app.tsx", import.meta.url));
18257
- const res = await Bun.build({ entrypoints: [entry], target: "browser" });
18258
- if (!res.success)
18259
- throw new AggregateError(res.logs, "webui build failed");
18260
- cachedJs = await res.outputs[0].text();
18261
- cachedJsMtime = newest;
18262
- }
18263
- return cachedJs;
18264
- }
18265
- if (cachedJs == null) {
18266
- const prebuilt = Bun.file(fileURLToPath(new URL("./webui.js", import.meta.url)));
18267
- if (!await prebuilt.exists()) {
18268
- throw new Error("webui bundle missing: dist/webui.js was not built \u2014 run `bun run build`");
18269
- }
18270
- cachedJs = await prebuilt.text();
18271
- }
18272
- return cachedJs;
18273
- }
18544
+ function bundleGetter(opts) {
18545
+ let cached2 = null;
18546
+ let cachedMtime = 0;
18547
+ return async () => {
18548
+ const fromBinary = injected[opts.key];
18549
+ if (fromBinary != null)
18550
+ return fromBinary;
18551
+ if (RUNNING_FROM_SOURCE) {
18552
+ const srcDir = fileURLToPath(new URL("..", import.meta.url));
18553
+ let newest = newestSourceMtime(srcDir);
18554
+ for (const extra of opts.extraDirs ?? []) {
18555
+ newest = Math.max(newest, newestSourceMtime(fileURLToPath(new URL(extra, import.meta.url))));
18556
+ }
18557
+ if (cached2 == null || newest > cachedMtime) {
18558
+ const entry = fileURLToPath(new URL(opts.entry, import.meta.url));
18559
+ const res = await Bun.build({ entrypoints: [entry], target: "browser" });
18560
+ if (!res.success)
18561
+ throw new AggregateError(res.logs, `${opts.dist} build failed`);
18562
+ cached2 = await res.outputs[0].text();
18563
+ cachedMtime = newest;
18564
+ }
18565
+ return cached2;
18566
+ }
18567
+ if (cached2 == null) {
18568
+ const prebuilt = Bun.file(fileURLToPath(new URL(`./${opts.dist}`, import.meta.url)));
18569
+ if (!await prebuilt.exists()) {
18570
+ throw new Error(`${opts.dist} missing: run \`bun run build\``);
18571
+ }
18572
+ cached2 = await prebuilt.text();
18573
+ }
18574
+ return cached2;
18575
+ };
18576
+ }
18577
+ var getJs = bundleGetter({ key: "js", entry: "../app.tsx", dist: "webui.js" });
18578
+ var getSwRaw = bundleGetter({ key: "sw", entry: "../sw.ts", dist: "sw.js" });
18579
+ var getDbWorker = bundleGetter({ key: "dbWorker", entry: "../data/db-worker.ts", dist: "db-worker.js" });
18580
+ var getRuntime = bundleGetter({ key: "runtime", entry: "../runtime.ts", dist: "mh-runtime.js" });
18581
+ var getSdk = bundleGetter({ key: "sdk", entry: "../../sdk/client.ts", dist: "metahub-sdk.js", extraDirs: ["../../sdk"] });
18274
18582
  async function getCss() {
18275
18583
  if (RUNNING_FROM_SOURCE) {
18276
18584
  return Bun.file(fileURLToPath(new URL("../styles.css", import.meta.url))).text();
18277
18585
  }
18278
18586
  return styles_default;
18279
18587
  }
18588
+ async function getSw() {
18589
+ const [raw, js, css] = await Promise.all([getSwRaw(), getJs(), getCss()]);
18590
+ const version2 = new Bun.CryptoHasher("sha256").update(js).update(css).digest("hex").slice(0, 16);
18591
+ return raw.replaceAll("__MH_SW_VERSION__", version2);
18592
+ }
18593
+ async function getWasm() {
18594
+ if (injectedWasm != null)
18595
+ return injectedWasm;
18596
+ const path = RUNNING_FROM_SOURCE ? join3(dirname(Bun.resolveSync("@sqlite.org/sqlite-wasm", fileURLToPath(new URL(".", import.meta.url)))), "sqlite3.wasm") : fileURLToPath(new URL("./sqlite3.wasm", import.meta.url));
18597
+ const f = Bun.file(path);
18598
+ if (!await f.exists()) {
18599
+ throw new Error(`sqlite3.wasm missing at ${path} \u2014 run \`bun install\` / \`bun run build\``);
18600
+ }
18601
+ return f.arrayBuffer();
18602
+ }
18603
+ var MANIFEST = JSON.stringify({
18604
+ name: "Metahub",
18605
+ short_name: "Metahub",
18606
+ start_url: "/",
18607
+ scope: "/",
18608
+ display: "standalone",
18609
+ background_color: "#ffffff",
18610
+ theme_color: "#ffffff",
18611
+ icons: [
18612
+ { src: "/icons/icon-192.png", sizes: "192x192", type: "image/png" },
18613
+ { src: "/icons/icon-512.png", sizes: "512x512", type: "image/png" }
18614
+ ]
18615
+ });
18616
+ var ICONS = {
18617
+ "/icons/icon-192.png": ICON_192,
18618
+ "/icons/icon-512.png": ICON_512,
18619
+ "/icons/icon-180.png": ICON_180
18620
+ };
18280
18621
  async function warmWebui() {
18281
18622
  try {
18282
18623
  await getJs();
@@ -18295,11 +18636,37 @@ async function serveWebui(req) {
18295
18636
  return asset(HTML, "text/html; charset=utf-8");
18296
18637
  if (pathname === "/webui.css")
18297
18638
  return asset(await getCss(), "text/css; charset=utf-8");
18298
- if (pathname === "/webui.js") {
18639
+ if (pathname === "/manifest.webmanifest")
18640
+ return asset(MANIFEST, "application/manifest+json");
18641
+ const icon = ICONS[pathname];
18642
+ if (icon) {
18643
+ return new Response(Buffer.from(icon, "base64"), {
18644
+ headers: { "content-type": "image/png", "cache-control": "public, max-age=86400" }
18645
+ });
18646
+ }
18647
+ if (pathname === "/sqlite3.wasm") {
18299
18648
  try {
18300
- return asset(await getJs(), "text/javascript; charset=utf-8");
18649
+ return new Response(await getWasm(), {
18650
+ headers: { "content-type": "application/wasm", "cache-control": "public, max-age=86400" }
18651
+ });
18301
18652
  } catch (e) {
18302
- console.error("[webui] failed to serve /webui.js \u2014", e);
18653
+ console.error("[webui] failed to serve /sqlite3.wasm \u2014", e);
18654
+ return new Response(`sqlite3.wasm unavailable: ${e}`, { status: 500 });
18655
+ }
18656
+ }
18657
+ const script = {
18658
+ "/webui.js": getJs,
18659
+ "/sw.js": getSw,
18660
+ "/db-worker.js": getDbWorker,
18661
+ "/mh-runtime.js": getRuntime,
18662
+ "/metahub-sdk.js": getSdk
18663
+ };
18664
+ const getter = script[pathname];
18665
+ if (getter) {
18666
+ try {
18667
+ return asset(await getter(), "text/javascript; charset=utf-8");
18668
+ } catch (e) {
18669
+ console.error(`[webui] failed to serve ${pathname} \u2014`, e);
18303
18670
  return new Response(`webui build failed: ${e}`, { status: 500 });
18304
18671
  }
18305
18672
  }
@@ -18318,12 +18685,20 @@ function hlcMillis(hlc2) {
18318
18685
  function hlcIso(hlc2) {
18319
18686
  return new Date(hlcMillis(hlc2)).toISOString();
18320
18687
  }
18688
+ function revisionKind(txn) {
18689
+ if (txn?.startsWith("repair:"))
18690
+ return "repair";
18691
+ if (txn?.startsWith("revert:"))
18692
+ return "revert";
18693
+ return "user";
18694
+ }
18321
18695
  function clusterRevisions(changes) {
18322
18696
  const groups = [];
18323
18697
  let cur = [];
18324
18698
  for (const c of changes) {
18325
18699
  const prev = cur[cur.length - 1];
18326
- if (prev && (prev.node_id !== c.node_id || hlcMillis(c.hlc) - hlcMillis(prev.hlc) > REVISION_GAP_MS)) {
18700
+ const boundary = prev && (prev.txn !== null && c.txn !== null ? prev.txn !== c.txn : prev.node_id !== c.node_id || hlcMillis(c.hlc) - hlcMillis(prev.hlc) > REVISION_GAP_MS);
18701
+ if (boundary) {
18327
18702
  groups.push(cur);
18328
18703
  cur = [];
18329
18704
  }
@@ -18348,7 +18723,7 @@ function registersAt(changes, at) {
18348
18723
  return out;
18349
18724
  }
18350
18725
  function rowChanges(db2, dataset, rowId) {
18351
- return db2.query("SELECT hlc, node_id, dataset, row_id, col, value FROM crdt_changes WHERE dataset = ? AND row_id = ? ORDER BY hlc").all(dataset, rowId);
18726
+ return db2.query(`SELECT ${CHANGE_SELECT} FROM crdt_changes WHERE dataset = ? AND row_id = ? ORDER BY hlc`).all(dataset, rowId);
18352
18727
  }
18353
18728
  function everBlockIds(db2, docId) {
18354
18729
  return db2.query("SELECT DISTINCT row_id FROM crdt_changes WHERE dataset = 'doc_blocks' AND col = 'doc_id' AND value = ?").all(JSON.stringify(docId)).map((r) => r.row_id);
@@ -18357,7 +18732,7 @@ function docChanges(db2, docId) {
18357
18732
  const blocks2 = everBlockIds(db2, docId);
18358
18733
  const placeholders = blocks2.map(() => "?").join(",");
18359
18734
  const blockClause = blocks2.length ? ` OR (dataset = 'doc_blocks' AND row_id IN (${placeholders}))` : "";
18360
- return db2.query(`SELECT hlc, node_id, dataset, row_id, col, value FROM crdt_changes
18735
+ return db2.query(`SELECT ${CHANGE_SELECT} FROM crdt_changes
18361
18736
  WHERE (dataset = 'documents' AND row_id = ?)${blockClause}
18362
18737
  ORDER BY hlc`).all(docId, ...blocks2);
18363
18738
  }
@@ -18390,6 +18765,7 @@ function listDocumentRevisions(db2, id) {
18390
18765
  version: last.hlc,
18391
18766
  at: hlcIso(last.hlc),
18392
18767
  node_id: last.node_id,
18768
+ kind: revisionKind(last.txn),
18393
18769
  changes: group.length,
18394
18770
  created,
18395
18771
  deleted,
@@ -18456,15 +18832,20 @@ function documentAtVersion(db2, id, at) {
18456
18832
  version: changes[changes.length - 1].hlc
18457
18833
  };
18458
18834
  }
18459
- function revertDocument(db2, id, to, opts = {}) {
18460
- const cur = getDocument(db2, id);
18461
- if (!cur)
18835
+ var revertDocument = grouped(function revertDocument2(db2, id, to, opts = {}) {
18836
+ let cur = getDocument(db2, id);
18837
+ if (!cur && !db2.query("SELECT 1 AS x FROM documents WHERE id = ?").get(id))
18462
18838
  throw new MhError("not_found", `no such document: ${id}`);
18463
18839
  if (opts.ifMatch !== undefined && documentVersion(db2, id) !== opts.ifMatch)
18464
18840
  throw new MhError("stale", `stale: document changed since ${opts.ifMatch}; re-read first`);
18465
18841
  const past = documentAtVersion(db2, id, to);
18466
18842
  if (past.deleted)
18467
18843
  throw new MhError("invalid_input", "target version is a deleted state; use doc delete instead");
18844
+ const undeleted = !cur;
18845
+ if (!cur) {
18846
+ emit(db2, "documents", id, "__deleted", 0);
18847
+ cur = getDocument(db2, id);
18848
+ }
18468
18849
  const fields = {};
18469
18850
  if (past.title !== cur.title)
18470
18851
  fields.title = past.title;
@@ -18474,43 +18855,98 @@ function revertDocument(db2, id, to, opts = {}) {
18474
18855
  updateDocument(db2, id, fields);
18475
18856
  return {
18476
18857
  id,
18477
- changed: Object.keys(fields).length > 0,
18858
+ changed: Object.keys(fields).length > 0 || undeleted,
18478
18859
  restored: past.version,
18479
- version: documentVersion(db2, id)
18860
+ version: documentVersion(db2, id),
18861
+ undeleted
18480
18862
  };
18481
- }
18863
+ }, "revert");
18482
18864
  var RECORD_META2 = new Set(["database_id", "created_hlc", "order_key", "__deleted"]);
18865
+ function recordRevisionOf(group) {
18866
+ const fields = new Set;
18867
+ let created = false;
18868
+ let deleted = false;
18869
+ let moved = false;
18870
+ for (const c of group) {
18871
+ if (c.col === "created_hlc")
18872
+ created = true;
18873
+ else if (c.col === "__deleted")
18874
+ deleted = flagSet(c.value);
18875
+ else if (c.col === "order_key")
18876
+ moved = true;
18877
+ else if (!RECORD_META2.has(c.col))
18878
+ fields.add(c.col);
18879
+ }
18880
+ const last = group[group.length - 1];
18881
+ return {
18882
+ version: last.hlc,
18883
+ at: hlcIso(last.hlc),
18884
+ node_id: last.node_id,
18885
+ kind: revisionKind(last.txn),
18886
+ changes: group.length,
18887
+ created,
18888
+ deleted,
18889
+ fields: [...fields],
18890
+ moved
18891
+ };
18892
+ }
18483
18893
  function listRecordRevisions(db2, id) {
18484
18894
  const changes = rowChanges(db2, "records", id);
18485
18895
  if (!changes.length)
18486
18896
  throw new MhError("not_found", `no such record: ${id}`);
18487
- return clusterRevisions(changes).map((group) => {
18488
- const fields = new Set;
18489
- let created = false;
18490
- let deleted = false;
18491
- let moved = false;
18492
- for (const c of group) {
18493
- if (c.col === "created_hlc")
18494
- created = true;
18495
- else if (c.col === "__deleted")
18496
- deleted = flagSet(c.value);
18497
- else if (c.col === "order_key")
18498
- moved = true;
18499
- else if (!RECORD_META2.has(c.col))
18500
- fields.add(c.col);
18897
+ return clusterRevisions(changes).map(recordRevisionOf).reverse();
18898
+ }
18899
+ function listDatabaseActivity(db2, databaseId, opts = {}) {
18900
+ if (!db2.query("SELECT 1 AS x FROM databases WHERE id = ?").get(databaseId))
18901
+ throw new MhError("not_found", `no such database: ${databaseId}`);
18902
+ const titleProp = db2.query("SELECT id FROM properties WHERE database_id = ? AND type = 'text' AND __deleted = 0 ORDER BY position LIMIT 1").get(databaseId)?.id ?? null;
18903
+ const changes = db2.query(`SELECT ${CHANGE_SELECT} FROM crdt_changes
18904
+ WHERE dataset = 'records' AND row_id IN (SELECT id FROM records WHERE database_id = ?)
18905
+ ORDER BY row_id, hlc`).all(databaseId);
18906
+ const out = [];
18907
+ let start = 0;
18908
+ for (let i = 1;i <= changes.length; i++) {
18909
+ if (i !== changes.length && changes[i].row_id === changes[start].row_id)
18910
+ continue;
18911
+ const rowId = changes[start].row_id;
18912
+ const state = new Map;
18913
+ for (const g of clusterRevisions(changes.slice(start, i))) {
18914
+ const touched = new Set;
18915
+ for (const c of g)
18916
+ if (!RECORD_META2.has(c.col))
18917
+ touched.add(c.col);
18918
+ const before = new Map;
18919
+ for (const col of touched)
18920
+ if (state.has(col))
18921
+ before.set(col, state.get(col));
18922
+ for (const c of g) {
18923
+ if (RECORD_META2.has(c.col))
18924
+ continue;
18925
+ if (c.value === null)
18926
+ state.delete(c.col);
18927
+ else
18928
+ state.set(c.col, JSON.parse(c.value));
18929
+ }
18930
+ const diffs = [];
18931
+ for (const col of touched) {
18932
+ const fc = { prop: col };
18933
+ if (before.has(col))
18934
+ fc.before = before.get(col);
18935
+ if (state.has(col))
18936
+ fc.after = state.get(col);
18937
+ diffs.push(fc);
18938
+ }
18939
+ out.push({
18940
+ ...recordRevisionOf(g),
18941
+ record_id: rowId,
18942
+ record_title: titleProp && state.has(titleProp) ? String(state.get(titleProp) ?? "") : null,
18943
+ diffs
18944
+ });
18501
18945
  }
18502
- const last = group[group.length - 1];
18503
- return {
18504
- version: last.hlc,
18505
- at: hlcIso(last.hlc),
18506
- node_id: last.node_id,
18507
- changes: group.length,
18508
- created,
18509
- deleted,
18510
- fields: [...fields],
18511
- moved
18512
- };
18513
- }).reverse();
18946
+ start = i;
18947
+ }
18948
+ out.sort((a, b) => a.version < b.version ? 1 : -1);
18949
+ return out.slice(0, opts.limit ?? 100);
18514
18950
  }
18515
18951
  function recordAtVersion(db2, id, at) {
18516
18952
  if (!at)
@@ -18536,7 +18972,7 @@ function recordAtVersion(db2, id, at) {
18536
18972
  version: changes[changes.length - 1].hlc
18537
18973
  };
18538
18974
  }
18539
- function revertRecord(db2, id, to) {
18975
+ var revertRecord = grouped(function revertRecord2(db2, id, to) {
18540
18976
  const cur = db2.query("SELECT database_id, data, __deleted FROM records WHERE id = ?").get(id);
18541
18977
  if (!cur)
18542
18978
  throw new MhError("not_found", `no such record: ${id}`);
@@ -18561,9 +18997,140 @@ function revertRecord(db2, id, to) {
18561
18997
  if (undeleted)
18562
18998
  emit(db2, "records", id, "__deleted", 0);
18563
18999
  return { id, changed: fields.length > 0 || undeleted, fields, undeleted, restored: past.version };
19000
+ }, "revert");
19001
+ var PROP_DEF_COLS = ["name", "type", "config", "position"];
19002
+ function listPropertyRevisions(db2, id) {
19003
+ const changes = rowChanges(db2, "properties", id);
19004
+ if (!changes.length)
19005
+ throw new MhError("not_found", `no such property: ${id}`);
19006
+ const clearedByTxn = new Map(db2.query("SELECT txn, COUNT(*) AS n FROM crdt_changes WHERE dataset = 'records' AND col = ? AND txn IS NOT NULL GROUP BY txn").all(id).map((r) => [r.txn, r.n]));
19007
+ return clusterRevisions(changes).map((group) => {
19008
+ const fields = new Set;
19009
+ let created = false;
19010
+ let deleted = false;
19011
+ for (const c of group) {
19012
+ if (c.col === "database_id")
19013
+ created = true;
19014
+ else if (c.col === "__deleted")
19015
+ deleted = flagSet(c.value);
19016
+ else if (PROP_DEF_COLS.includes(c.col))
19017
+ fields.add(c.col);
19018
+ }
19019
+ const last = group[group.length - 1];
19020
+ return {
19021
+ version: last.hlc,
19022
+ at: hlcIso(last.hlc),
19023
+ node_id: last.node_id,
19024
+ kind: revisionKind(last.txn),
19025
+ changes: group.length,
19026
+ created,
19027
+ deleted,
19028
+ fields: [...fields],
19029
+ cells_cleared: last.txn ? clearedByTxn.get(last.txn) ?? 0 : 0
19030
+ };
19031
+ }).reverse();
19032
+ }
19033
+ function propertyAtVersion(db2, id, at) {
19034
+ if (!at)
19035
+ throw new MhError("invalid_input", "missing version cutoff");
19036
+ const changes = rowChanges(db2, "properties", id).filter((c) => c.hlc <= at);
19037
+ if (!changes.length)
19038
+ throw new MhError("not_found", `no version of property ${id} at or before ${at}`);
19039
+ const regs = registersAt(changes, at);
19040
+ const reg = (col) => {
19041
+ const r = regs.get(col);
19042
+ return r?.value == null ? undefined : JSON.parse(r.value);
19043
+ };
19044
+ return {
19045
+ id,
19046
+ database_id: reg("database_id") ?? null,
19047
+ name: reg("name") ?? "",
19048
+ type: reg("type") ?? "text",
19049
+ config: reg("config") ?? null,
19050
+ position: reg("position") ?? null,
19051
+ deleted: regs.get("__deleted") ? flagSet(regs.get("__deleted").value) : false,
19052
+ version: changes[changes.length - 1].hlc
19053
+ };
19054
+ }
19055
+ var revertProperty = grouped(function revertProperty2(db2, id, to) {
19056
+ const cur = db2.query("SELECT database_id, name, type, config, position, __deleted FROM properties WHERE id = ?").get(id);
19057
+ if (!cur)
19058
+ throw new MhError("not_found", `no such property: ${id}`);
19059
+ const past = propertyAtVersion(db2, id, to);
19060
+ if (past.deleted)
19061
+ throw new MhError("invalid_input", "target version is a deleted state; use prop remove instead");
19062
+ const curDef = {
19063
+ name: cur.name,
19064
+ type: cur.type,
19065
+ config: cur.config ? JSON.parse(cur.config) : null,
19066
+ position: cur.position
19067
+ };
19068
+ const pastDef = {
19069
+ name: past.name,
19070
+ type: past.type,
19071
+ config: past.config,
19072
+ position: past.position
19073
+ };
19074
+ const fields = [];
19075
+ for (const col of PROP_DEF_COLS) {
19076
+ if (JSON.stringify(pastDef[col] ?? null) !== JSON.stringify(curDef[col] ?? null)) {
19077
+ emit(db2, "properties", id, col, pastDef[col]);
19078
+ fields.push(col);
19079
+ }
19080
+ }
19081
+ const undeleted = cur.__deleted !== 0;
19082
+ if (undeleted)
19083
+ emit(db2, "properties", id, "__deleted", 0);
19084
+ const schemaTxns = new Set(db2.query("SELECT DISTINCT txn FROM crdt_changes WHERE dataset = 'properties' AND row_id = ? AND hlc > ? AND txn IS NOT NULL").all(id, to).map((r) => r.txn));
19085
+ const cellChanges = db2.query("SELECT row_id, hlc, value, txn FROM crdt_changes WHERE dataset = 'records' AND col = ? ORDER BY row_id, hlc").all(id);
19086
+ const isCascade = (txn) => txn !== null && (schemaTxns.has(txn) || txn.startsWith("repair:"));
19087
+ let restored = 0;
19088
+ let skipped = 0;
19089
+ let i = 0;
19090
+ while (i < cellChanges.length) {
19091
+ const rowId = cellChanges[i].row_id;
19092
+ let winner = cellChanges[i];
19093
+ let lastUser = null;
19094
+ for (;i < cellChanges.length && cellChanges[i].row_id === rowId; i++) {
19095
+ winner = cellChanges[i];
19096
+ if (!isCascade(winner.txn))
19097
+ lastUser = winner;
19098
+ }
19099
+ if (winner.hlc <= to)
19100
+ continue;
19101
+ if (!isCascade(winner.txn)) {
19102
+ skipped++;
19103
+ continue;
19104
+ }
19105
+ const target = lastUser ? lastUser.value : null;
19106
+ if (target === winner.value)
19107
+ continue;
19108
+ emit(db2, "records", rowId, id, target === null ? undefined : JSON.parse(target));
19109
+ restored++;
19110
+ }
19111
+ return {
19112
+ id,
19113
+ changed: fields.length > 0 || undeleted || restored > 0,
19114
+ fields,
19115
+ restored_cells: restored,
19116
+ skipped_cells: skipped,
19117
+ undeleted,
19118
+ restored: past.version
19119
+ };
19120
+ }, "revert");
19121
+ function recordFieldHistory(db2, id, propId) {
19122
+ const rows = db2.query("SELECT hlc, node_id, value FROM crdt_changes WHERE dataset = 'records' AND row_id = ? AND col = ? ORDER BY hlc DESC").all(id, propId);
19123
+ return rows.map((r) => ({
19124
+ version: r.hlc,
19125
+ at: hlcIso(r.hlc),
19126
+ node_id: r.node_id,
19127
+ value: r.value === null ? undefined : JSON.parse(r.value),
19128
+ cleared: r.value === null
19129
+ }));
18564
19130
  }
18565
19131
 
18566
19132
  // src/webui/server/routes.ts
19133
+ init_node();
18567
19134
  var DatabaseSchema = exports_external.object({
18568
19135
  id: exports_external.string(),
18569
19136
  name: exports_external.string(),
@@ -18581,7 +19148,8 @@ var PropertySchema = exports_external.object({
18581
19148
  var RecordSchema = exports_external.object({
18582
19149
  id: exports_external.string(),
18583
19150
  database_id: exports_external.string(),
18584
- values: exports_external.record(exports_external.string(), exports_external.any()).describe('Field values keyed by property name (e.g. {"Amount":35}). Reads nest cells here; writes take a flat {column: value} map.')
19151
+ values: exports_external.record(exports_external.string(), exports_external.any()).describe('Field values keyed by property name (e.g. {"Amount":35}). Reads nest cells here; writes take a flat {column: value} map. Lossy when two properties share a name \u2014 prefer `cells` then.'),
19152
+ cells: exports_external.record(exports_external.string(), exports_external.any()).describe("Field values keyed by property id \u2014 lossless under duplicate property names. Writes accept property ids as keys too.")
18585
19153
  });
18586
19154
  var MoveRecordReq = exports_external.object({
18587
19155
  target: exports_external.string(),
@@ -18616,6 +19184,10 @@ var UpdateDatabaseReq = exports_external.object({
18616
19184
  name: exports_external.string().optional(),
18617
19185
  icon: exports_external.string().nullable().optional()
18618
19186
  });
19187
+ var DuplicateDatabaseReq = exports_external.object({
19188
+ name: exports_external.string().optional().describe("Name for the copy (defaults to the source name)"),
19189
+ icon: exports_external.string().optional()
19190
+ });
18619
19191
  var CreatePropertyReq = exports_external.object({
18620
19192
  db: exports_external.string(),
18621
19193
  name: exports_external.string(),
@@ -18642,10 +19214,15 @@ var UpdateDocumentReq = exports_external.object({
18642
19214
  parent_id: exports_external.string().nullable().optional(),
18643
19215
  if_match: exports_external.string().optional()
18644
19216
  });
19217
+ var DuplicateDocumentReq = exports_external.object({
19218
+ title: exports_external.string().optional().describe("Title for the copy (defaults to the source title)"),
19219
+ parent_id: exports_external.string().nullable().optional()
19220
+ });
18645
19221
  var RevisionBase = exports_external.object({
18646
19222
  version: exports_external.string(),
18647
19223
  at: exports_external.string().describe("Wall-clock time of the revision (ISO 8601)"),
18648
19224
  node_id: exports_external.string(),
19225
+ kind: exports_external.enum(["user", "repair", "revert"]).describe("Source of the revision"),
18649
19226
  changes: exports_external.number(),
18650
19227
  created: exports_external.boolean(),
18651
19228
  deleted: exports_external.boolean()
@@ -18659,6 +19236,15 @@ var RecordRevisionSchema = RevisionBase.extend({
18659
19236
  fields: exports_external.array(exports_external.string()).describe("Property ids of the cells written"),
18660
19237
  moved: exports_external.boolean()
18661
19238
  });
19239
+ var DatabaseActivityEntrySchema = RecordRevisionSchema.extend({
19240
+ record_id: exports_external.string(),
19241
+ record_title: exports_external.string().nullable().describe("Title-property value as of this revision (deleted records keep their last title)"),
19242
+ diffs: exports_external.array(exports_external.object({
19243
+ prop: exports_external.string(),
19244
+ before: exports_external.any().optional(),
19245
+ after: exports_external.any().optional()
19246
+ })).describe("Value-level cell changes; a missing side means the cell did not exist")
19247
+ });
18662
19248
  var DocumentVersionStateSchema = exports_external.object({
18663
19249
  id: exports_external.string(),
18664
19250
  title: exports_external.string(),
@@ -18677,7 +19263,8 @@ var RevertDocResultSchema = exports_external.object({
18677
19263
  id: exports_external.string(),
18678
19264
  changed: exports_external.boolean(),
18679
19265
  restored: exports_external.string(),
18680
- version: exports_external.string()
19266
+ version: exports_external.string(),
19267
+ undeleted: exports_external.boolean()
18681
19268
  });
18682
19269
  var RevertRecordResultSchema = exports_external.object({
18683
19270
  id: exports_external.string(),
@@ -18686,6 +19273,41 @@ var RevertRecordResultSchema = exports_external.object({
18686
19273
  undeleted: exports_external.boolean(),
18687
19274
  restored: exports_external.string()
18688
19275
  });
19276
+ var PropertyRevisionSchema = RevisionBase.extend({
19277
+ fields: exports_external.array(exports_external.string()).describe("Definition columns touched (name/type/config/position)"),
19278
+ cells_cleared: exports_external.number()
19279
+ });
19280
+ var RevertPropertyReq = exports_external.object({
19281
+ to: exports_external.string().describe("Version token from /api/property/history")
19282
+ });
19283
+ var RevertPropertyResultSchema = exports_external.object({
19284
+ id: exports_external.string(),
19285
+ changed: exports_external.boolean(),
19286
+ fields: exports_external.array(exports_external.string()),
19287
+ restored_cells: exports_external.number(),
19288
+ skipped_cells: exports_external.number(),
19289
+ undeleted: exports_external.boolean(),
19290
+ restored: exports_external.string()
19291
+ });
19292
+ var RecordVersionStateSchema = exports_external.object({
19293
+ id: exports_external.string(),
19294
+ database_id: exports_external.string().nullable(),
19295
+ deleted: exports_external.boolean(),
19296
+ data: exports_external.record(exports_external.string(), exports_external.any()).describe("Cells at that version, keyed by property id"),
19297
+ version: exports_external.string()
19298
+ });
19299
+ var FieldHistoryEntrySchema = exports_external.object({
19300
+ version: exports_external.string(),
19301
+ at: exports_external.string(),
19302
+ node_id: exports_external.string(),
19303
+ value: exports_external.any().optional(),
19304
+ cleared: exports_external.boolean()
19305
+ });
19306
+ var NodeInfoSchema = exports_external.object({
19307
+ node_id: exports_external.string(),
19308
+ label: exports_external.string().nullable().describe("Peer label from pairing; null when unnamed"),
19309
+ self: exports_external.boolean().describe("True for the node serving this request")
19310
+ });
18689
19311
  function need3(req, key) {
18690
19312
  const v = new URL(req.url).searchParams.get(key);
18691
19313
  if (!v)
@@ -18745,6 +19367,17 @@ var webuiRoutes = [
18745
19367
  return updateDatabase(db2, need3(req, "id"), body);
18746
19368
  })
18747
19369
  },
19370
+ {
19371
+ method: "POST",
19372
+ path: "/api/database/duplicate",
19373
+ summary: "Copy a database whole \u2014 schema and all records. Query: ?id=<id>",
19374
+ request: DuplicateDatabaseReq,
19375
+ response: DatabaseSchema,
19376
+ handler: handle3(async (req, { db: db2 }) => {
19377
+ const body = await req.json().catch(() => ({}));
19378
+ return duplicateDatabase(db2, need3(req, "id"), body);
19379
+ })
19380
+ },
18748
19381
  {
18749
19382
  method: "DELETE",
18750
19383
  path: "/api/database",
@@ -18752,6 +19385,18 @@ var webuiRoutes = [
18752
19385
  response: OkSchema2,
18753
19386
  handler: handle3((req, { db: db2 }) => ({ ok: deleteDatabase(db2, need3(req, "id")) }))
18754
19387
  },
19388
+ {
19389
+ method: "GET",
19390
+ path: "/api/database/activity",
19391
+ summary: "Recent changes across all records of a database, newest first. Query: ?db=<id>&limit=<n>",
19392
+ response: exports_external.array(DatabaseActivityEntrySchema),
19393
+ handler: handle3((req, { db: db2 }) => {
19394
+ const limit = opt(req, "limit");
19395
+ return listDatabaseActivity(db2, need3(req, "db"), {
19396
+ limit: limit ? Number(limit) : undefined
19397
+ });
19398
+ })
19399
+ },
18755
19400
  {
18756
19401
  method: "GET",
18757
19402
  path: "/api/properties",
@@ -18799,6 +19444,24 @@ var webuiRoutes = [
18799
19444
  response: OkSchema2,
18800
19445
  handler: handle3((req, { db: db2 }) => ({ ok: removeProperty(db2, need3(req, "id")) }))
18801
19446
  },
19447
+ {
19448
+ method: "GET",
19449
+ path: "/api/property/history",
19450
+ summary: "List a property's definition revisions, newest first. Query: ?id=<id>",
19451
+ response: exports_external.array(PropertyRevisionSchema),
19452
+ handler: handle3((req, { db: db2 }) => listPropertyRevisions(db2, need3(req, "id")))
19453
+ },
19454
+ {
19455
+ method: "POST",
19456
+ path: "/api/property/revert",
19457
+ summary: "Schema rollback: restore a property's definition and the cells its type-change/removal cleared (user edits since are kept). Query: ?id=<id>",
19458
+ request: RevertPropertyReq,
19459
+ response: RevertPropertyResultSchema,
19460
+ handler: handle3(async (req, { db: db2 }) => {
19461
+ const body = await req.json();
19462
+ return revertProperty(db2, need3(req, "id"), body.to);
19463
+ })
19464
+ },
18802
19465
  {
18803
19466
  method: "GET",
18804
19467
  path: "/api/records",
@@ -18865,6 +19528,20 @@ var webuiRoutes = [
18865
19528
  response: exports_external.array(RecordRevisionSchema),
18866
19529
  handler: handle3((req, { db: db2 }) => listRecordRevisions(db2, need3(req, "id")))
18867
19530
  },
19531
+ {
19532
+ method: "GET",
19533
+ path: "/api/record/at",
19534
+ summary: "A record's cells as of a past version. Query: ?id=<id>&version=<token>",
19535
+ response: RecordVersionStateSchema,
19536
+ handler: handle3((req, { db: db2 }) => recordAtVersion(db2, need3(req, "id"), need3(req, "version")))
19537
+ },
19538
+ {
19539
+ method: "GET",
19540
+ path: "/api/record/field-history",
19541
+ summary: "The write trail of one record cell, newest first. Query: ?id=<id>&prop=<propertyId>",
19542
+ response: exports_external.array(FieldHistoryEntrySchema),
19543
+ handler: handle3((req, { db: db2 }) => recordFieldHistory(db2, need3(req, "id"), need3(req, "prop")))
19544
+ },
18868
19545
  {
18869
19546
  method: "POST",
18870
19547
  path: "/api/record/revert",
@@ -18927,6 +19604,21 @@ var webuiRoutes = [
18927
19604
  return { ...doc2, version: documentVersion(db2, doc2.id) };
18928
19605
  })
18929
19606
  },
19607
+ {
19608
+ method: "POST",
19609
+ path: "/api/document/duplicate",
19610
+ summary: "Copy a document \u2014 title and every block \u2014 next to the source. Query: ?id=<id>",
19611
+ request: DuplicateDocumentReq,
19612
+ response: DocumentSchema,
19613
+ handler: handle3(async (req, { db: db2 }) => {
19614
+ const body = await req.json().catch(() => ({}));
19615
+ const doc2 = duplicateDocument(db2, need3(req, "id"), {
19616
+ title: body.title,
19617
+ parentId: body.parent_id
19618
+ });
19619
+ return { ...doc2, version: documentVersion(db2, doc2.id) };
19620
+ })
19621
+ },
18930
19622
  {
18931
19623
  method: "PATCH",
18932
19624
  path: "/api/document/move",
@@ -18970,6 +19662,20 @@ var webuiRoutes = [
18970
19662
  response: OkSchema2,
18971
19663
  handler: handle3((req, { db: db2 }) => ({ ok: deleteDocument(db2, need3(req, "id")) }))
18972
19664
  },
19665
+ {
19666
+ method: "GET",
19667
+ path: "/api/nodes",
19668
+ summary: "Known nodes for display: this device plus paired peers with their labels",
19669
+ response: exports_external.array(NodeInfoSchema),
19670
+ handler: handle3((_req, { db: db2 }) => {
19671
+ const self = getNodeId(db2);
19672
+ const peers = db2.query("SELECT node_id, label FROM peers WHERE node_id IS NOT NULL AND node_id <> '' GROUP BY node_id").all();
19673
+ return [
19674
+ { node_id: self, label: null, self: true },
19675
+ ...peers.filter((p) => p.node_id !== self).map((p) => ({ ...p, self: false }))
19676
+ ];
19677
+ })
19678
+ },
18973
19679
  {
18974
19680
  method: "GET",
18975
19681
  path: "/api/search",
@@ -18982,6 +19688,7 @@ var webuiRoutes = [
18982
19688
  }
18983
19689
  ];
18984
19690
  export {
19691
+ withChangeGroup,
18985
19692
  webuiRoutes,
18986
19693
  warmWebui,
18987
19694
  updateSite,
@@ -19022,6 +19729,7 @@ export {
19022
19729
  moveDocument,
19023
19730
  migrateRecords,
19024
19731
  migratePeers,
19732
+ migrateOplog,
19025
19733
  migrateDocuments,
19026
19734
  migrateDocBlocks,
19027
19735
  metahubHome,
@@ -19035,15 +19743,18 @@ export {
19035
19743
  listDatabases,
19036
19744
  keysBetween,
19037
19745
  keyBetween,
19746
+ initSchema,
19038
19747
  ingest,
19039
19748
  inferContentType,
19040
19749
  idKind,
19041
19750
  hasIndex,
19751
+ grouped,
19042
19752
  getSiteByName,
19043
19753
  getSite,
19044
19754
  getRecord,
19045
19755
  getProperty,
19046
19756
  getNodeId,
19757
+ getFileRow,
19047
19758
  getFileForServe,
19048
19759
  getDocument,
19049
19760
  getDatabase,
@@ -19057,6 +19768,8 @@ export {
19057
19768
  emitFields,
19058
19769
  emit,
19059
19770
  editDocument,
19771
+ duplicateDocument,
19772
+ duplicateDatabase,
19060
19773
  documentVersion,
19061
19774
  deleteSite,
19062
19775
  deleteRecord,
@@ -19081,16 +19794,24 @@ export {
19081
19794
  SyncResponseSchema,
19082
19795
  SyncRequestSchema,
19083
19796
  SYNC_PATH,
19797
+ SITE_FILE_SELECT,
19798
+ SITE_FILE_COLS,
19799
+ SITE_COLS,
19084
19800
  RENEW_PATH,
19085
19801
  PortInUseError,
19086
19802
  PairResponseSchema,
19087
19803
  PairRequestSchema,
19088
19804
  PROP_TYPES,
19805
+ PROPERTY_COLS,
19089
19806
  PAIR_PATH,
19090
19807
  MhError,
19091
19808
  HealthResponseSchema,
19092
19809
  HEALTH_PATH,
19093
- ChangeSchema
19810
+ DOCUMENT_COLS,
19811
+ DATABASE_COLS,
19812
+ ChangeSchema,
19813
+ CHANGE_SELECT,
19814
+ CHANGE_COLS
19094
19815
  };
19095
19816
 
19096
- //# debugId=3270CE1250D22E6A64756E2164756E21
19817
+ //# debugId=7F98FB2AC8CA296264756E2164756E21