@tensorix/metahub 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/README.zh-CN.md +9 -5
- package/dist/cli.js +5539 -4193
- package/dist/cli.js.map +68 -61
- package/dist/core/audit.d.ts.map +1 -1
- package/dist/core/blobs-core.d.ts.map +1 -1
- package/dist/core/compact.d.ts.map +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/crdt.d.ts +12 -4
- package/dist/core/crdt.d.ts.map +1 -1
- package/dist/core/databases.d.ts +3 -2
- package/dist/core/databases.d.ts.map +1 -1
- package/dist/core/db.d.ts +1 -1
- package/dist/core/db.d.ts.map +1 -1
- package/dist/core/documents.d.ts.map +1 -1
- package/dist/core/driver.d.ts +5 -1
- package/dist/core/driver.d.ts.map +1 -1
- package/dist/core/errors.d.ts +19 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/guest-intent.d.ts +1 -1
- package/dist/core/guest-intent.d.ts.map +1 -1
- package/dist/core/history-cache.d.ts +13 -0
- package/dist/core/history-cache.d.ts.map +1 -0
- package/dist/core/history.d.ts.map +1 -1
- package/dist/core/hlc.d.ts +13 -0
- package/dist/core/hlc.d.ts.map +1 -1
- package/dist/core/integrity.d.ts +19 -2
- package/dist/core/integrity.d.ts.map +1 -1
- package/dist/core/node.d.ts +52 -6
- package/dist/core/node.d.ts.map +1 -1
- package/dist/core/resolve.d.ts +8 -0
- package/dist/core/resolve.d.ts.map +1 -1
- package/dist/core/schema-init.d.ts +4 -1
- package/dist/core/schema-init.d.ts.map +1 -1
- package/dist/core/schema.d.ts +1 -1
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/search.d.ts +1 -1
- package/dist/core/search.d.ts.map +1 -1
- package/dist/core/site-channel-store.d.ts +1 -1
- package/dist/core/site-channel-store.d.ts.map +1 -1
- package/dist/core/snapshot.d.ts.map +1 -1
- package/dist/core/sync/auth.d.ts +2 -4
- package/dist/core/sync/auth.d.ts.map +1 -1
- package/dist/core/sync/cf-oauth.d.ts.map +1 -1
- package/dist/core/sync/client.d.ts +4 -0
- package/dist/core/sync/client.d.ts.map +1 -1
- package/dist/core/sync/devices.d.ts +6 -0
- package/dist/core/sync/devices.d.ts.map +1 -1
- package/dist/core/sync/drop-protocol.d.ts.map +1 -1
- package/dist/core/sync/drop-pull.d.ts +1 -1
- package/dist/core/sync/drop-pull.d.ts.map +1 -1
- package/dist/core/sync/http-status.d.ts.map +1 -1
- package/dist/core/sync/locale.d.ts +8 -0
- package/dist/core/sync/locale.d.ts.map +1 -0
- package/dist/core/sync/page-messages.d.ts +106 -0
- package/dist/core/sync/page-messages.d.ts.map +1 -0
- package/dist/core/sync/pairing.d.ts +4 -1
- package/dist/core/sync/pairing.d.ts.map +1 -1
- package/dist/core/sync/peers-routes.d.ts.map +1 -1
- package/dist/core/sync/peers.d.ts +3 -1
- package/dist/core/sync/peers.d.ts.map +1 -1
- package/dist/core/sync/protocol.d.ts +1 -0
- package/dist/core/sync/protocol.d.ts.map +1 -1
- package/dist/core/sync/remote-shares-cache.d.ts +32 -0
- package/dist/core/sync/remote-shares-cache.d.ts.map +1 -0
- package/dist/core/sync/room-client.d.ts +1 -1
- package/dist/core/sync/room-client.d.ts.map +1 -1
- package/dist/core/sync/room-protocol.d.ts +1 -1
- package/dist/core/sync/room-protocol.d.ts.map +1 -1
- package/dist/core/sync/room-serve.d.ts.map +1 -1
- package/dist/core/sync/routes.d.ts +1 -1
- package/dist/core/sync/routes.d.ts.map +1 -1
- package/dist/core/sync/server.d.ts +2 -0
- package/dist/core/sync/server.d.ts.map +1 -1
- package/dist/core/sync/share-actions.d.ts +7 -2
- package/dist/core/sync/share-actions.d.ts.map +1 -1
- package/dist/core/sync/share-copy.d.ts +7 -0
- package/dist/core/sync/share-copy.d.ts.map +1 -0
- package/dist/core/sync/share-export.d.ts +3 -1
- package/dist/core/sync/share-export.d.ts.map +1 -1
- package/dist/core/sync/share-serve.d.ts.map +1 -1
- package/dist/core/sync/sites-serve.d.ts.map +1 -1
- package/dist/core/sync/storage.d.ts +5 -1
- package/dist/core/sync/storage.d.ts.map +1 -1
- package/dist/core/tables.d.ts +16 -0
- package/dist/core/tables.d.ts.map +1 -0
- package/dist/db-worker.js +131 -88
- package/dist/edge-worker.js +353 -90
- package/dist/index.js +2051 -471
- package/dist/index.js.map +57 -49
- package/dist/metahub-sdk.js +5 -5
- package/dist/mh-runtime.js +9 -1
- package/dist/sw.js +15 -7
- package/dist/webui/server/assets.d.ts.map +1 -1
- package/dist/webui/server/changes-route.d.ts +2 -0
- package/dist/webui/server/changes-route.d.ts.map +1 -1
- package/dist/webui/server/edge-routes.d.ts.map +1 -1
- package/dist/webui/server/routes.d.ts +2 -0
- package/dist/webui/server/routes.d.ts.map +1 -1
- package/dist/webui-fmt.js +1 -1
- package/dist/webui.js +173 -214
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12,19 +12,43 @@ var __esm = (fn, res, err) => () => {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
// src/core/errors.ts
|
|
15
|
+
function interpolateMsg(s, params) {
|
|
16
|
+
return params ? s.replace(/\{(\w+)\}/g, (m, k) => (k in params) ? String(params[k]) : m) : s;
|
|
17
|
+
}
|
|
18
|
+
function mhError(code, key, params) {
|
|
19
|
+
return new MhError(code, interpolateMsg(key, params), { key, params });
|
|
20
|
+
}
|
|
21
|
+
function isSqliteBusy(e) {
|
|
22
|
+
if (!(e instanceof Error))
|
|
23
|
+
return false;
|
|
24
|
+
const code = e.code;
|
|
25
|
+
if (typeof code === "string" && SQLITE_BUSY_RE.test(code))
|
|
26
|
+
return true;
|
|
27
|
+
return e.message === "database is locked";
|
|
28
|
+
}
|
|
29
|
+
function asMhError(e) {
|
|
30
|
+
if (e instanceof MhError)
|
|
31
|
+
return e;
|
|
32
|
+
if (isSqliteBusy(e))
|
|
33
|
+
return mhError("busy", "\u6570\u636E\u5E93\u6B63\u88AB\u53E6\u4E00\u4E2A\u8FDB\u7A0B\u5199\u5165\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
15
36
|
function errorCode(e) {
|
|
16
|
-
return e
|
|
37
|
+
return asMhError(e)?.code;
|
|
17
38
|
}
|
|
18
|
-
var MhError;
|
|
39
|
+
var MhError, SQLITE_BUSY_RE;
|
|
19
40
|
var init_errors = __esm(() => {
|
|
20
41
|
MhError = class MhError extends Error {
|
|
21
42
|
code;
|
|
22
|
-
|
|
43
|
+
i18n;
|
|
44
|
+
constructor(code, message, i18n) {
|
|
23
45
|
super(message);
|
|
24
46
|
this.code = code;
|
|
47
|
+
this.i18n = i18n;
|
|
25
48
|
this.name = "MhError";
|
|
26
49
|
}
|
|
27
50
|
};
|
|
51
|
+
SQLITE_BUSY_RE = /^SQLITE_(BUSY|LOCKED)/;
|
|
28
52
|
});
|
|
29
53
|
|
|
30
54
|
// src/core/paths.ts
|
|
@@ -41,6 +65,12 @@ function cacheDir() {
|
|
|
41
65
|
}
|
|
42
66
|
var init_paths = () => {};
|
|
43
67
|
|
|
68
|
+
// src/core/driver.ts
|
|
69
|
+
function writeTx(db, fn) {
|
|
70
|
+
const tx = db.transaction(fn);
|
|
71
|
+
return (tx.immediate ?? tx)();
|
|
72
|
+
}
|
|
73
|
+
|
|
44
74
|
// src/core/schema.ts
|
|
45
75
|
var CORE_SCHEMA = `
|
|
46
76
|
CREATE TABLE IF NOT EXISTS meta (
|
|
@@ -352,6 +382,22 @@ CREATE TABLE IF NOT EXISTS blob_policy (
|
|
|
352
382
|
redundancy TEXT, -- 'all' | 'any'
|
|
353
383
|
__deleted INTEGER NOT NULL DEFAULT 0
|
|
354
384
|
);
|
|
385
|
+
|
|
386
|
+
-- Synced device roster (system tier, see tables.ts): one row per node id, written
|
|
387
|
+
-- by the device itself on open (describeSelf) and by anyone who renames it.
|
|
388
|
+
-- Replicates so every device shows the same name and icon for a peer \u2014 the only
|
|
389
|
+
-- record a purely bucket-joined device leaves besides its oplog stream. Each
|
|
390
|
+
-- column is its own LWW register, so a rename and a platform refresh never
|
|
391
|
+
-- clobber each other. meta.node_label survives as a node-local read fallback.
|
|
392
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
393
|
+
id TEXT PRIMARY KEY, -- node_id
|
|
394
|
+
label TEXT, -- shared display name; any device may set it
|
|
395
|
+
platform TEXT, -- macos | windows | linux | ios | android | web
|
|
396
|
+
form TEXT, -- laptop | desktop | phone | server | browser
|
|
397
|
+
app TEXT, -- cli | server | desktop | web
|
|
398
|
+
first_seen INTEGER, -- epoch ms of the device's first self-description
|
|
399
|
+
__deleted INTEGER NOT NULL DEFAULT 0
|
|
400
|
+
);
|
|
355
401
|
`, FTS_SCHEMA = `
|
|
356
402
|
CREATE VIRTUAL TABLE IF NOT EXISTS search_fts USING fts5(
|
|
357
403
|
kind UNINDEXED,
|
|
@@ -410,30 +456,142 @@ function getNodeId(db) {
|
|
|
410
456
|
db.query("INSERT INTO meta (key, value) VALUES ('node_id', ?)").run(id);
|
|
411
457
|
return id;
|
|
412
458
|
}
|
|
413
|
-
function
|
|
459
|
+
function readNodeMeta(db, nodeId) {
|
|
460
|
+
const r = db.query(`SELECT ${NODE_COLS} FROM nodes WHERE id = ? AND __deleted = 0`).get(nodeId);
|
|
461
|
+
return r ? rowToMeta(r) : null;
|
|
462
|
+
}
|
|
463
|
+
function listNodeMeta(db) {
|
|
464
|
+
return db.query(`SELECT ${NODE_COLS} FROM nodes WHERE __deleted = 0`).all().map(rowToMeta);
|
|
465
|
+
}
|
|
466
|
+
function legacySelfLabel(db) {
|
|
414
467
|
const row = db.query("SELECT value FROM meta WHERE key = 'node_label'").get();
|
|
415
468
|
return row?.value ?? null;
|
|
416
469
|
}
|
|
417
|
-
function
|
|
418
|
-
if (label == null
|
|
470
|
+
function mirrorLegacySelfLabel(db, label) {
|
|
471
|
+
if (label == null) {
|
|
419
472
|
db.query("DELETE FROM meta WHERE key = 'node_label'").run();
|
|
420
473
|
return;
|
|
421
474
|
}
|
|
422
|
-
db.query("INSERT INTO meta (key, value) VALUES ('node_label', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(label
|
|
475
|
+
db.query("INSERT INTO meta (key, value) VALUES ('node_label', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(label);
|
|
476
|
+
}
|
|
477
|
+
function nodeLabelOf(db, nodeId) {
|
|
478
|
+
const synced = readNodeMeta(db, nodeId)?.label;
|
|
479
|
+
if (synced)
|
|
480
|
+
return synced;
|
|
481
|
+
const peer = db.query("SELECT label FROM peers WHERE node_id = ? AND label IS NOT NULL AND label <> '' LIMIT 1").get(nodeId);
|
|
482
|
+
if (peer?.label)
|
|
483
|
+
return peer.label;
|
|
484
|
+
return nodeId === getNodeId(db) ? legacySelfLabel(db) : null;
|
|
485
|
+
}
|
|
486
|
+
function getNodeLabel(db) {
|
|
487
|
+
return nodeLabelOf(db, getNodeId(db));
|
|
488
|
+
}
|
|
489
|
+
function setNodeLabel(db, label, nodeId) {
|
|
490
|
+
const id = nodeId ?? getNodeId(db);
|
|
491
|
+
const next = label == null || label.trim() === "" ? null : label.trim();
|
|
492
|
+
const cur = readNodeMeta(db, id);
|
|
493
|
+
if (id === getNodeId(db))
|
|
494
|
+
mirrorLegacySelfLabel(db, next);
|
|
495
|
+
if (cur && cur.label === next)
|
|
496
|
+
return;
|
|
497
|
+
if (!cur && next == null)
|
|
498
|
+
return;
|
|
499
|
+
withChangeGroup(db, "node", () => {
|
|
500
|
+
emit(db, "nodes", id, "label", next);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
function describeSelf(db, info = {}) {
|
|
504
|
+
const id = getNodeId(db);
|
|
505
|
+
const cur = readNodeMeta(db, id);
|
|
506
|
+
const fields = {};
|
|
507
|
+
if (info.platform !== undefined && (cur?.platform ?? null) !== info.platform)
|
|
508
|
+
fields.platform = info.platform;
|
|
509
|
+
if (info.form !== undefined && (cur?.form ?? null) !== info.form)
|
|
510
|
+
fields.form = info.form;
|
|
511
|
+
if (info.app !== undefined && (cur?.app ?? null) !== info.app)
|
|
512
|
+
fields.app = info.app;
|
|
513
|
+
if (cur?.first_seen == null)
|
|
514
|
+
fields.first_seen = info.now ?? Date.now();
|
|
515
|
+
if (!cur?.label) {
|
|
516
|
+
const seed = legacySelfLabel(db) ?? info.defaultLabel ?? null;
|
|
517
|
+
if (seed)
|
|
518
|
+
fields.label = seed;
|
|
519
|
+
}
|
|
520
|
+
if (Object.keys(fields).length > 0) {
|
|
521
|
+
withChangeGroup(db, "node", () => {
|
|
522
|
+
for (const [col, v] of Object.entries(fields))
|
|
523
|
+
emit(db, "nodes", id, col, v);
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return readNodeMeta(db, id);
|
|
527
|
+
}
|
|
528
|
+
function platformFromProcess(p) {
|
|
529
|
+
switch (p) {
|
|
530
|
+
case "darwin":
|
|
531
|
+
return "macos";
|
|
532
|
+
case "win32":
|
|
533
|
+
return "windows";
|
|
534
|
+
case "linux":
|
|
535
|
+
return "linux";
|
|
536
|
+
case "android":
|
|
537
|
+
return "android";
|
|
538
|
+
default:
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function defaultForm(platform, app) {
|
|
543
|
+
if (app === "web")
|
|
544
|
+
return "browser";
|
|
545
|
+
if (platform === "ios" || platform === "android")
|
|
546
|
+
return "phone";
|
|
547
|
+
if (platform === "linux")
|
|
548
|
+
return "server";
|
|
549
|
+
if (platform === "macos" || platform === "windows")
|
|
550
|
+
return "laptop";
|
|
551
|
+
return null;
|
|
423
552
|
}
|
|
424
553
|
function displayNodes(db) {
|
|
425
554
|
const self = getNodeId(db);
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
555
|
+
const ids = new Set([self]);
|
|
556
|
+
for (const r of listNodeMeta(db))
|
|
557
|
+
ids.add(r.node_id);
|
|
558
|
+
const peers = db.query("SELECT node_id FROM peers WHERE node_id IS NOT NULL AND node_id <> '' GROUP BY node_id").all();
|
|
559
|
+
for (const p of peers)
|
|
560
|
+
ids.add(p.node_id);
|
|
561
|
+
const out = [...ids].map((node_id) => {
|
|
562
|
+
const m = readNodeMeta(db, node_id);
|
|
563
|
+
return {
|
|
564
|
+
node_id,
|
|
565
|
+
label: nodeLabelOf(db, node_id),
|
|
566
|
+
self: node_id === self,
|
|
567
|
+
platform: m?.platform ?? null,
|
|
568
|
+
form: m?.form ?? null,
|
|
569
|
+
app: m?.app ?? null
|
|
570
|
+
};
|
|
571
|
+
});
|
|
572
|
+
out.sort((a, b) => Number(b.self) - Number(a.self));
|
|
573
|
+
return out;
|
|
431
574
|
}
|
|
575
|
+
var NODE_COLS = "id, label, platform, form, app, first_seen", rowToMeta = (r) => ({
|
|
576
|
+
node_id: r.id,
|
|
577
|
+
label: r.label,
|
|
578
|
+
platform: r.platform ?? null,
|
|
579
|
+
form: r.form ?? null,
|
|
580
|
+
app: r.app ?? null,
|
|
581
|
+
first_seen: r.first_seen
|
|
582
|
+
});
|
|
432
583
|
var init_node = __esm(() => {
|
|
433
584
|
init_ids();
|
|
585
|
+
init_crdt();
|
|
434
586
|
});
|
|
435
587
|
|
|
436
588
|
// src/core/hlc.ts
|
|
589
|
+
function hlcSkewBound(now) {
|
|
590
|
+
return formatHlc({ millis: now + HLC_MAX_SKEW_MS, counter: MAX_HLC_COUNTER, node: "~~~~~~~~" });
|
|
591
|
+
}
|
|
592
|
+
function isSkewedHlc(hlc, now) {
|
|
593
|
+
return hlc > hlcSkewBound(now);
|
|
594
|
+
}
|
|
437
595
|
function carryCounter(millis, counter) {
|
|
438
596
|
if (counter <= MAX_HLC_COUNTER)
|
|
439
597
|
return { millis, counter };
|
|
@@ -464,6 +622,8 @@ function writeLast(db, h) {
|
|
|
464
622
|
}
|
|
465
623
|
function nextHlc(db, node, now = Date.now()) {
|
|
466
624
|
const last = readLast(db, node);
|
|
625
|
+
if (last.millis > now + HLC_MAX_SKEW_MS)
|
|
626
|
+
throw mhError("clock_skew", "\u672C\u673A\u903B\u8F91\u65F6\u949F\u6BD4\u7CFB\u7EDF\u65F6\u95F4\u8D85\u524D {min} \u5206\u949F\uFF08\u7CFB\u7EDF\u65F6\u949F\u66FE\u88AB\u8C03\u9519\uFF0C\u6216\u6536\u5230\u8FC7\u65F6\u949F\u9519\u8BEF\u8BBE\u5907\u7684\u6570\u636E\uFF09\uFF0C\u8FD0\u884C mh repair --clock \u4FEE\u590D", { min: Math.ceil((last.millis - now) / 60000) });
|
|
467
627
|
const millis = Math.max(last.millis, now);
|
|
468
628
|
const counter = millis === last.millis ? last.counter + 1 : 0;
|
|
469
629
|
const h = { ...carryCounter(millis, counter), node };
|
|
@@ -475,6 +635,8 @@ function observeHlc(db, node, remote, now = Date.now()) {
|
|
|
475
635
|
const r = parseHlc(remote);
|
|
476
636
|
if (!Number.isFinite(r.millis) || !Number.isFinite(r.counter))
|
|
477
637
|
return;
|
|
638
|
+
if (r.millis > now + HLC_MAX_SKEW_MS)
|
|
639
|
+
return;
|
|
478
640
|
const millis = Math.max(last.millis, r.millis, now);
|
|
479
641
|
let counter;
|
|
480
642
|
if (millis === last.millis && millis === r.millis)
|
|
@@ -487,7 +649,18 @@ function observeHlc(db, node, remote, now = Date.now()) {
|
|
|
487
649
|
counter = 0;
|
|
488
650
|
writeLast(db, { ...carryCounter(millis, counter), node });
|
|
489
651
|
}
|
|
490
|
-
|
|
652
|
+
function persistedHlc(db) {
|
|
653
|
+
const row = db.query("SELECT value FROM meta WHERE key = 'hlc'").get();
|
|
654
|
+
return row ? parseHlc(row.value) : null;
|
|
655
|
+
}
|
|
656
|
+
function resetHlc(db, node, now = Date.now()) {
|
|
657
|
+
writeLast(db, { millis: now, counter: 0, node });
|
|
658
|
+
}
|
|
659
|
+
var MAX_HLC_COUNTER = 65535, HLC_MAX_SKEW_MS;
|
|
660
|
+
var init_hlc = __esm(() => {
|
|
661
|
+
init_errors();
|
|
662
|
+
HLC_MAX_SKEW_MS = 5 * 60000;
|
|
663
|
+
});
|
|
491
664
|
|
|
492
665
|
// src/core/blocks.ts
|
|
493
666
|
function parseDocBlocks(md) {
|
|
@@ -590,6 +763,78 @@ function lcs(a, b) {
|
|
|
590
763
|
return pairs;
|
|
591
764
|
}
|
|
592
765
|
|
|
766
|
+
// src/core/history-cache.ts
|
|
767
|
+
function cacheFor(db) {
|
|
768
|
+
let c = caches.get(db);
|
|
769
|
+
if (!c) {
|
|
770
|
+
c = { docs: new Map, blockToDoc: new Map };
|
|
771
|
+
caches.set(db, c);
|
|
772
|
+
}
|
|
773
|
+
return c;
|
|
774
|
+
}
|
|
775
|
+
function dropDoc(c, docId) {
|
|
776
|
+
const e = c.docs.get(docId);
|
|
777
|
+
if (!e)
|
|
778
|
+
return;
|
|
779
|
+
c.docs.delete(docId);
|
|
780
|
+
for (const b of e.blockIds)
|
|
781
|
+
if (c.blockToDoc.get(b) === docId)
|
|
782
|
+
c.blockToDoc.delete(b);
|
|
783
|
+
}
|
|
784
|
+
function getDocHistory(db, docId) {
|
|
785
|
+
const c = caches.get(db);
|
|
786
|
+
const e = c?.docs.get(docId);
|
|
787
|
+
if (!c || !e)
|
|
788
|
+
return;
|
|
789
|
+
c.docs.delete(docId);
|
|
790
|
+
c.docs.set(docId, e);
|
|
791
|
+
return e;
|
|
792
|
+
}
|
|
793
|
+
function setDocHistory(db, docId, changes, blockIds) {
|
|
794
|
+
const c = cacheFor(db);
|
|
795
|
+
dropDoc(c, docId);
|
|
796
|
+
const entry = { changes, blockIds: new Set(blockIds), revisions: null, states: new Map };
|
|
797
|
+
c.docs.set(docId, entry);
|
|
798
|
+
for (const b of entry.blockIds)
|
|
799
|
+
c.blockToDoc.set(b, docId);
|
|
800
|
+
while (c.docs.size > MAX_DOCS)
|
|
801
|
+
dropDoc(c, c.docs.keys().next().value);
|
|
802
|
+
return entry;
|
|
803
|
+
}
|
|
804
|
+
function rememberDocState(entry, at, state) {
|
|
805
|
+
if (entry.states.size >= MAX_STATES)
|
|
806
|
+
entry.states.delete(entry.states.keys().next().value);
|
|
807
|
+
entry.states.set(at, state);
|
|
808
|
+
}
|
|
809
|
+
function noteChange(db, dataset, rowId, col, value) {
|
|
810
|
+
const c = caches.get(db);
|
|
811
|
+
if (!c)
|
|
812
|
+
return;
|
|
813
|
+
if (dataset === "documents") {
|
|
814
|
+
dropDoc(c, rowId);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (dataset !== "doc_blocks")
|
|
818
|
+
return;
|
|
819
|
+
const owner = c.blockToDoc.get(rowId);
|
|
820
|
+
if (owner)
|
|
821
|
+
dropDoc(c, owner);
|
|
822
|
+
if (col === "doc_id" && value) {
|
|
823
|
+
try {
|
|
824
|
+
const target = JSON.parse(value);
|
|
825
|
+
if (typeof target === "string")
|
|
826
|
+
dropDoc(c, target);
|
|
827
|
+
} catch {}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function invalidateHistory(db) {
|
|
831
|
+
caches.delete(db);
|
|
832
|
+
}
|
|
833
|
+
var caches, MAX_DOCS = 16, MAX_STATES = 64;
|
|
834
|
+
var init_history_cache = __esm(() => {
|
|
835
|
+
caches = new WeakMap;
|
|
836
|
+
});
|
|
837
|
+
|
|
593
838
|
// src/core/intent-retention.ts
|
|
594
839
|
function safeNow(now) {
|
|
595
840
|
return Math.max(0, Math.trunc(Number.isFinite(now) ? now : Date.now()));
|
|
@@ -638,27 +883,27 @@ function withNodeId(node, fn) {
|
|
|
638
883
|
currentNodeOverride = prev;
|
|
639
884
|
}
|
|
640
885
|
}
|
|
641
|
-
function withChangeGroup(label, fn) {
|
|
886
|
+
function withChangeGroup(db, label, fn) {
|
|
642
887
|
if (currentTxn !== null)
|
|
643
888
|
return fn();
|
|
644
889
|
currentTxn = (currentActor ? currentActor + "/" : "") + (label ? label + ":" : "") + randomSuffix(8);
|
|
645
890
|
try {
|
|
646
|
-
return fn
|
|
891
|
+
return writeTx(db, fn);
|
|
647
892
|
} finally {
|
|
648
893
|
currentTxn = null;
|
|
649
894
|
}
|
|
650
895
|
}
|
|
651
|
-
function withTxnId(txn, fn) {
|
|
896
|
+
function withTxnId(db, txn, fn) {
|
|
652
897
|
const prev = currentTxn;
|
|
653
898
|
currentTxn = txn;
|
|
654
899
|
try {
|
|
655
|
-
return fn
|
|
900
|
+
return writeTx(db, fn);
|
|
656
901
|
} finally {
|
|
657
902
|
currentTxn = prev;
|
|
658
903
|
}
|
|
659
904
|
}
|
|
660
905
|
function grouped(fn, label = null) {
|
|
661
|
-
return (...args) => withChangeGroup(label, () => fn(...args));
|
|
906
|
+
return (db, ...args) => withChangeGroup(db, label, () => fn(db, ...args));
|
|
662
907
|
}
|
|
663
908
|
function decodeMaterializeValue(dataset, col, valueJson) {
|
|
664
909
|
if (valueJson !== null)
|
|
@@ -705,6 +950,16 @@ function materialize(db, dataset, rowId, col, valueJson) {
|
|
|
705
950
|
}
|
|
706
951
|
return;
|
|
707
952
|
}
|
|
953
|
+
if (dataset === "databases" && col.startsWith("meta.")) {
|
|
954
|
+
ensureRow(db, "databases", rowId);
|
|
955
|
+
const key = col.slice(5);
|
|
956
|
+
if (valueJson === null) {
|
|
957
|
+
db.query(`UPDATE databases SET meta = json_remove(coalesce(meta, '{}'), '$."' || ? || '"') WHERE id = ?`).run(key, rowId);
|
|
958
|
+
} else {
|
|
959
|
+
db.query(`UPDATE databases SET meta = json_set(coalesce(meta, '{}'), '$."' || ? || '"', json(?)) WHERE id = ?`).run(key, valueJson, rowId);
|
|
960
|
+
}
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
708
963
|
const d = DOMAIN[dataset];
|
|
709
964
|
if (!d || !d.cols.has(col))
|
|
710
965
|
return;
|
|
@@ -722,6 +977,8 @@ function materialize(db, dataset, rowId, col, valueJson) {
|
|
|
722
977
|
function applyChange(db, c) {
|
|
723
978
|
const inserted = 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).changes > 0;
|
|
724
979
|
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);
|
|
980
|
+
if (inserted)
|
|
981
|
+
noteChange(db, c.dataset, c.row_id, c.col, c.value);
|
|
725
982
|
if (cur.h !== c.hlc)
|
|
726
983
|
return { inserted, winner: false };
|
|
727
984
|
try {
|
|
@@ -735,18 +992,21 @@ function applyChange(db, c) {
|
|
|
735
992
|
return { inserted, winner: true };
|
|
736
993
|
}
|
|
737
994
|
function emit(db, dataset, rowId, col, value) {
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
995
|
+
const mint = () => {
|
|
996
|
+
const node = currentNodeOverride ?? getNodeId(db);
|
|
997
|
+
const change = {
|
|
998
|
+
hlc: nextHlc(db, node),
|
|
999
|
+
node_id: node,
|
|
1000
|
+
dataset,
|
|
1001
|
+
row_id: rowId,
|
|
1002
|
+
col,
|
|
1003
|
+
value: value === undefined ? null : JSON.stringify(value),
|
|
1004
|
+
txn: currentTxn
|
|
1005
|
+
};
|
|
1006
|
+
applyChange(db, change);
|
|
1007
|
+
return change;
|
|
747
1008
|
};
|
|
748
|
-
|
|
749
|
-
return change;
|
|
1009
|
+
return currentTxn !== null ? mint() : writeTx(db, mint);
|
|
750
1010
|
}
|
|
751
1011
|
function emitFields(db, dataset, rowId, fields) {
|
|
752
1012
|
const out = [];
|
|
@@ -756,16 +1016,24 @@ function emitFields(db, dataset, rowId, fields) {
|
|
|
756
1016
|
return out;
|
|
757
1017
|
}
|
|
758
1018
|
function ingest(db, changes, opts = {}) {
|
|
1019
|
+
return ingestDetailed(db, changes, opts).received;
|
|
1020
|
+
}
|
|
1021
|
+
function ingestDetailed(db, changes, opts = {}) {
|
|
759
1022
|
const now = opts.now ?? Date.now();
|
|
760
1023
|
pruneExpiredIntentReceipts(db, now);
|
|
761
1024
|
const node = getNodeId(db);
|
|
762
1025
|
let received = 0;
|
|
763
1026
|
let poisoned = 0;
|
|
764
|
-
const
|
|
765
|
-
|
|
1027
|
+
const skewed = {};
|
|
1028
|
+
const tx = () => {
|
|
1029
|
+
for (const c of changes) {
|
|
766
1030
|
if (isExpiredIntentReceipt(c, now))
|
|
767
1031
|
continue;
|
|
768
|
-
|
|
1032
|
+
if (isSkewedHlc(c.hlc, now)) {
|
|
1033
|
+
skewed[c.node_id] = (skewed[c.node_id] ?? 0) + 1;
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
observeHlc(db, node, c.hlc, now);
|
|
769
1037
|
try {
|
|
770
1038
|
if (applyChange(db, c).inserted)
|
|
771
1039
|
received++;
|
|
@@ -774,11 +1042,13 @@ function ingest(db, changes, opts = {}) {
|
|
|
774
1042
|
console.error(`[ingest] skipped a change that failed to apply (dataset=${c.dataset} col=${c.col} row=${c.row_id}): ${e.message}`);
|
|
775
1043
|
}
|
|
776
1044
|
}
|
|
777
|
-
}
|
|
778
|
-
tx
|
|
1045
|
+
};
|
|
1046
|
+
writeTx(db, tx);
|
|
779
1047
|
if (poisoned)
|
|
780
1048
|
console.error(`[ingest] ${poisoned} change(s) skipped this batch (see above)`);
|
|
781
|
-
|
|
1049
|
+
for (const [n, k] of Object.entries(skewed))
|
|
1050
|
+
console.error(`[ingest] refused ${k} change(s) from node ${n}: stamped more than 5 min in the future (that device should run mh repair --clock)`);
|
|
1051
|
+
return { received, skewed };
|
|
782
1052
|
}
|
|
783
1053
|
function changesSince(db, since, opts = {}) {
|
|
784
1054
|
const rows = db.query(`SELECT ${CHANGE_SELECT} FROM crdt_changes WHERE hlc > ? ORDER BY hlc`).all(since);
|
|
@@ -814,7 +1084,9 @@ function changesAfterSeq(db, seq, opts = {}) {
|
|
|
814
1084
|
var CHANGE_COLS, CHANGE_SELECT, currentTxn = null, currentActor = null, ACTOR_TAG_RE, currentNodeOverride = null, DOMAIN, RECORD_META, OPLOG_ONLY_DATASETS, NOT_NULL_ZERO_COLS;
|
|
815
1085
|
var init_crdt = __esm(() => {
|
|
816
1086
|
init_node();
|
|
1087
|
+
init_hlc();
|
|
817
1088
|
init_ids();
|
|
1089
|
+
init_history_cache();
|
|
818
1090
|
init_intent_retention();
|
|
819
1091
|
CHANGE_COLS = ["hlc", "node_id", "dataset", "row_id", "col", "value", "txn"];
|
|
820
1092
|
CHANGE_SELECT = CHANGE_COLS.join(", ");
|
|
@@ -870,6 +1142,10 @@ var init_crdt = __esm(() => {
|
|
|
870
1142
|
blob_policy: {
|
|
871
1143
|
table: "blob_policy",
|
|
872
1144
|
cols: new Set(["full_nodes", "redundancy", "__deleted"])
|
|
1145
|
+
},
|
|
1146
|
+
nodes: {
|
|
1147
|
+
table: "nodes",
|
|
1148
|
+
cols: new Set(["label", "platform", "form", "app", "first_seen", "__deleted"])
|
|
873
1149
|
}
|
|
874
1150
|
};
|
|
875
1151
|
RECORD_META = new Set(["database_id", "created_hlc", "order_key", "__deleted"]);
|
|
@@ -887,7 +1163,8 @@ var init_crdt = __esm(() => {
|
|
|
887
1163
|
"sites:spa",
|
|
888
1164
|
"site_channels:__deleted",
|
|
889
1165
|
"site_files:__deleted",
|
|
890
|
-
"blob_policy:__deleted"
|
|
1166
|
+
"blob_policy:__deleted",
|
|
1167
|
+
"nodes:__deleted"
|
|
891
1168
|
]);
|
|
892
1169
|
});
|
|
893
1170
|
|
|
@@ -1116,7 +1393,19 @@ var init_properties = __esm(() => {
|
|
|
1116
1393
|
function rowOut(r) {
|
|
1117
1394
|
if (!r)
|
|
1118
1395
|
return null;
|
|
1119
|
-
|
|
1396
|
+
const meta = r.meta === null ? null : JSON.parse(r.meta);
|
|
1397
|
+
return { ...r, meta: meta && Object.keys(meta).length ? meta : null };
|
|
1398
|
+
}
|
|
1399
|
+
function emitMetaPatch(db, id, cur, patch) {
|
|
1400
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
1401
|
+
if (k.includes("."))
|
|
1402
|
+
throw new MhError("invalid_input", `meta key must not contain ".": ${k}`);
|
|
1403
|
+
const next = v === undefined ? null : v;
|
|
1404
|
+
const prev = cur && k in cur ? cur[k] : null;
|
|
1405
|
+
if (JSON.stringify(prev ?? null) === JSON.stringify(next ?? null))
|
|
1406
|
+
continue;
|
|
1407
|
+
emit(db, "databases", id, `meta.${k}`, next === null ? undefined : next);
|
|
1408
|
+
}
|
|
1120
1409
|
}
|
|
1121
1410
|
function getDatabase(db, id) {
|
|
1122
1411
|
return rowOut(db.query(`SELECT ${DATABASE_SELECT} FROM databases WHERE id = ? AND __deleted = 0`).get(id));
|
|
@@ -1155,7 +1444,9 @@ var init_databases = __esm(() => {
|
|
|
1155
1444
|
if (fields.meta !== undefined) {
|
|
1156
1445
|
if (fields.meta !== null && (typeof fields.meta !== "object" || Array.isArray(fields.meta)))
|
|
1157
1446
|
throw new MhError("invalid_input", "meta must be a JSON object or null");
|
|
1158
|
-
|
|
1447
|
+
const cur = getDatabase(db, id).meta;
|
|
1448
|
+
const patch = fields.meta ?? Object.fromEntries(Object.keys(cur ?? {}).map((k) => [k, null]));
|
|
1449
|
+
emitMetaPatch(db, id, cur, patch);
|
|
1159
1450
|
}
|
|
1160
1451
|
return getDatabase(db, id);
|
|
1161
1452
|
});
|
|
@@ -1169,8 +1460,7 @@ var init_databases = __esm(() => {
|
|
|
1169
1460
|
});
|
|
1170
1461
|
if (src.meta !== null) {
|
|
1171
1462
|
const { collapsed: _hidden, ...meta } = src.meta;
|
|
1172
|
-
|
|
1173
|
-
emit(db, "databases", dup.id, "meta", meta);
|
|
1463
|
+
emitMetaPatch(db, dup.id, null, meta);
|
|
1174
1464
|
}
|
|
1175
1465
|
const propIdMap = new Map;
|
|
1176
1466
|
const selfRelNew = new Set;
|
|
@@ -1298,7 +1588,37 @@ ${fmt(shown)}
|
|
|
1298
1588
|
function resolveRef(db, ref, opts = {}) {
|
|
1299
1589
|
return resolveEntity(db, ref, opts).id;
|
|
1300
1590
|
}
|
|
1301
|
-
|
|
1591
|
+
function lookupCandidates(db, ref, opts = {}) {
|
|
1592
|
+
const trimmed = ref.trim();
|
|
1593
|
+
if (!trimmed)
|
|
1594
|
+
return [];
|
|
1595
|
+
const limit = Math.max(1, Math.min(opts.limit ?? LOOKUP_LIMIT, 100));
|
|
1596
|
+
const cands = resolveCandidates(db, trimmed).filter((c) => c.kind !== "prop");
|
|
1597
|
+
cands.sort((a, b) => Number(b.id === trimmed) - Number(a.id === trimmed));
|
|
1598
|
+
const titleCache = new Map;
|
|
1599
|
+
const out = [];
|
|
1600
|
+
for (const c of cands.slice(0, limit)) {
|
|
1601
|
+
if (c.kind === "db") {
|
|
1602
|
+
out.push({ ...c, database_id: null });
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
const row = db.query(`SELECT database_id FROM ${SPECS[c.kind].table} WHERE id = ?`).get(c.id);
|
|
1606
|
+
const databaseId = row?.database_id ?? null;
|
|
1607
|
+
let label = c.label;
|
|
1608
|
+
if (c.kind === "rec" && !label && databaseId) {
|
|
1609
|
+
if (!titleCache.has(databaseId))
|
|
1610
|
+
titleCache.set(databaseId, titlePropId(db, databaseId));
|
|
1611
|
+
const tp = titleCache.get(databaseId);
|
|
1612
|
+
if (tp) {
|
|
1613
|
+
const t = db.query(`SELECT data ->> '${tp.replace(/'/g, "''")}' AS title FROM records WHERE id = ?`).get(c.id);
|
|
1614
|
+
label = t?.title ? String(t.title) : "";
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
out.push({ kind: c.kind, id: c.id, label, database_id: databaseId });
|
|
1618
|
+
}
|
|
1619
|
+
return out;
|
|
1620
|
+
}
|
|
1621
|
+
var SPECS, PUBLIC_KINDS, HI = "{", LOOKUP_LIMIT = 20;
|
|
1302
1622
|
var init_resolve = __esm(() => {
|
|
1303
1623
|
init_ids();
|
|
1304
1624
|
init_errors();
|
|
@@ -1999,25 +2319,6 @@ var init_documents = __esm(() => {
|
|
|
1999
2319
|
throw new MhError("invalid_input", `anchor not found: no match for --old`);
|
|
2000
2320
|
if (count > 1 && !opts.replaceAll)
|
|
2001
2321
|
throw new MhError("ambiguous", `${count} matches for --old; add surrounding context or use --replace-all`);
|
|
2002
|
-
if (!opts.old.includes(`
|
|
2003
|
-
|
|
2004
|
-
`)) {
|
|
2005
|
-
const affected = blocks.filter((b) => b.text.includes(opts.old));
|
|
2006
|
-
const next = affected.map((b) => opts.replaceAll ? b.text.replaceAll(opts.old, () => opts.new) : b.text.replace(opts.old, () => opts.new));
|
|
2007
|
-
if (next.every((t) => t.length > 0 && !t.includes(`
|
|
2008
|
-
|
|
2009
|
-
`))) {
|
|
2010
|
-
const targets = opts.replaceAll ? affected : affected.slice(0, 1);
|
|
2011
|
-
let wrote = false;
|
|
2012
|
-
targets.forEach((b, i) => {
|
|
2013
|
-
if (next[i] !== b.text) {
|
|
2014
|
-
emit(db, "doc_blocks", b.id, "text", next[i]);
|
|
2015
|
-
wrote = true;
|
|
2016
|
-
}
|
|
2017
|
-
});
|
|
2018
|
-
return { id, changed: wrote, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
2322
|
const newBody = opts.replaceAll ? body.replaceAll(opts.old, () => opts.new) : body.replace(opts.old, () => opts.new);
|
|
2022
2323
|
reconcileBody(db, id, newBody);
|
|
2023
2324
|
return { id, changed: newBody !== body, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
|
|
@@ -2205,20 +2506,17 @@ function cacheStats(db) {
|
|
|
2205
2506
|
}
|
|
2206
2507
|
function knownNodes(db) {
|
|
2207
2508
|
const self = getNodeId(db);
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
for (const p of peers)
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
if (!byId.has(g.node_id))
|
|
2217
|
-
byId.set(g.node_id, null);
|
|
2218
|
-
return [...byId.entries()].map(([nodeId, label]) => ({ nodeId, label, self: nodeId === self }));
|
|
2509
|
+
const ids = new Set([self]);
|
|
2510
|
+
for (const r of db.query("SELECT id FROM nodes WHERE __deleted = 0").all())
|
|
2511
|
+
ids.add(r.id);
|
|
2512
|
+
for (const p of db.query("SELECT DISTINCT node_id FROM peers WHERE node_id IS NOT NULL").all())
|
|
2513
|
+
ids.add(p.node_id);
|
|
2514
|
+
for (const g of db.query("SELECT DISTINCT node_id FROM peer_grants WHERE node_id IS NOT NULL").all())
|
|
2515
|
+
ids.add(g.node_id);
|
|
2516
|
+
return [...ids].map((nodeId) => ({ nodeId, label: nodeLabelOf(db, nodeId), self: nodeId === self }));
|
|
2219
2517
|
}
|
|
2220
2518
|
var POLICY_ID = "default", blobBytesResolver = null, setFullNodes = (db, nodeIds) => {
|
|
2221
|
-
withChangeGroup(null, () => {
|
|
2519
|
+
withChangeGroup(db, null, () => {
|
|
2222
2520
|
const seen = new Set;
|
|
2223
2521
|
const list = nodeIds.filter((n) => n && !seen.has(n) && seen.add(n) != null);
|
|
2224
2522
|
emit(db, "blob_policy", POLICY_ID, "full_nodes", list);
|
|
@@ -2281,7 +2579,7 @@ function migrateRecords(db) {
|
|
|
2281
2579
|
db.exec("ALTER TABLE records ADD COLUMN order_key TEXT");
|
|
2282
2580
|
if (!tableExists3(db, "record_values"))
|
|
2283
2581
|
return;
|
|
2284
|
-
|
|
2582
|
+
writeTx(db, () => {
|
|
2285
2583
|
db.exec(`
|
|
2286
2584
|
UPDATE records SET data = coalesce((
|
|
2287
2585
|
SELECT json_group_object(rv.property_id, json(rv.value))
|
|
@@ -2292,7 +2590,6 @@ function migrateRecords(db) {
|
|
|
2292
2590
|
db.exec("DROP TABLE record_values");
|
|
2293
2591
|
backfillRecordOrderKeys(db);
|
|
2294
2592
|
});
|
|
2295
|
-
tx();
|
|
2296
2593
|
backfillRecordOrderKeys(db);
|
|
2297
2594
|
}
|
|
2298
2595
|
function migrateShares(db) {
|
|
@@ -2350,7 +2647,7 @@ function migrateOplog(db) {
|
|
|
2350
2647
|
function migrateCrdtChangesSeq(db) {
|
|
2351
2648
|
if (hasColumn(db, "crdt_changes", "seq"))
|
|
2352
2649
|
return;
|
|
2353
|
-
|
|
2650
|
+
writeTx(db, () => {
|
|
2354
2651
|
db.exec(`
|
|
2355
2652
|
DROP TABLE IF EXISTS crdt_changes_new;
|
|
2356
2653
|
CREATE TABLE crdt_changes_new (
|
|
@@ -2380,7 +2677,6 @@ function migrateCrdtChangesSeq(db) {
|
|
|
2380
2677
|
if (tableExists3(db, "peers"))
|
|
2381
2678
|
db.exec("UPDATE peers SET push_cursor = 0, pull_cursor = 0");
|
|
2382
2679
|
});
|
|
2383
|
-
tx();
|
|
2384
2680
|
}
|
|
2385
2681
|
function migrateStoragePeerUrls(db) {
|
|
2386
2682
|
if (!tableExists3(db, "peers"))
|
|
@@ -2413,7 +2709,7 @@ function migrateStoragePeerUrls(db) {
|
|
|
2413
2709
|
if (renames.length === 0)
|
|
2414
2710
|
return;
|
|
2415
2711
|
const map = new Map(renames.map(({ oldUrl, newUrl }) => [oldUrl, newUrl]));
|
|
2416
|
-
|
|
2712
|
+
writeTx(db, () => {
|
|
2417
2713
|
for (const { oldUrl, newUrl } of renames) {
|
|
2418
2714
|
if (tableExists3(db, "storage_cursors"))
|
|
2419
2715
|
db.query("UPDATE storage_cursors SET peer_url = ? WHERE peer_url = ?").run(newUrl, oldUrl);
|
|
@@ -2425,7 +2721,6 @@ function migrateStoragePeerUrls(db) {
|
|
|
2425
2721
|
setFullNodes(db, fullNodes.map((n) => map.get(n) ?? n));
|
|
2426
2722
|
}
|
|
2427
2723
|
});
|
|
2428
|
-
tx();
|
|
2429
2724
|
}
|
|
2430
2725
|
function migrateDatabases(db) {
|
|
2431
2726
|
if (hasColumn(db, "databases", "meta"))
|
|
@@ -2468,20 +2763,27 @@ function migrateSitesAccess(db) {
|
|
|
2468
2763
|
backfill("public_grants", "NULL");
|
|
2469
2764
|
}
|
|
2470
2765
|
}
|
|
2471
|
-
function
|
|
2472
|
-
if (!tableExists3(db,
|
|
2766
|
+
function replayDataset(db, dataset) {
|
|
2767
|
+
if (!tableExists3(db, dataset))
|
|
2473
2768
|
return;
|
|
2474
|
-
const
|
|
2769
|
+
const key = `${dataset}_replay_seq`;
|
|
2770
|
+
const wm = db.query("SELECT value FROM meta WHERE key = ?").get(key);
|
|
2475
2771
|
const since = wm ? Number(wm.value) : 0;
|
|
2476
2772
|
const changes = db.query(`SELECT seq, ${CHANGE_SELECT} FROM crdt_changes
|
|
2477
|
-
WHERE dataset =
|
|
2773
|
+
WHERE dataset = ? AND seq > ? ORDER BY seq`).all(dataset, since);
|
|
2478
2774
|
if (changes.length === 0)
|
|
2479
2775
|
return;
|
|
2480
|
-
db
|
|
2481
|
-
for (const change of
|
|
2776
|
+
writeTx(db, () => {
|
|
2777
|
+
for (const change of changes)
|
|
2482
2778
|
applyChange(db, change);
|
|
2483
|
-
db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(
|
|
2484
|
-
})
|
|
2779
|
+
db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, String(changes[changes.length - 1].seq));
|
|
2780
|
+
});
|
|
2781
|
+
}
|
|
2782
|
+
function migrateSiteChannels(db) {
|
|
2783
|
+
replayDataset(db, "site_channels");
|
|
2784
|
+
}
|
|
2785
|
+
function migrateNodes(db) {
|
|
2786
|
+
replayDataset(db, "nodes");
|
|
2485
2787
|
}
|
|
2486
2788
|
function initSchema(db) {
|
|
2487
2789
|
runSchema(db);
|
|
@@ -2497,8 +2799,8 @@ function initSchema(db) {
|
|
|
2497
2799
|
migrateShares(db);
|
|
2498
2800
|
migrateSitesAccess(db);
|
|
2499
2801
|
migrateSiteChannels(db);
|
|
2802
|
+
migrateNodes(db);
|
|
2500
2803
|
}
|
|
2501
|
-
var SITE_CHANNELS_REPLAY_KEY = "site_channels_replay_seq";
|
|
2502
2804
|
var init_schema_init = __esm(() => {
|
|
2503
2805
|
init_records();
|
|
2504
2806
|
init_documents();
|
|
@@ -2949,7 +3251,7 @@ function writeFileRow(db, siteId, cleanPath, contentType, encoding, content) {
|
|
|
2949
3251
|
return { ...same, changed: false };
|
|
2950
3252
|
const existing = fileIdFor(db, siteId, cleanPath);
|
|
2951
3253
|
const id = existing ?? newId("sf", cleanPath);
|
|
2952
|
-
withChangeGroup(null, () => {
|
|
3254
|
+
withChangeGroup(db, null, () => {
|
|
2953
3255
|
if (!existing) {
|
|
2954
3256
|
const first = emit(db, "site_files", id, "site_id", siteId);
|
|
2955
3257
|
emit(db, "site_files", id, "path", cleanPath);
|
|
@@ -8156,7 +8458,8 @@ var init_protocol = __esm(() => {
|
|
|
8156
8458
|
SyncResponseSchema = object({
|
|
8157
8459
|
node_id: string2().describe("Server's node id"),
|
|
8158
8460
|
changes: array(ChangeSchema).describe("Server changes after `since`"),
|
|
8159
|
-
cursor: number2().describe("New server cursor for the client to store")
|
|
8461
|
+
cursor: number2().describe("New server cursor for the client to store"),
|
|
8462
|
+
skewed_rejected: number2().optional().describe("Pushed changes the server refused for being stamped >5 min in the future")
|
|
8160
8463
|
});
|
|
8161
8464
|
HealthResponseSchema = object({
|
|
8162
8465
|
ok: boolean2(),
|
|
@@ -8184,6 +8487,9 @@ function getPeer(db, url) {
|
|
|
8184
8487
|
function setPeer(db, url, c) {
|
|
8185
8488
|
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(url, c.pull_cursor, c.push_cursor);
|
|
8186
8489
|
}
|
|
8490
|
+
function skewWarnings(source, skewed) {
|
|
8491
|
+
return Object.entries(skewed).map(([node, n]) => `${source}: refused ${n} change(s) from device ${node} stamped more than 5 min in the future \u2014 run \`mh repair --clock\` on that device`);
|
|
8492
|
+
}
|
|
8187
8493
|
function advanceAckedPrefix(db, fromSeq, received) {
|
|
8188
8494
|
if (received.length === 0)
|
|
8189
8495
|
return fromSeq;
|
|
@@ -8222,12 +8528,21 @@ async function syncWithPeer(db, url, opts = {}) {
|
|
|
8222
8528
|
throw new MhError(res.status === 401 || res.status === 403 ? "auth" : "network", `sync failed: ${res.status} ${await res.text()}`);
|
|
8223
8529
|
const data = await res.json();
|
|
8224
8530
|
const changes = data.changes ?? [];
|
|
8225
|
-
const
|
|
8531
|
+
const r = ingestDetailed(db, changes);
|
|
8532
|
+
const pulled = r.received;
|
|
8226
8533
|
setPeer(db, url, {
|
|
8227
8534
|
pull_cursor: data.cursor,
|
|
8228
8535
|
push_cursor: advanceAckedPrefix(db, toPush.cursor, changes)
|
|
8229
8536
|
});
|
|
8230
|
-
|
|
8537
|
+
const warnings = skewWarnings(url, r.skewed);
|
|
8538
|
+
if (data.skewed_rejected)
|
|
8539
|
+
warnings.push(`${url} refused ${data.skewed_rejected} of this device's change(s) as stamped more than 5 min in the future \u2014 run \`mh repair --clock\` here`);
|
|
8540
|
+
return {
|
|
8541
|
+
pushed: toPush.changes.length,
|
|
8542
|
+
pulled,
|
|
8543
|
+
received: changes.length,
|
|
8544
|
+
...warnings.length ? { warnings } : {}
|
|
8545
|
+
};
|
|
8231
8546
|
}
|
|
8232
8547
|
var changeKey = (c) => `${c.hlc}\x00${c.node_id}\x00${c.dataset}\x00${c.row_id}\x00${c.col}`;
|
|
8233
8548
|
var init_client = __esm(() => {
|
|
@@ -8726,9 +9041,10 @@ async function syncWithStorage(db, peerUrl, client, config, opts = {}) {
|
|
|
8726
9041
|
}
|
|
8727
9042
|
let snapKeys = [];
|
|
8728
9043
|
let pulled = 0;
|
|
9044
|
+
const skewed = {};
|
|
8729
9045
|
if (opts.pull ?? true) {
|
|
8730
9046
|
snapKeys = (await client.list(snapshotRoot(base))).map((o) => o.key).filter((k) => k.endsWith(".snap"));
|
|
8731
|
-
pulled += await pullSnapshots(db, peerUrl, snapKeys, client, key);
|
|
9047
|
+
pulled += await pullSnapshots(db, peerUrl, snapKeys, client, key, skewed);
|
|
8732
9048
|
for (const remote of await listRemoteNodes(client, base)) {
|
|
8733
9049
|
if (remote === node)
|
|
8734
9050
|
continue;
|
|
@@ -8745,7 +9061,7 @@ async function syncWithStorage(db, peerUrl, client, config, opts = {}) {
|
|
|
8745
9061
|
if (!bytes)
|
|
8746
9062
|
continue;
|
|
8747
9063
|
const changes = await decodeSegment(bytes, key);
|
|
8748
|
-
pulled +=
|
|
9064
|
+
pulled += collectSkew(skewed, ingestDetailed(db, changes));
|
|
8749
9065
|
setStorageCursor(db, peerUrl, remote, seg.key);
|
|
8750
9066
|
}
|
|
8751
9067
|
}
|
|
@@ -8772,9 +9088,15 @@ async function syncWithStorage(db, peerUrl, client, config, opts = {}) {
|
|
|
8772
9088
|
if (ownCount >= threshold)
|
|
8773
9089
|
await publishSnapshot(db, client, config, opts.snapshotRetainSegments);
|
|
8774
9090
|
}
|
|
8775
|
-
|
|
9091
|
+
const warnings = skewWarnings(peerUrl, skewed);
|
|
9092
|
+
return { pushed, pulled, pendingPush, ...warnings.length ? { warnings } : {} };
|
|
8776
9093
|
}
|
|
8777
|
-
|
|
9094
|
+
function collectSkew(into, r) {
|
|
9095
|
+
for (const [n, k] of Object.entries(r.skewed))
|
|
9096
|
+
into[n] = (into[n] ?? 0) + k;
|
|
9097
|
+
return r.received;
|
|
9098
|
+
}
|
|
9099
|
+
async function pullSnapshots(db, peerUrl, present, client, key, skewed = {}) {
|
|
8778
9100
|
if (present.length === 0)
|
|
8779
9101
|
return 0;
|
|
8780
9102
|
const consumed = getSnapshotConsumed(db, peerUrl);
|
|
@@ -8793,7 +9115,7 @@ async function pullSnapshots(db, peerUrl, present, client, key) {
|
|
|
8793
9115
|
if (!bytes)
|
|
8794
9116
|
continue;
|
|
8795
9117
|
const changes = await decodeSegment(bytes, key);
|
|
8796
|
-
pulled +=
|
|
9118
|
+
pulled += collectSkew(skewed, ingestDetailed(db, changes));
|
|
8797
9119
|
consumed.add(k);
|
|
8798
9120
|
for (const c of changes)
|
|
8799
9121
|
if (!local[c.node_id] || c.hlc > local[c.node_id])
|
|
@@ -8814,6 +9136,7 @@ var init_storage = __esm(() => {
|
|
|
8814
9136
|
init_crdt();
|
|
8815
9137
|
init_publisher_lease();
|
|
8816
9138
|
init_errors();
|
|
9139
|
+
init_client();
|
|
8817
9140
|
init_e2ee();
|
|
8818
9141
|
init_intent_retention();
|
|
8819
9142
|
DEFAULT_SNAPSHOT_MAX_INTERVAL_MS = 30 * 60000;
|
|
@@ -9025,7 +9348,7 @@ var package_default;
|
|
|
9025
9348
|
var init_package = __esm(() => {
|
|
9026
9349
|
package_default = {
|
|
9027
9350
|
name: "@tensorix/metahub",
|
|
9028
|
-
version: "0.5.
|
|
9351
|
+
version: "0.5.3",
|
|
9029
9352
|
description: "Metahub core library and CLI",
|
|
9030
9353
|
license: "AGPL-3.0-only",
|
|
9031
9354
|
type: "module",
|
|
@@ -9397,7 +9720,7 @@ function applyGuestIntent(db, policy, session, intent, opts) {
|
|
|
9397
9720
|
kind: policy.audience,
|
|
9398
9721
|
guestNode: session.guestNode
|
|
9399
9722
|
};
|
|
9400
|
-
|
|
9723
|
+
return writeTx(db, () => {
|
|
9401
9724
|
const prepared = prepareIntent(db, policy, principal, intent, opts.clock);
|
|
9402
9725
|
const txn = intentTxnPrefix(session.guestNode, intent.intentId) + prepared.fingerprint;
|
|
9403
9726
|
const receipt = readReceipt(db, session.guestNode, intent.intentId, acceptedNow);
|
|
@@ -9417,7 +9740,6 @@ function applyGuestIntent(db, policy, session, intent, opts) {
|
|
|
9417
9740
|
return applySubmitted(db, policy, principal, intent, prepared, txn, acceptedNow);
|
|
9418
9741
|
return applyAuthority(db, policy, principal, intent, prepared, txn, acceptedNow);
|
|
9419
9742
|
});
|
|
9420
|
-
return tx();
|
|
9421
9743
|
}
|
|
9422
9744
|
function prepareIntent(db, policy, principal, intent, clock) {
|
|
9423
9745
|
if (typeof intent.payload !== "object" || intent.payload === null || Array.isArray(intent.payload))
|
|
@@ -9455,7 +9777,7 @@ function prepareIntent(db, policy, principal, intent, clock) {
|
|
|
9455
9777
|
};
|
|
9456
9778
|
}
|
|
9457
9779
|
function applyAuthority(db, policy, principal, intent, prepared, txn, acceptedNow) {
|
|
9458
|
-
return withTxnId(txn, () => withNodeId(principal.guestNode, () => {
|
|
9780
|
+
return withTxnId(db, txn, () => withNodeId(principal.guestNode, () => {
|
|
9459
9781
|
if (intent.action === "createRecord")
|
|
9460
9782
|
assertGuestCreateCapacity(db, prepared.database, policy.limits);
|
|
9461
9783
|
const current = intent.action === "createRecord" ? createRecordPrepared(db, prepared.database, prepared.cells) : updateRecordPrepared(db, intent.recordId, prepared.cells);
|
|
@@ -9579,7 +9901,7 @@ function isRecordRow(value) {
|
|
|
9579
9901
|
}
|
|
9580
9902
|
function clampMillis(submittedAt, now) {
|
|
9581
9903
|
const safeNow = Math.max(0, Math.trunc(Number.isFinite(now) ? now : Date.now()));
|
|
9582
|
-
const ceil = safeNow +
|
|
9904
|
+
const ceil = safeNow + HLC_MAX_SKEW_MS;
|
|
9583
9905
|
if (!Number.isFinite(submittedAt))
|
|
9584
9906
|
return safeNow;
|
|
9585
9907
|
return Math.max(0, Math.min(Math.trunc(submittedAt), ceil));
|
|
@@ -9618,16 +9940,16 @@ function stableStringify(value) {
|
|
|
9618
9940
|
}
|
|
9619
9941
|
return JSON.stringify(value) ?? "null";
|
|
9620
9942
|
}
|
|
9621
|
-
var
|
|
9943
|
+
var INTENT_ID_RE, RECEIPT_COL = "result";
|
|
9622
9944
|
var init_guest_intent = __esm(() => {
|
|
9623
9945
|
init_errors();
|
|
9624
9946
|
init_records();
|
|
9947
|
+
init_hlc();
|
|
9625
9948
|
init_crdt();
|
|
9626
9949
|
init_ids();
|
|
9627
9950
|
init_intent_retention();
|
|
9628
9951
|
init_grants_core();
|
|
9629
9952
|
init_intent_retention();
|
|
9630
|
-
HLC_SKEW_MS = 5 * 60000;
|
|
9631
9953
|
INTENT_ID_RE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
9632
9954
|
});
|
|
9633
9955
|
|
|
@@ -9635,6 +9957,7 @@ var init_guest_intent = __esm(() => {
|
|
|
9635
9957
|
var ROOM_PROTOCOL_VERSION = 1, ROOM_BLOB_CHUNK_LIMIT, STATE_TABLE;
|
|
9636
9958
|
var init_room_protocol = __esm(() => {
|
|
9637
9959
|
init_errors();
|
|
9960
|
+
init_history_cache();
|
|
9638
9961
|
init_crdt();
|
|
9639
9962
|
init_schema_init();
|
|
9640
9963
|
init_ids();
|
|
@@ -9736,13 +10059,13 @@ async function syncWithRoom(db, cfg, transport, opts = {}) {
|
|
|
9736
10059
|
}
|
|
9737
10060
|
assertGuestAuthored(resp.changes, cfg.guestBase);
|
|
9738
10061
|
const pulled = ingest(db, resp.changes);
|
|
9739
|
-
db
|
|
10062
|
+
writeTx(db, () => {
|
|
9740
10063
|
applyPartitionDiff(db, cfg.peerKey, {
|
|
9741
10064
|
entered: [...prep.shipped, ...pulledMemberRows(db, cfg, resp.changes)],
|
|
9742
10065
|
left: prep.diff.left
|
|
9743
10066
|
});
|
|
9744
10067
|
writeCursors(db, cfg.peerKey, { pull: resp.cursor, push: prep.pushCursor });
|
|
9745
|
-
})
|
|
10068
|
+
});
|
|
9746
10069
|
if (resp.need_baseline.length > 0)
|
|
9747
10070
|
await healBaselines(db, cfg, transport, resp.need_baseline);
|
|
9748
10071
|
let healed = false;
|
|
@@ -9778,14 +10101,14 @@ async function healBaselines(db, cfg, transport, keys) {
|
|
|
9778
10101
|
const resp = await roundTrip(db, cfg, transport, changes, evict);
|
|
9779
10102
|
if (resp.share_state === "expired")
|
|
9780
10103
|
return;
|
|
9781
|
-
db
|
|
10104
|
+
writeTx(db, () => {
|
|
9782
10105
|
applyPartitionDiff(db, cfg.peerKey, {
|
|
9783
10106
|
entered: [...entered, ...pulledMemberRows(db, cfg, resp.changes)],
|
|
9784
10107
|
left: evict
|
|
9785
10108
|
});
|
|
9786
10109
|
const cur = readCursors(db, cfg.peerKey);
|
|
9787
10110
|
writeCursors(db, cfg.peerKey, { pull: resp.cursor, push: cur.push });
|
|
9788
|
-
})
|
|
10111
|
+
});
|
|
9789
10112
|
}
|
|
9790
10113
|
async function fullReconcile(db, cfg, transport) {
|
|
9791
10114
|
const members = computePartitionMembers(db, cfg.scope);
|
|
@@ -9793,11 +10116,11 @@ async function fullReconcile(db, cfg, transport) {
|
|
|
9793
10116
|
const resp = await roundTrip(db, cfg, transport, winners, [], members);
|
|
9794
10117
|
if (resp.share_state === "expired")
|
|
9795
10118
|
return;
|
|
9796
|
-
db
|
|
10119
|
+
writeTx(db, () => {
|
|
9797
10120
|
resetPartitionShadow(db, cfg.peerKey, members);
|
|
9798
10121
|
const cur = readCursors(db, cfg.peerKey);
|
|
9799
10122
|
writeCursors(db, cfg.peerKey, { pull: resp.cursor, push: cur.push });
|
|
9800
|
-
})
|
|
10123
|
+
});
|
|
9801
10124
|
}
|
|
9802
10125
|
async function roundTrip(db, cfg, transport, changes, evict, members) {
|
|
9803
10126
|
const resp = await transport({
|
|
@@ -10199,7 +10522,7 @@ function updatePublicSiteChannelUrls(db, siteId, siteName) {
|
|
|
10199
10522
|
return changed;
|
|
10200
10523
|
}
|
|
10201
10524
|
function applySiteUpdate(db, siteId, opts, hooks) {
|
|
10202
|
-
return db
|
|
10525
|
+
return withChangeGroup(db, "site.update", () => {
|
|
10203
10526
|
const updated = updateSite(db, siteId, {
|
|
10204
10527
|
...opts,
|
|
10205
10528
|
visibility: opts.visibility === "public" ? "private" : opts.visibility
|
|
@@ -10211,15 +10534,15 @@ function applySiteUpdate(db, siteId, opts, hooks) {
|
|
|
10211
10534
|
if (opts.name !== undefined)
|
|
10212
10535
|
updatePublicSiteChannelUrls(db, siteId, updated.name);
|
|
10213
10536
|
return getSite(db, siteId);
|
|
10214
|
-
})
|
|
10537
|
+
});
|
|
10215
10538
|
}
|
|
10216
10539
|
function applySiteDelete(db, siteId) {
|
|
10217
|
-
return db
|
|
10540
|
+
return withChangeGroup(db, "site.delete", () => {
|
|
10218
10541
|
if (!deleteSite(db, siteId))
|
|
10219
10542
|
return false;
|
|
10220
10543
|
revokeAllSiteChannels(db, siteId);
|
|
10221
10544
|
return true;
|
|
10222
|
-
})
|
|
10545
|
+
});
|
|
10223
10546
|
}
|
|
10224
10547
|
function revokeAllSiteChannels(db, siteId) {
|
|
10225
10548
|
const active = listSiteChannelRows(db, siteId).filter((channel) => channel.desired_state === "active");
|
|
@@ -10344,7 +10667,7 @@ var init_site_channel_store = __esm(() => {
|
|
|
10344
10667
|
policy_json: input.policy == null ? null : JSON.stringify(input.policy),
|
|
10345
10668
|
desired_state: input.desiredState ?? "active"
|
|
10346
10669
|
};
|
|
10347
|
-
db
|
|
10670
|
+
writeTx(db, () => {
|
|
10348
10671
|
if (!existing) {
|
|
10349
10672
|
const first = emit(db, "site_channels", id, "site_id", input.siteId);
|
|
10350
10673
|
emit(db, "site_channels", id, "created_hlc", first.hlc);
|
|
@@ -10354,7 +10677,7 @@ var init_site_channel_store = __esm(() => {
|
|
|
10354
10677
|
continue;
|
|
10355
10678
|
emit(db, "site_channels", id, col, value);
|
|
10356
10679
|
}
|
|
10357
|
-
})
|
|
10680
|
+
});
|
|
10358
10681
|
return getSiteChannelRow(db, id);
|
|
10359
10682
|
});
|
|
10360
10683
|
setSiteChannelDesiredState = grouped(function setSiteChannelDesiredState(db, id, desired) {
|
|
@@ -10527,6 +10850,8 @@ async function addAndSyncStoragePeer(db, spec) {
|
|
|
10527
10850
|
publish: spec.publish,
|
|
10528
10851
|
priority: spec.priority
|
|
10529
10852
|
};
|
|
10853
|
+
if (spec.provider)
|
|
10854
|
+
config.provider = spec.provider;
|
|
10530
10855
|
if (encrypt) {
|
|
10531
10856
|
if (spec.recoveryCode) {
|
|
10532
10857
|
const rawKey = await decodeRecoveryCode(spec.recoveryCode);
|
|
@@ -10630,14 +10955,13 @@ async function rotateStoragePeer(db, url, input) {
|
|
|
10630
10955
|
};
|
|
10631
10956
|
}
|
|
10632
10957
|
function removePeer(db, url) {
|
|
10633
|
-
|
|
10958
|
+
return writeTx(db, () => {
|
|
10634
10959
|
const changed = db.query("DELETE FROM peers WHERE url = ?").run(url).changes > 0;
|
|
10635
10960
|
db.query("DELETE FROM peer_grants WHERE peer_url = ?").run(url);
|
|
10636
10961
|
db.query("DELETE FROM storage_cursors WHERE peer_url = ?").run(url);
|
|
10637
10962
|
db.query("DELETE FROM room_rows WHERE peer_key = ?").run(url);
|
|
10638
10963
|
return changed;
|
|
10639
10964
|
});
|
|
10640
|
-
return tx();
|
|
10641
10965
|
}
|
|
10642
10966
|
function setPeerEnabled(db, url, enabled) {
|
|
10643
10967
|
return db.query("UPDATE peers SET enabled = ? WHERE url = ?").run(enabled ? 1 : 0, url).changes > 0;
|
|
@@ -10712,7 +11036,7 @@ async function syncPeerOnce(db, url, opts) {
|
|
|
10712
11036
|
result = await syncWithPeer(db, url, { timeoutMs: opts?.timeoutMs });
|
|
10713
11037
|
}
|
|
10714
11038
|
updatePeerStatus(db, url, "ok", null);
|
|
10715
|
-
const warnings = [];
|
|
11039
|
+
const warnings = [...result.warnings ?? []];
|
|
10716
11040
|
try {
|
|
10717
11041
|
if (peer?.kind !== "s3" && peer?.kind !== "room")
|
|
10718
11042
|
clearCoveredSiteRollbacks(db, url);
|
|
@@ -10881,20 +11205,20 @@ function normalizePublicBaseUrl(input) {
|
|
|
10881
11205
|
try {
|
|
10882
11206
|
parsed = new URL(input.trim());
|
|
10883
11207
|
} catch {
|
|
10884
|
-
throw new
|
|
11208
|
+
throw new MhError("invalid_input", "\u8BF7\u8F93\u5165\u5B8C\u6574\u7684 http(s) \u5730\u5740");
|
|
10885
11209
|
}
|
|
10886
11210
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
10887
|
-
throw new
|
|
11211
|
+
throw new MhError("invalid_input", "\u7AD9\u70B9\u5165\u53E3\u53EA\u652F\u6301 HTTP \u6216 HTTPS");
|
|
10888
11212
|
if (parsed.username || parsed.password || parsed.search || parsed.hash)
|
|
10889
|
-
throw new
|
|
11213
|
+
throw new MhError("invalid_input", "\u7AD9\u70B9\u5165\u53E3\u4E0D\u80FD\u5305\u542B\u51ED\u636E\u3001\u67E5\u8BE2\u53C2\u6570\u6216\u7247\u6BB5");
|
|
10890
11214
|
if (parsed.pathname !== "/" && parsed.pathname !== "")
|
|
10891
|
-
throw new
|
|
11215
|
+
throw new MhError("invalid_input", "\u7AD9\u70B9\u5165\u53E3\u5FC5\u987B\u662F\u57DF\u540D\u6839\u5730\u5740\uFF0C\u4E0D\u80FD\u5305\u542B\u8DEF\u5F84");
|
|
10892
11216
|
const host = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
10893
11217
|
const scope = hostScope(host);
|
|
10894
11218
|
if (scope === "invalid")
|
|
10895
|
-
throw new
|
|
11219
|
+
throw new MhError("invalid_input", "\u7AD9\u70B9\u5165\u53E3\u5FC5\u987B\u662F\u53EF\u8BBF\u95EE\u7684\u4E3B\u673A\u5730\u5740");
|
|
10896
11220
|
if (parsed.protocol === "http:" && scope === "public")
|
|
10897
|
-
throw new
|
|
11221
|
+
throw new MhError("invalid_input", "\u516C\u7F51\u5165\u53E3\u5FC5\u987B\u4F7F\u7528 HTTPS\uFF1BHTTP \u4EC5\u5141\u8BB8\u672C\u673A\u6216\u5C40\u57DF\u7F51\u5730\u5740");
|
|
10898
11222
|
parsed.pathname = "";
|
|
10899
11223
|
const url = parsed.toString().replace(/\/+$/, "");
|
|
10900
11224
|
return { url, scope };
|
|
@@ -10942,6 +11266,7 @@ function setServerConfig(db, partial) {
|
|
|
10942
11266
|
var MAX_BLOB_UPLOAD_BYTES, K_HOST = "cfg_host", K_PORT = "cfg_port", K_SYNC_INTERVAL = "cfg_sync_interval", K_AUTO_SYNC = "cfg_auto_sync", K_BLOB_QUOTA = "cfg_blob_quota", K_PUBLIC_BASE_URL = "cfg_public_base_url", DEFAULT_CONFIG;
|
|
10943
11267
|
var init_config = __esm(() => {
|
|
10944
11268
|
init_token();
|
|
11269
|
+
init_errors();
|
|
10945
11270
|
MAX_BLOB_UPLOAD_BYTES = Number(process.env.METAHUB_MAX_BLOB_UPLOAD) || 100 * 1024 * 1024;
|
|
10946
11271
|
DEFAULT_CONFIG = {
|
|
10947
11272
|
host: "127.0.0.1",
|
|
@@ -11966,6 +12291,8 @@ function redeemPairingCode(db, code) {
|
|
|
11966
12291
|
}
|
|
11967
12292
|
function mintGrant(db, peerUrl, nodeId) {
|
|
11968
12293
|
const token = randomSuffix(32);
|
|
12294
|
+
if (nodeId)
|
|
12295
|
+
db.query("DELETE FROM peer_grants WHERE node_id = ?").run(nodeId);
|
|
11969
12296
|
db.query("INSERT INTO peer_grants (token, peer_url, node_id, created_at) VALUES (?, ?, ?, ?)").run(token, peerUrl, nodeId, Date.now());
|
|
11970
12297
|
return token;
|
|
11971
12298
|
}
|
|
@@ -12163,10 +12490,14 @@ function listDevices(db) {
|
|
|
12163
12490
|
const ensure = (nodeId) => {
|
|
12164
12491
|
let v = byNode.get(nodeId);
|
|
12165
12492
|
if (!v) {
|
|
12493
|
+
const meta = readNodeMeta(db, nodeId);
|
|
12166
12494
|
v = {
|
|
12167
12495
|
nodeId,
|
|
12168
|
-
label:
|
|
12496
|
+
label: nodeLabelOf(db, nodeId),
|
|
12169
12497
|
self: nodeId === self,
|
|
12498
|
+
platform: meta?.platform ?? null,
|
|
12499
|
+
form: meta?.form ?? null,
|
|
12500
|
+
app: meta?.app ?? null,
|
|
12170
12501
|
channels: [],
|
|
12171
12502
|
lastActivityAt: null,
|
|
12172
12503
|
revocable: "none",
|
|
@@ -12177,7 +12508,9 @@ function listDevices(db) {
|
|
|
12177
12508
|
}
|
|
12178
12509
|
return v;
|
|
12179
12510
|
};
|
|
12180
|
-
ensure(self)
|
|
12511
|
+
ensure(self);
|
|
12512
|
+
for (const r of db.query("SELECT id FROM nodes WHERE __deleted = 0").all())
|
|
12513
|
+
ensure(r.id);
|
|
12181
12514
|
const oplog = db.query("SELECT node_id, MAX(hlc) AS h FROM crdt_changes GROUP BY node_id").all();
|
|
12182
12515
|
for (const r of oplog)
|
|
12183
12516
|
ensure(r.node_id).channels.push({ kind: "oplog", ref: "", lastSeenAt: parseHlc(r.h).millis });
|
|
@@ -12186,8 +12519,6 @@ function listDevices(db) {
|
|
|
12186
12519
|
if (!p.node_id)
|
|
12187
12520
|
continue;
|
|
12188
12521
|
const v = ensure(p.node_id);
|
|
12189
|
-
if (!v.label)
|
|
12190
|
-
v.label = p.label;
|
|
12191
12522
|
v.channels.push({
|
|
12192
12523
|
kind: "paired_out",
|
|
12193
12524
|
ref: p.url,
|
|
@@ -12205,6 +12536,9 @@ function listDevices(db) {
|
|
|
12205
12536
|
nodeId: null,
|
|
12206
12537
|
label: null,
|
|
12207
12538
|
self: false,
|
|
12539
|
+
platform: null,
|
|
12540
|
+
form: null,
|
|
12541
|
+
app: null,
|
|
12208
12542
|
channels: [ch],
|
|
12209
12543
|
lastActivityAt: g.created_at,
|
|
12210
12544
|
revocable: "yes",
|
|
@@ -12293,6 +12627,7 @@ async function refreshBucketPresence(db, url) {
|
|
|
12293
12627
|
}
|
|
12294
12628
|
var init_devices = __esm(() => {
|
|
12295
12629
|
init_errors();
|
|
12630
|
+
init_hlc();
|
|
12296
12631
|
init_node();
|
|
12297
12632
|
init_peers();
|
|
12298
12633
|
init_pairing();
|
|
@@ -12324,7 +12659,8 @@ function s3PeerViews(db) {
|
|
|
12324
12659
|
prefix: c.prefix ?? null,
|
|
12325
12660
|
accessKeyId: c.accessKeyId ?? null,
|
|
12326
12661
|
encrypt: c.encrypt !== false,
|
|
12327
|
-
virtualHostedStyle: c.virtualHostedStyle ?? null
|
|
12662
|
+
virtualHostedStyle: c.virtualHostedStyle ?? null,
|
|
12663
|
+
provider: c.provider ?? null
|
|
12328
12664
|
};
|
|
12329
12665
|
});
|
|
12330
12666
|
}
|
|
@@ -12401,7 +12737,8 @@ var init_peers_routes = __esm(() => {
|
|
|
12401
12737
|
prefix: string2().optional(),
|
|
12402
12738
|
encrypt: boolean2().optional(),
|
|
12403
12739
|
passphrase: string2().optional(),
|
|
12404
|
-
corsOrigins: array(string2()).optional()
|
|
12740
|
+
corsOrigins: array(string2()).optional(),
|
|
12741
|
+
provider: string2().optional()
|
|
12405
12742
|
});
|
|
12406
12743
|
S3PeerSchema = object({
|
|
12407
12744
|
url: string2(),
|
|
@@ -12418,7 +12755,8 @@ var init_peers_routes = __esm(() => {
|
|
|
12418
12755
|
prefix: string2().nullable(),
|
|
12419
12756
|
accessKeyId: string2().nullable(),
|
|
12420
12757
|
encrypt: boolean2(),
|
|
12421
|
-
virtualHostedStyle: boolean2().nullable()
|
|
12758
|
+
virtualHostedStyle: boolean2().nullable(),
|
|
12759
|
+
provider: string2().nullable()
|
|
12422
12760
|
});
|
|
12423
12761
|
DataPlaceSchema = object({
|
|
12424
12762
|
kind: _enum(["self", "device", "bucket"]),
|
|
@@ -12836,12 +13174,171 @@ var init_blob_routes = __esm(() => {
|
|
|
12836
13174
|
];
|
|
12837
13175
|
});
|
|
12838
13176
|
|
|
12839
|
-
// src/core/sync/
|
|
12840
|
-
function
|
|
13177
|
+
// src/core/sync/locale.ts
|
|
13178
|
+
function localeFromTags(tags) {
|
|
13179
|
+
if (!tags || tags.length === 0)
|
|
13180
|
+
return "zh-CN";
|
|
13181
|
+
for (const tag of tags) {
|
|
13182
|
+
if (!tag)
|
|
13183
|
+
continue;
|
|
13184
|
+
if (/^zh\b/i.test(tag))
|
|
13185
|
+
return "zh-CN";
|
|
13186
|
+
if (/^en\b/i.test(tag))
|
|
13187
|
+
return "en";
|
|
13188
|
+
}
|
|
13189
|
+
return "en";
|
|
13190
|
+
}
|
|
13191
|
+
function localeFromAcceptLanguage(header) {
|
|
13192
|
+
if (!header)
|
|
13193
|
+
return "zh-CN";
|
|
13194
|
+
return localeFromTags(header.split(",").map((s) => s.split(";")[0].trim()));
|
|
13195
|
+
}
|
|
13196
|
+
function localeFromCookie(header) {
|
|
13197
|
+
if (!header)
|
|
13198
|
+
return null;
|
|
13199
|
+
for (const part of header.split(";")) {
|
|
13200
|
+
const eq = part.indexOf("=");
|
|
13201
|
+
if (eq < 0)
|
|
13202
|
+
continue;
|
|
13203
|
+
if (part.slice(0, eq).trim() !== LANG_COOKIE)
|
|
13204
|
+
continue;
|
|
13205
|
+
const v = decodeURIComponent(part.slice(eq + 1).trim());
|
|
13206
|
+
return isLocale(v) ? v : null;
|
|
13207
|
+
}
|
|
13208
|
+
return null;
|
|
13209
|
+
}
|
|
13210
|
+
function pickLocale(req) {
|
|
13211
|
+
return localeFromCookie(req.headers.get("cookie")) ?? localeFromAcceptLanguage(req.headers.get("accept-language"));
|
|
13212
|
+
}
|
|
13213
|
+
var LANG_COOKIE = "mh-lang", isLocale = (v) => v === "zh-CN" || v === "en";
|
|
13214
|
+
|
|
13215
|
+
// src/core/sync/page-messages.ts
|
|
13216
|
+
function pm(locale, key, params) {
|
|
13217
|
+
const s = locale === "en" ? EN[key] : key;
|
|
13218
|
+
return params ? s.replace(/\{(\w+)\}/g, (m, k) => (k in params) ? String(params[k]) : m) : s;
|
|
13219
|
+
}
|
|
13220
|
+
function pmIfKnown(locale, text, params) {
|
|
13221
|
+
if (locale === "en" && text in EN)
|
|
13222
|
+
return pm(locale, text, params);
|
|
13223
|
+
return params ? pm(locale, text, params) : text;
|
|
13224
|
+
}
|
|
13225
|
+
function localizeError(locale, e) {
|
|
13226
|
+
if (e instanceof MhError && e.i18n)
|
|
13227
|
+
return pmIfKnown(locale, e.i18n.key, e.i18n.params);
|
|
12841
13228
|
const message = e instanceof Error ? e.message : String(e);
|
|
13229
|
+
return pmIfKnown(locale, message);
|
|
13230
|
+
}
|
|
13231
|
+
var EN;
|
|
13232
|
+
var init_page_messages = __esm(() => {
|
|
13233
|
+
init_errors();
|
|
13234
|
+
EN = {
|
|
13235
|
+
"\u672C\u673A\u903B\u8F91\u65F6\u949F\u6BD4\u7CFB\u7EDF\u65F6\u95F4\u8D85\u524D {min} \u5206\u949F\uFF08\u7CFB\u7EDF\u65F6\u949F\u66FE\u88AB\u8C03\u9519\uFF0C\u6216\u6536\u5230\u8FC7\u65F6\u949F\u9519\u8BEF\u8BBE\u5907\u7684\u6570\u636E\uFF09\uFF0C\u8FD0\u884C mh repair --clock \u4FEE\u590D": "This device's logical clock is {min} min ahead of wall time (the system clock was wrong, or data arrived from a device with a wrong clock); run mh repair --clock",
|
|
13236
|
+
"\u6570\u636E\u5E93\u6B63\u88AB\u53E6\u4E00\u4E2A\u8FDB\u7A0B\u5199\u5165\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5": "The database is being written by another process; retry shortly",
|
|
13237
|
+
"\u901A\u8FC7 metahub \u5206\u4EAB": "Shared via metahub",
|
|
13238
|
+
"\u7531 metahub \u6258\u7BA1": "Hosted by metahub",
|
|
13239
|
+
"\uFF08\u7A7A\u6587\u6863\uFF09": "(empty document)",
|
|
13240
|
+
\u{6587}\u{6863}: "Document",
|
|
13241
|
+
\u{8868}\u{683c}: "Table",
|
|
13242
|
+
\u{7f16}\u{8f91}: "Edit",
|
|
13243
|
+
\u{7f16}\u{8f91}\u{6587}\u{6863}: "Edit document",
|
|
13244
|
+
\u{4fdd}\u{5b58}: "Save",
|
|
13245
|
+
"\u4FDD\u5B58\u4E2D\u2026": "Saving\u2026",
|
|
13246
|
+
\u{4fdd}\u{5b58}\u{5931}\u{8d25}: "Save failed",
|
|
13247
|
+
"\u5DF2\u4FDD\u5B58 \u2713": "Saved \u2713",
|
|
13248
|
+
"\u53EF\u7F16\u8F91\u6587\u672C/\u6570\u5B57/URL/\u65E5\u671F\u5355\u5143\u683C\uFF0C\u5931\u7126\u81EA\u52A8\u4FDD\u5B58": "Text, number, URL and date cells are editable; changes save on blur",
|
|
13249
|
+
"\u7F16\u8F91\u7AD9\u70B9\u6587\u4EF6\uFF08\u6587\u672C\u6587\u4EF6\u53EF\u5728\u7EBF\u7F16\u8F91\uFF1B\u4FDD\u5B58\u5373\u5BF9\u6240\u6709\u8BBF\u95EE\u8005\u751F\u6548\uFF09\u3002": "Edit site files (text files can be edited here; saving applies to every visitor).",
|
|
13250
|
+
"\uFF08\u6682\u65E0\u6587\u4EF6\uFF09": "(no files yet)",
|
|
13251
|
+
"\u9884\u89C8 \u2197": "Preview \u2197",
|
|
13252
|
+
"\uFF08\u4E8C\u8FDB\u5236\u6587\u4EF6\uFF0C\u4E0D\u53EF\u5728\u7EBF\u7F16\u8F91\uFF09": "(binary file, cannot be edited here)",
|
|
13253
|
+
"{name} \xB7 \u6587\u4EF6": "{name} \xB7 Files",
|
|
13254
|
+
"\uD83D\uDD12 \u6B64\u5206\u4EAB\u53D7\u53E3\u4EE4\u4FDD\u62A4": "\uD83D\uDD12 This share is protected by a passcode",
|
|
13255
|
+
"\u53E3\u4EE4\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5\u3002": "Wrong passcode, try again.",
|
|
13256
|
+
\u{53e3}\u{4ee4}: "Passcode",
|
|
13257
|
+
\u{89e3}\u{9501}: "Unlock",
|
|
13258
|
+
\u{53d7}\u{4fdd}\u{62a4}\u{7684}\u{5206}\u{4eab}: "Protected share",
|
|
13259
|
+
\u{590d}\u{5236}\u{5168}\u{6587}: "Copy all",
|
|
13260
|
+
\u{5df2}\u{590d}\u{5236}: "Copied",
|
|
13261
|
+
\u{590d}\u{5236}\u{5931}\u{8d25}: "Copy failed",
|
|
13262
|
+
\u{7ad9}\u{70b9}\u{5c1a}\u{672a}\u{540c}\u{6b65}: "Site not synced yet",
|
|
13263
|
+
"\u5206\u4EAB\u65B9\u8BBE\u5907\u4E0A\u7EBF\u540C\u6B65\u540E\u5373\u53EF\u8BBF\u95EE\u3002": "Available once the sharing device comes online and syncs.",
|
|
13264
|
+
\u{9875}\u{9762}\u{4e0d}\u{5b58}\u{5728}: "Page not found",
|
|
13265
|
+
"\u8FD9\u4E2A\u7AD9\u70B9\u91CC\u6CA1\u6709\u8FD9\u4E2A\u9875\u9762\u6216\u6587\u4EF6\u3002": "This site has no such page or file.",
|
|
13266
|
+
\u{8d44}\u{6e90}\u{6682}\u{4e0d}\u{53ef}\u{7528}: "Resource temporarily unavailable",
|
|
13267
|
+
"\u6587\u4EF6\u5185\u5BB9\u5C1A\u672A\u540C\u6B65\u5230\u623F\u95F4\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002": "The file content has not synced to the room yet. Try again later.",
|
|
13268
|
+
"\u6587\u4EF6\u7684\u5185\u5BB9\u6682\u65F6\u65E0\u6CD5\u53D6\u5F97\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002": "The file content cannot be retrieved right now. Try again later.",
|
|
13269
|
+
\u{7ad9}\u{70b9}\u{4e0d}\u{5b58}\u{5728}: "Site not found",
|
|
13270
|
+
"\u6CA1\u6709\u540D\u4E3A \u201C{name}\u201D \u7684\u7AD9\u70B9\u3002": "There is no site named \u201C{name}\u201D.",
|
|
13271
|
+
\u{8fd4}\u{56de}\u{7ad9}\u{70b9}\u{9996}\u{9875}: "Back to site home",
|
|
13272
|
+
"\u9A8C\u8BC1\u8BBF\u95EE\u4EE4\u724C \xB7 metahub": "Verify access token \xB7 metahub",
|
|
13273
|
+
\u{8f93}\u{5165}\u{8bbf}\u{95ee}\u{4ee4}\u{724c}: "Enter the access token",
|
|
13274
|
+
"\u6B64\u670D\u52A1\u5668\u5DF2\u5F00\u542F\u8BBF\u95EE\u4FDD\u62A4\u3002\u5728\u5DF2\u767B\u5F55\u8BBE\u5907\u7684\u300C\u8BBE\u7F6E \u2192 \u8BBE\u5907\u4E0E\u6388\u6743\u300D\u83B7\u53D6\u767B\u5F55\u94FE\u63A5\u6216\u4E8C\u7EF4\u7801,\u6216\u5728\u670D\u52A1\u5668\u4E0A\u8FD0\u884C {cmd}\u3002": "This server requires an access token. Get a login link or QR code from Settings \u2192 Devices on a signed-in device, or run {cmd} on the server.",
|
|
13275
|
+
\u{7c98}\u{8d34}\u{8bbf}\u{95ee}\u{4ee4}\u{724c}\u{6216}\u{767b}\u{5f55}\u{94fe}\u{63a5}: "Paste the access token or login link",
|
|
13276
|
+
\u{663e}\u{793a}\u{4ee4}\u{724c}: "Show token",
|
|
13277
|
+
\u{9690}\u{85cf}\u{4ee4}\u{724c}: "Hide token",
|
|
13278
|
+
\u{89e3}\u{9501}\u{5e76}\u{8fdb}\u{5165}: "Unlock and enter",
|
|
13279
|
+
"\u8BF7\u8F93\u5165\u8BBF\u95EE\u4EE4\u724C\u3002": "Enter the access token.",
|
|
13280
|
+
"\u9A8C\u8BC1\u4E2D\u2026": "Verifying\u2026",
|
|
13281
|
+
"\u4EE4\u724C\u65E0\u6548,\u8BF7\u68C0\u67E5\u540E\u91CD\u8BD5\u3002": "Invalid token. Check it and try again.",
|
|
13282
|
+
"\u767B\u5F55\u5DF2\u8FC7\u671F,\u8BF7\u91CD\u65B0\u8F93\u5165\u4EE4\u724C\u3002": "Session expired. Enter the token again.",
|
|
13283
|
+
\u{9644}\u{4ef6}\u{5df2}\u{5931}\u{6548}: "Attachment unavailable",
|
|
13284
|
+
\u{94fe}\u{63a5}\u{7f3a}\u{5c11}\u{5206}\u{4eab}\u{6570}\u{636e}: "The link is missing share data",
|
|
13285
|
+
\u{8bf7}\u{8f93}\u{5165}\u{5206}\u{4eab}\u{53e3}\u{4ee4}: "Enter the share passcode",
|
|
13286
|
+
\u{94fe}\u{63a5}\u{7f3a}\u{5c11}\u{5bc6}\u{94a5}: "The link is missing its key",
|
|
13287
|
+
\u{53e3}\u{4ee4}\u{9519}\u{8bef}\u{6216}\u{94fe}\u{63a5}\u{5df2}\u{5931}\u{6548}: "Wrong passcode or the link has expired",
|
|
13288
|
+
\u{94fe}\u{63a5}\u{5df2}\u{5931}\u{6548}\u{6216}\u{5bc6}\u{94a5}\u{4e0d}\u{6b63}\u{786e}: "The link has expired or the key is wrong",
|
|
13289
|
+
\u{5206}\u{4eab}: "Share",
|
|
13290
|
+
\u{6b63}\u{5728}\u{52a0}\u{8f7d}: "Loading",
|
|
13291
|
+
"\u2705 \u5DF2\u6388\u6743": "\u2705 Authorized",
|
|
13292
|
+
"\u53EF\u4EE5\u5173\u95ED\u6B64\u6807\u7B7E\u9875\uFF0C\u56DE\u5230 MetaHub \u7EE7\u7EED\u90E8\u7F72\u3002": "You can close this tab and return to MetaHub to continue deploying.",
|
|
13293
|
+
\u{7ad9}\u{70b9}\u{5165}\u{53e3}\u{5065}\u{5eb7}\u{54cd}\u{5e94}\u{8fc7}\u{5927}: "Site entry health response is too large",
|
|
13294
|
+
\u{7ad9}\u{70b9}\u{5165}\u{53e3}\u{4e0d}\u{80fd}\u{6307}\u{5411}\u{4e91}\u{5e73}\u{53f0}\u{5143}\u{6570}\u{636e}\u{670d}\u{52a1}: "Site entry must not point at a cloud metadata service",
|
|
13295
|
+
"Desktop sidecar \u4EC5\u7528\u4E8E\u672C\u673A\u9884\u89C8\uFF1B\u8BF7\u542F\u52A8\u5E26\u9274\u6743\u7684 mh --server\uFF0C\u6216\u4F7F\u7528 Edge": "The desktop sidecar is for local preview only; start an authenticated mh --server, or use Edge",
|
|
13296
|
+
"\u7AD9\u70B9\u5165\u53E3\u5065\u5EB7\u68C0\u67E5\u5931\u8D25\uFF08HTTP {status}\uFF09": "Site entry health check failed (HTTP {status})",
|
|
13297
|
+
\u{7ad9}\u{70b9}\u{5165}\u{53e3}\u{5065}\u{5eb7}\u{54cd}\u{5e94}\u{65e0}\u{6548}: "Site entry health response is invalid",
|
|
13298
|
+
"\u7AD9\u70B9\u5165\u53E3\u8282\u70B9\u4E0D\u5339\u914D\uFF08\u671F\u671B {expected}\uFF0C\u5B9E\u9645 {actual}\uFF09\uFF1B\u8FD9\u662F\u9632\u8BEF\u914D\u68C0\u67E5\uFF0C\u4E0D\u662F\u8EAB\u4EFD\u8BA4\u8BC1": "Site entry node mismatch (expected {expected}, got {actual}); this is a misconfiguration guard, not authentication",
|
|
13299
|
+
"\u76EE\u6807\u8BBE\u5907\u7248\u672C\u4E0D\u652F\u6301\u5B9A\u5411\u53D1\u5E03\u6E20\u9053\uFF1B\u8BF7\u5148\u5347\u7EA7\u8BE5\u8BBE\u5907\uFF0C\u518D\u91CD\u65B0\u9A8C\u8BC1\u5165\u53E3": "The target device does not support targeted publishing channels; upgrade it, then verify the entry again",
|
|
13300
|
+
"\u65E0\u6CD5\u9A8C\u8BC1\u7AD9\u70B9\u5165\u53E3\uFF1A\u8FDE\u63A5\u8D85\u65F6": "Could not verify the site entry: connection timed out",
|
|
13301
|
+
"\u65E0\u6CD5\u9A8C\u8BC1\u7AD9\u70B9\u5165\u53E3\uFF1A{msg}": "Could not verify the site entry: {msg}",
|
|
13302
|
+
"\u8BF7\u5148\u5728\u201C\u8BBE\u7F6E \u2192 \u7AD9\u70B9\u6258\u7BA1\u201D\u914D\u7F6E\u5E76\u9A8C\u8BC1\u5F53\u524D\u8BBE\u5907\u7684\u516C\u7F51\u6216\u5C40\u57DF\u7F51\u5165\u53E3": "Configure and verify this device's public or LAN entry under Settings \u2192 Site hosting first",
|
|
13303
|
+
"\u914D\u5BF9\u8BBE\u5907\u7F3A\u5C11\u8282\u70B9\u8EAB\u4EFD\uFF0C\u91CD\u65B0\u914D\u5BF9\u540E\u518D\u8BD5": "The paired device has no node identity; pair again and retry",
|
|
13304
|
+
"\u8BE5\u7AD9\u70B9\u4ECD\u5728\u7B49\u5F85\u76EE\u6807\u8BBE\u5907\u786E\u8BA4\u56DE\u6EDA\uFF0C\u8BF7\u5148\u91CD\u8BD5\u56DE\u6EDA": "This site is still waiting for the target device to confirm the rollback; retry the rollback first",
|
|
13305
|
+
\u{914d}\u{5bf9}\u{8bbe}\u{5907}\u{540c}\u{6b65}\u{5931}\u{8d25}: "Sync with the paired device failed",
|
|
13306
|
+
\u{6ca1}\u{6709}\u{5f85}\u{786e}\u{8ba4}\u{7684}\u{7ad9}\u{70b9}\u{56de}\u{6eda}: "No site rollback is awaiting confirmation",
|
|
13307
|
+
"\u76EE\u6807\u914D\u5BF9\u8BBE\u5907\u4E0D\u5B58\u5728\uFF0C\u8BF7\u91CD\u65B0\u914D\u5BF9\u540E\u91CD\u8BD5": "The target paired device does not exist; pair again and retry",
|
|
13308
|
+
\u{7ad9}\u{70b9}\u{6e20}\u{9053}\u{5df2}\u{4e0d}\u{5b58}\u{5728}: "The site channel no longer exists",
|
|
13309
|
+
"OAuth \u6D41\u7A0B\u4E0D\u5B58\u5728\u6216\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55": "The OAuth flow does not exist or has expired; sign in again",
|
|
13310
|
+
"OAuth \u6D41\u7A0B\u4E0D\u5B58\u5728\u6216\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u8BD5": "The OAuth flow does not exist or has expired; try again",
|
|
13311
|
+
"Cloudflare \u6388\u6743\u5931\u8D25": "Cloudflare authorization failed",
|
|
13312
|
+
"Cloudflare \u6388\u6743\u5C1A\u672A\u5B8C\u6210\uFF0C\u8BF7\u7A0D\u5019": "Cloudflare authorization is not finished yet; please wait",
|
|
13313
|
+
"\u7F3A\u5C11 Cloudflare \u8D26\u53F7 id": "Missing Cloudflare account id",
|
|
13314
|
+
"\u7F3A\u5C11 Cloudflare \u51ED\u636E\uFF08\u8BF7\u5148\u767B\u5F55\u6216\u63D0\u4F9B API Token\uFF09": "Missing Cloudflare credentials (sign in first or provide an API token)",
|
|
13315
|
+
"Cloudflare \u6388\u6743\u5931\u8D25\uFF1A{detail}": "Cloudflare authorization failed: {detail}",
|
|
13316
|
+
"\u672A\u8FD4\u56DE access token": "no access token returned",
|
|
13317
|
+
"\u65E0\u6CD5\u8FDE\u63A5 Cloudflare \u6388\u6743\u670D\u52A1\uFF1A{msg}": "Could not reach the Cloudflare authorization service: {msg}",
|
|
13318
|
+
"Cloudflare \u4EE4\u724C\u4EA4\u6362\u5931\u8D25\uFF08HTTP {status}\uFF09": "Cloudflare token exchange failed (HTTP {status})",
|
|
13319
|
+
"\u65E0\u6CD5\u8BFB\u53D6 Cloudflare \u8D26\u53F7\u5217\u8868\uFF1A{msg}": "Could not read the Cloudflare account list: {msg}",
|
|
13320
|
+
"\u8BFB\u53D6 Cloudflare \u8D26\u53F7\u5931\u8D25\uFF08HTTP {status}\uFF09": "Reading Cloudflare accounts failed (HTTP {status})",
|
|
13321
|
+
"\u672A\u914D\u7F6E Cloudflare OAuth \u5BA2\u6237\u7AEF\uFF0C\u8BF7\u6539\u7528 API Token \u90E8\u7F72": "No Cloudflare OAuth client is configured; deploy with an API token instead",
|
|
13322
|
+
"Cloudflare \u6388\u6743\u8D85\u65F6\uFF0C\u8BF7\u91CD\u8BD5": "Cloudflare authorization timed out; try again",
|
|
13323
|
+
"Cloudflare \u6388\u6743\u88AB\u62D2\u7EDD\uFF1A{detail}": "Cloudflare authorization was denied: {detail}",
|
|
13324
|
+
"OAuth state \u4E0D\u5339\u914D\uFF0C\u7591\u4F3C\u4F2A\u9020\u56DE\u8C03\uFF0C\u5DF2\u4E2D\u6B62": "OAuth state mismatch; the callback looks forged, aborted",
|
|
13325
|
+
"Cloudflare \u672A\u8FD4\u56DE\u6388\u6743\u7801": "Cloudflare did not return an authorization code",
|
|
13326
|
+
"\u8BF7\u8F93\u5165\u5B8C\u6574\u7684 http(s) \u5730\u5740": "Enter a complete http(s) address",
|
|
13327
|
+
"\u7AD9\u70B9\u5165\u53E3\u53EA\u652F\u6301 HTTP \u6216 HTTPS": "Site entry supports HTTP or HTTPS only",
|
|
13328
|
+
"\u7AD9\u70B9\u5165\u53E3\u4E0D\u80FD\u5305\u542B\u51ED\u636E\u3001\u67E5\u8BE2\u53C2\u6570\u6216\u7247\u6BB5": "Site entry must not contain credentials, query parameters or fragments",
|
|
13329
|
+
"\u7AD9\u70B9\u5165\u53E3\u5FC5\u987B\u662F\u57DF\u540D\u6839\u5730\u5740\uFF0C\u4E0D\u80FD\u5305\u542B\u8DEF\u5F84": "Site entry must be a domain root without a path",
|
|
13330
|
+
\u{7ad9}\u{70b9}\u{5165}\u{53e3}\u{5fc5}\u{987b}\u{662f}\u{53ef}\u{8bbf}\u{95ee}\u{7684}\u{4e3b}\u{673a}\u{5730}\u{5740}: "Site entry must be a reachable host address",
|
|
13331
|
+
"\u516C\u7F51\u5165\u53E3\u5FC5\u987B\u4F7F\u7528 HTTPS\uFF1BHTTP \u4EC5\u5141\u8BB8\u672C\u673A\u6216\u5C40\u57DF\u7F51\u5730\u5740": "A public entry must use HTTPS; HTTP is only allowed for local or LAN addresses"
|
|
13332
|
+
};
|
|
13333
|
+
});
|
|
13334
|
+
|
|
13335
|
+
// src/core/sync/routes.ts
|
|
13336
|
+
function errorResponse(e, req) {
|
|
13337
|
+
const message = req ? localizeError(pickLocale(req), e) : e instanceof Error ? e.message : String(e);
|
|
12842
13338
|
const code = errorCode(e);
|
|
12843
13339
|
return Response.json(code ? { error: message, code } : { error: message }, {
|
|
12844
|
-
status: code ? HTTP_STATUS[code] : 400
|
|
13340
|
+
status: code ? HTTP_STATUS[code] : 400,
|
|
13341
|
+
...code === "busy" ? { headers: { "retry-after": "1" } } : {}
|
|
12845
13342
|
});
|
|
12846
13343
|
}
|
|
12847
13344
|
var HTTP_STATUS, syncRoutes, routes;
|
|
@@ -12852,6 +13349,7 @@ var init_routes = __esm(() => {
|
|
|
12852
13349
|
init_peers_routes();
|
|
12853
13350
|
init_blob_routes();
|
|
12854
13351
|
init_errors();
|
|
13352
|
+
init_page_messages();
|
|
12855
13353
|
init_package();
|
|
12856
13354
|
HTTP_STATUS = {
|
|
12857
13355
|
invalid_input: 400,
|
|
@@ -12862,6 +13360,8 @@ var init_routes = __esm(() => {
|
|
|
12862
13360
|
auth: 401,
|
|
12863
13361
|
network: 502,
|
|
12864
13362
|
rate_limited: 429,
|
|
13363
|
+
busy: 503,
|
|
13364
|
+
clock_skew: 409,
|
|
12865
13365
|
port_in_use: 500
|
|
12866
13366
|
};
|
|
12867
13367
|
syncRoutes = [
|
|
@@ -12873,12 +13373,18 @@ var init_routes = __esm(() => {
|
|
|
12873
13373
|
response: SyncResponseSchema,
|
|
12874
13374
|
async handler(req, { db, node }) {
|
|
12875
13375
|
const body = await req.json();
|
|
12876
|
-
|
|
13376
|
+
const r = ingestDetailed(db, body.changes ?? []);
|
|
13377
|
+
const skewed = Object.values(r.skewed).reduce((a, b) => a + b, 0);
|
|
12877
13378
|
const batch = changesAfterSeq(db, body.since ?? 0, {
|
|
12878
13379
|
limit: body.limit,
|
|
12879
13380
|
excludeDatasets: body.exclude_datasets
|
|
12880
13381
|
});
|
|
12881
|
-
return Response.json({
|
|
13382
|
+
return Response.json({
|
|
13383
|
+
node_id: node,
|
|
13384
|
+
changes: batch.changes,
|
|
13385
|
+
cursor: batch.cursor,
|
|
13386
|
+
...skewed ? { skewed_rejected: skewed } : {}
|
|
13387
|
+
});
|
|
12882
13388
|
}
|
|
12883
13389
|
},
|
|
12884
13390
|
{
|
|
@@ -13086,12 +13592,22 @@ function renewToken(req, url, cfg) {
|
|
|
13086
13592
|
function wantsHtml(req) {
|
|
13087
13593
|
return req.method === "GET" && (req.headers.get("accept") ?? "").includes("text/html");
|
|
13088
13594
|
}
|
|
13089
|
-
function unlockPage() {
|
|
13090
|
-
|
|
13595
|
+
function unlockPage(locale = "zh-CN") {
|
|
13596
|
+
const m = (key) => escapeAttr(pm(locale, key));
|
|
13597
|
+
const L = JSON.stringify({
|
|
13598
|
+
go: pm(locale, "\u89E3\u9501\u5E76\u8FDB\u5165"),
|
|
13599
|
+
show: pm(locale, "\u663E\u793A\u4EE4\u724C"),
|
|
13600
|
+
hide: pm(locale, "\u9690\u85CF\u4EE4\u724C"),
|
|
13601
|
+
empty: pm(locale, "\u8BF7\u8F93\u5165\u8BBF\u95EE\u4EE4\u724C\u3002"),
|
|
13602
|
+
verifying: pm(locale, "\u9A8C\u8BC1\u4E2D\u2026"),
|
|
13603
|
+
invalid: pm(locale, "\u4EE4\u724C\u65E0\u6548,\u8BF7\u68C0\u67E5\u540E\u91CD\u8BD5\u3002"),
|
|
13604
|
+
expired: pm(locale, "\u767B\u5F55\u5DF2\u8FC7\u671F,\u8BF7\u91CD\u65B0\u8F93\u5165\u4EE4\u724C\u3002")
|
|
13605
|
+
});
|
|
13606
|
+
return `<!doctype html><html lang="${locale}"><head>
|
|
13091
13607
|
<meta charset="utf-8">
|
|
13092
13608
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
13093
13609
|
<meta name="theme-color" content="#ffffff">
|
|
13094
|
-
<title
|
|
13610
|
+
<title>${m("\u9A8C\u8BC1\u8BBF\u95EE\u4EE4\u724C \xB7 metahub")}</title>
|
|
13095
13611
|
<script>
|
|
13096
13612
|
// FOUC-guard theme resolution, same key/logic as src/webui/theme.ts.
|
|
13097
13613
|
try {
|
|
@@ -13105,18 +13621,18 @@ function unlockPage() {
|
|
|
13105
13621
|
<div class="boot" id="boot"><svg viewBox="0 0 24 24">${CUBE_SVG}</svg></div>
|
|
13106
13622
|
<form class="card" id="f" style="display:none" autocomplete="on">
|
|
13107
13623
|
<div class="brand"><svg viewBox="0 0 24 24">${CUBE_SVG}</svg></div>
|
|
13108
|
-
<h1
|
|
13109
|
-
<p class="sub"
|
|
13624
|
+
<h1>${m("\u8F93\u5165\u8BBF\u95EE\u4EE4\u724C")}</h1>
|
|
13625
|
+
<p class="sub">${pm(locale, "\u6B64\u670D\u52A1\u5668\u5DF2\u5F00\u542F\u8BBF\u95EE\u4FDD\u62A4\u3002\u5728\u5DF2\u767B\u5F55\u8BBE\u5907\u7684\u300C\u8BBE\u7F6E \u2192 \u8BBE\u5907\u4E0E\u6388\u6743\u300D\u83B7\u53D6\u767B\u5F55\u94FE\u63A5\u6216\u4E8C\u7EF4\u7801,\u6216\u5728\u670D\u52A1\u5668\u4E0A\u8FD0\u884C {cmd}\u3002", { cmd: "<code>mh token show</code>" })}</p>
|
|
13110
13626
|
<div class="field">
|
|
13111
|
-
<input id="t" type="password" placeholder="\u7C98\u8D34\u8BBF\u95EE\u4EE4\u724C\u6216\u767B\u5F55\u94FE\u63A5"
|
|
13627
|
+
<input id="t" type="password" placeholder="${m("\u7C98\u8D34\u8BBF\u95EE\u4EE4\u724C\u6216\u767B\u5F55\u94FE\u63A5")}"
|
|
13112
13628
|
autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" enterkeyhint="go">
|
|
13113
|
-
<button type="button" class="peek" id="peek" aria-label="\u663E\u793A\u4EE4\u724C"><svg viewBox="0 0 24 24">${EYE_SVG}</svg></button>
|
|
13629
|
+
<button type="button" class="peek" id="peek" aria-label="${m("\u663E\u793A\u4EE4\u724C")}"><svg viewBox="0 0 24 24">${EYE_SVG}</svg></button>
|
|
13114
13630
|
</div>
|
|
13115
13631
|
<p class="err" id="e" style="display:none"></p>
|
|
13116
|
-
<button type="submit" class="go" id="go"
|
|
13632
|
+
<button type="submit" class="go" id="go">${m("\u89E3\u9501\u5E76\u8FDB\u5165")}</button>
|
|
13117
13633
|
</form>
|
|
13118
13634
|
<script>
|
|
13119
|
-
var KEY = "mh_token";
|
|
13635
|
+
var KEY = "mh_token", L = ${L};
|
|
13120
13636
|
var form = document.getElementById("f"), boot = document.getElementById("boot");
|
|
13121
13637
|
var input = document.getElementById("t"), errEl = document.getElementById("e");
|
|
13122
13638
|
var goBtn = document.getElementById("go"), peek = document.getElementById("peek");
|
|
@@ -13141,7 +13657,7 @@ function unlockPage() {
|
|
|
13141
13657
|
}
|
|
13142
13658
|
function fail(msg) {
|
|
13143
13659
|
goBtn.disabled = false;
|
|
13144
|
-
goBtn.
|
|
13660
|
+
goBtn.textContent = L.go;
|
|
13145
13661
|
errEl.textContent = msg;
|
|
13146
13662
|
errEl.style.display = "flex";
|
|
13147
13663
|
form.classList.remove("bad");
|
|
@@ -13154,7 +13670,7 @@ function unlockPage() {
|
|
|
13154
13670
|
var pw = input.type === "password";
|
|
13155
13671
|
input.type = pw ? "text" : "password";
|
|
13156
13672
|
peek.innerHTML = '<svg viewBox="0 0 24 24">' + (pw ? ${JSON.stringify(EYE_OFF_SVG)} : ${JSON.stringify(EYE_SVG)}) + "</svg>";
|
|
13157
|
-
peek.setAttribute("aria-label", pw ?
|
|
13673
|
+
peek.setAttribute("aria-label", pw ? L.hide : L.show);
|
|
13158
13674
|
input.focus();
|
|
13159
13675
|
});
|
|
13160
13676
|
input.addEventListener("input", function () {
|
|
@@ -13164,9 +13680,9 @@ function unlockPage() {
|
|
|
13164
13680
|
form.addEventListener("submit", function (ev) {
|
|
13165
13681
|
ev.preventDefault();
|
|
13166
13682
|
var token = normalize(input.value);
|
|
13167
|
-
if (!token) { fail(
|
|
13683
|
+
if (!token) { fail(L.empty); return; }
|
|
13168
13684
|
goBtn.disabled = true;
|
|
13169
|
-
goBtn.innerHTML = '<span class="spin"></span
|
|
13685
|
+
goBtn.innerHTML = '<span class="spin"></span>' + L.verifying;
|
|
13170
13686
|
var hdr = { headers: { authorization: "Bearer " + token } };
|
|
13171
13687
|
// Prefer the renewal endpoint (managed mode returns the canonical token,
|
|
13172
13688
|
// also upgrading a pasted in-grace token). Fall back to a cheap
|
|
@@ -13177,7 +13693,7 @@ function unlockPage() {
|
|
|
13177
13693
|
.then(function (d) {
|
|
13178
13694
|
if (d && d.token) { save(d.token); location.reload(); return; }
|
|
13179
13695
|
return fetch("/api/version", hdr).then(function (r) {
|
|
13180
|
-
if (r.status === 401) { fail(
|
|
13696
|
+
if (r.status === 401) { fail(L.invalid); return; }
|
|
13181
13697
|
// 200 (static-token mode) or no UI (404) \u2192 trust it and reload.
|
|
13182
13698
|
save(token); location.reload();
|
|
13183
13699
|
});
|
|
@@ -13194,7 +13710,7 @@ function unlockPage() {
|
|
|
13194
13710
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
13195
13711
|
.then(function (d) {
|
|
13196
13712
|
if (d && d.token) { save(d.token); location.reload(); }
|
|
13197
|
-
else showForm(
|
|
13713
|
+
else showForm(L.expired);
|
|
13198
13714
|
})
|
|
13199
13715
|
.catch(function () { showForm(""); });
|
|
13200
13716
|
} else {
|
|
@@ -13322,8 +13838,9 @@ var COOKIE = "mh_token", COOKIE_ATTRS = "path=/; SameSite=Strict; Max-Age=315360
|
|
|
13322
13838
|
@keyframes spin{to{transform:rotate(360deg)}}
|
|
13323
13839
|
@media (prefers-reduced-motion:reduce){*{animation-duration:.01ms!important}
|
|
13324
13840
|
.brand path{stroke-dashoffset:0}}
|
|
13325
|
-
|
|
13841
|
+
`, escapeAttr = (v) => v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
13326
13842
|
var init_auth = __esm(() => {
|
|
13843
|
+
init_page_messages();
|
|
13327
13844
|
init_inject_runtime();
|
|
13328
13845
|
init_token();
|
|
13329
13846
|
init_pairing();
|
|
@@ -13353,7 +13870,9 @@ var init_http_status = __esm(() => {
|
|
|
13353
13870
|
stale: 409,
|
|
13354
13871
|
conflict: 409,
|
|
13355
13872
|
auth: 401,
|
|
13356
|
-
rate_limited: 429
|
|
13873
|
+
rate_limited: 429,
|
|
13874
|
+
busy: 503,
|
|
13875
|
+
clock_skew: 409
|
|
13357
13876
|
};
|
|
13358
13877
|
});
|
|
13359
13878
|
|
|
@@ -14132,7 +14651,7 @@ async function serveSite(req, ctx, auth, opts = {}) {
|
|
|
14132
14651
|
const ambientOnly = mutating && authActive(auth) && explicitToken(req, url) == null;
|
|
14133
14652
|
if (!ambientOnly && hasValidToken(req, url, auth)) {
|
|
14134
14653
|
if (!site)
|
|
14135
|
-
return
|
|
14654
|
+
return siteNotFound(req, name);
|
|
14136
14655
|
if (!opts.forwardApi)
|
|
14137
14656
|
return new Response("not found", { status: 404 });
|
|
14138
14657
|
const fwd = new Request(`${url.origin}/${filePath}${url.search}`, req);
|
|
@@ -14159,17 +14678,18 @@ async function serveSite(req, ctx, auth, opts = {}) {
|
|
|
14159
14678
|
return serveSiteFile(req, ctx.db, site.id, filePath, { spa: site.spa === 1, isPublic: true });
|
|
14160
14679
|
}
|
|
14161
14680
|
if (!hasValidToken(req, url, auth)) {
|
|
14162
|
-
return wantsHtml(req) ? new Response(unlockPage(), { headers: { ...HTML_HEADERS, "x-mh-unlock": "1" } }) : unauthorized();
|
|
14681
|
+
return wantsHtml(req) ? new Response(unlockPage(pickLocale(req)), { headers: { ...HTML_HEADERS, "x-mh-unlock": "1" } }) : unauthorized();
|
|
14163
14682
|
}
|
|
14164
14683
|
if (!site)
|
|
14165
|
-
return
|
|
14684
|
+
return siteNotFound(req, name);
|
|
14166
14685
|
const res = await serveSiteFile(req, ctx.db, site.id, filePath, { spa: site.spa === 1 });
|
|
14167
14686
|
return withShim(res, auth, req, url);
|
|
14168
14687
|
}
|
|
14169
14688
|
async function serveSiteFile(req, db, siteId, path, opts = {}) {
|
|
14689
|
+
const locale = pickLocale(req);
|
|
14170
14690
|
const meta = getFileMetaForServe(db, siteId, path, { spa: opts.spa });
|
|
14171
14691
|
if (!meta)
|
|
14172
|
-
return notFoundResponse("\u9875\u9762\u4E0D\u5B58\u5728", "\u8FD9\u4E2A\u7AD9\u70B9\u91CC\u6CA1\u6709\u8FD9\u4E2A\u9875\u9762\u6216\u6587\u4EF6\u3002");
|
|
14692
|
+
return notFoundResponse(locale, pm(locale, "\u9875\u9762\u4E0D\u5B58\u5728"), pm(locale, "\u8FD9\u4E2A\u7AD9\u70B9\u91CC\u6CA1\u6709\u8FD9\u4E2A\u9875\u9762\u6216\u6587\u4EF6\u3002"));
|
|
14173
14693
|
const cacheControl = siteCacheControl(meta.row.content_type, meta.row.encoding, opts.isPublic ?? false);
|
|
14174
14694
|
const headers = { etag: meta.etag, "cache-control": cacheControl };
|
|
14175
14695
|
if (etagMatches(req.headers.get("if-none-match"), meta.etag)) {
|
|
@@ -14177,7 +14697,7 @@ async function serveSiteFile(req, db, siteId, path, opts = {}) {
|
|
|
14177
14697
|
}
|
|
14178
14698
|
const bytes = await decodeFileRow(db, meta.row);
|
|
14179
14699
|
if (!bytes) {
|
|
14180
|
-
return notFoundResponse("\u8D44\u6E90\u6682\u4E0D\u53EF\u7528", "\u6587\u4EF6\u7684\u5185\u5BB9\u6682\u65F6\u65E0\u6CD5\u53D6\u5F97\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002");
|
|
14700
|
+
return notFoundResponse(locale, pm(locale, "\u8D44\u6E90\u6682\u4E0D\u53EF\u7528"), pm(locale, "\u6587\u4EF6\u7684\u5185\u5BB9\u6682\u65F6\u65E0\u6CD5\u53D6\u5F97\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002"));
|
|
14181
14701
|
}
|
|
14182
14702
|
const body = new Uint8Array(bytes.byteLength);
|
|
14183
14703
|
body.set(bytes);
|
|
@@ -14202,8 +14722,12 @@ function etagMatches(header, etag) {
|
|
|
14202
14722
|
}
|
|
14203
14723
|
return false;
|
|
14204
14724
|
}
|
|
14205
|
-
function
|
|
14206
|
-
const
|
|
14725
|
+
function siteNotFound(req, name) {
|
|
14726
|
+
const locale = pickLocale(req);
|
|
14727
|
+
return notFoundResponse(locale, pm(locale, "\u7AD9\u70B9\u4E0D\u5B58\u5728"), pm(locale, "\u6CA1\u6709\u540D\u4E3A \u201C{name}\u201D \u7684\u7AD9\u70B9\u3002", { name: escapeHtml(name) }));
|
|
14728
|
+
}
|
|
14729
|
+
function notFoundResponse(locale, title, detail) {
|
|
14730
|
+
const html = `<!doctype html><html lang="${locale}"><head>
|
|
14207
14731
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
14208
14732
|
<meta name="robots" content="noindex">
|
|
14209
14733
|
<title>${escapeHtml(title)}</title>
|
|
@@ -14224,8 +14748,8 @@ function notFoundResponse(title, detail) {
|
|
|
14224
14748
|
<p class="code">404</p>
|
|
14225
14749
|
<h1>${escapeHtml(title)}</h1>
|
|
14226
14750
|
<p>${detail}</p>
|
|
14227
|
-
<a class="home" href="./"
|
|
14228
|
-
<footer
|
|
14751
|
+
<a class="home" href="./">${escapeHtml(pm(locale, "\u8FD4\u56DE\u7AD9\u70B9\u9996\u9875"))}</a>
|
|
14752
|
+
<footer>${escapeHtml(pm(locale, "\u7531 metahub \u6258\u7BA1"))}</footer>
|
|
14229
14753
|
</main></body></html>`;
|
|
14230
14754
|
return new Response(html, {
|
|
14231
14755
|
status: 404,
|
|
@@ -14240,6 +14764,7 @@ var init_sites_serve = __esm(() => {
|
|
|
14240
14764
|
init_grants_routes();
|
|
14241
14765
|
init_rate_limit();
|
|
14242
14766
|
init_share_render();
|
|
14767
|
+
init_page_messages();
|
|
14243
14768
|
init_auth();
|
|
14244
14769
|
});
|
|
14245
14770
|
|
|
@@ -14294,6 +14819,51 @@ async function mintGuestSession(scope, opts) {
|
|
|
14294
14819
|
return { sub: opts.sub, exp, cookie };
|
|
14295
14820
|
}
|
|
14296
14821
|
|
|
14822
|
+
// src/core/sync/share-copy.ts
|
|
14823
|
+
function markdownTable(header, rows) {
|
|
14824
|
+
const cell = (s) => s.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").trim();
|
|
14825
|
+
const line = (cells) => `| ${cells.map(cell).join(" | ")} |`;
|
|
14826
|
+
return [line(header), `| ${header.map(() => "---").join(" | ")} |`, ...rows.map(line)].join(`
|
|
14827
|
+
`);
|
|
14828
|
+
}
|
|
14829
|
+
function copyButtonHtml(locale = "zh-CN") {
|
|
14830
|
+
const label = escapeHtml(pm(locale, "\u590D\u5236\u5168\u6587"));
|
|
14831
|
+
return `<button id="mh-copy" class="copy-btn" type="button" aria-label="${label}">${ICON_COPY}${ICON_CHECK}<span>${label}</span></button>`;
|
|
14832
|
+
}
|
|
14833
|
+
function copySourceHtml(text) {
|
|
14834
|
+
return `<template id="mh-src">${escapeHtml(text)}</template>`;
|
|
14835
|
+
}
|
|
14836
|
+
function copyScript(locale = "zh-CN") {
|
|
14837
|
+
const L = JSON.stringify({ idle: pm(locale, "\u590D\u5236\u5168\u6587"), done: pm(locale, "\u5DF2\u590D\u5236"), fail: pm(locale, "\u590D\u5236\u5931\u8D25") });
|
|
14838
|
+
return `(function(){
|
|
14839
|
+
var btn=document.getElementById('mh-copy');if(!btn)return;var L=${L};
|
|
14840
|
+
var label=btn.querySelector('span');var timer=0;
|
|
14841
|
+
function absolutize(root){root.querySelectorAll('img[src],a[href],video[src],audio[src]').forEach(function(el){var a=el.tagName==='A'?'href':'src';var v=el.getAttribute(a);if(v&&!/^(blob|data):/.test(v))el.setAttribute(a,new URL(v,location.href).href);});}
|
|
14842
|
+
function payload(){var src=document.getElementById('mh-src');var text=src?src.content.textContent:'';var body=document.querySelector('article.doc')||document.querySelector('.table-wrap');var html='';if(body){var c=body.cloneNode(true);absolutize(c);html=c.innerHTML;}return{text:text,html:html};}
|
|
14843
|
+
function legacy(text){var ta=document.createElement('textarea');ta.value=text;ta.setAttribute('readonly','');ta.style.cssText='position:fixed;top:0;left:0;opacity:0';document.body.appendChild(ta);ta.select();var ok=false;try{ok=document.execCommand('copy');}catch(e){}ta.remove();return ok?Promise.resolve():Promise.reject(new Error('copy'));}
|
|
14844
|
+
function write(p){var nc=navigator.clipboard;if(nc&&nc.write&&window.ClipboardItem){try{return nc.write([new ClipboardItem({'text/plain':new Blob([p.text],{type:'text/plain'}),'text/html':new Blob([p.html],{type:'text/html'})})]);}catch(e){}}if(nc&&nc.writeText)return nc.writeText(p.text);return legacy(p.text);}
|
|
14845
|
+
function settle(cls,txt){clearTimeout(timer);btn.classList.remove('done','fail');void btn.offsetWidth;btn.classList.add(cls);if(label)label.textContent=txt;timer=setTimeout(function(){btn.classList.remove(cls);if(label)label.textContent=L.idle;},1600);}
|
|
14846
|
+
btn.addEventListener('click',function(){var p=payload();write(p).then(function(){settle('done',L.done);},function(){legacy(p.text).then(function(){settle('done',L.done);},function(){settle('fail',L.fail);});});});
|
|
14847
|
+
})();`;
|
|
14848
|
+
}
|
|
14849
|
+
var ICON_COPY = '<svg class="ic-copy" viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>', ICON_CHECK = '<svg class="ic-check" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5 10 17l9-10"/></svg>', COPY_CSS = `.mh-tools{display:flex;align-items:center;gap:8px;flex:none}
|
|
14850
|
+
.copy-btn{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 12px;border:1px solid var(--line);border-radius:7px;background:var(--bg);color:var(--fg);font:500 13px/1 inherit;font-family:inherit;cursor:pointer;transition:background .12s,border-color .12s,color .12s}
|
|
14851
|
+
.copy-btn:hover{background:var(--card)}.copy-btn:active{transform:translateY(.5px)}
|
|
14852
|
+
.copy-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
14853
|
+
.copy-btn svg{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;flex:none}
|
|
14854
|
+
.copy-btn .ic-check{display:none}
|
|
14855
|
+
.copy-btn.done{color:var(--ok,#1a7f37);border-color:var(--ok,#1a7f37)}
|
|
14856
|
+
.copy-btn.done .ic-copy{display:none}.copy-btn.done .ic-check{display:block}
|
|
14857
|
+
.copy-btn.done .ic-check path{stroke-dasharray:24;stroke-dashoffset:24;animation:mh-draw .22s ease-out forwards}
|
|
14858
|
+
.copy-btn.fail{color:#cf222e;border-color:#cf222e}
|
|
14859
|
+
@keyframes mh-draw{to{stroke-dashoffset:0}}
|
|
14860
|
+
@media (prefers-color-scheme:dark){:root{--ok:#3fb950}}
|
|
14861
|
+
@media (max-width:480px){.copy-btn span{display:none}.copy-btn{padding:0 9px}}
|
|
14862
|
+
@media (prefers-reduced-motion:reduce){.copy-btn .ic-check path{animation:none;stroke-dashoffset:0}}`;
|
|
14863
|
+
var init_share_copy = __esm(() => {
|
|
14864
|
+
init_page_messages();
|
|
14865
|
+
});
|
|
14866
|
+
|
|
14297
14867
|
// src/core/sync/share-serve.ts
|
|
14298
14868
|
function notFound() {
|
|
14299
14869
|
return new Response("not found", { status: 404 });
|
|
@@ -14324,25 +14894,26 @@ async function serveShare(req, ctx, opts = {}) {
|
|
|
14324
14894
|
return notFound();
|
|
14325
14895
|
if (share.transport !== "server")
|
|
14326
14896
|
return notFound();
|
|
14897
|
+
const locale = pickLocale(req);
|
|
14327
14898
|
if (shareExpired(share))
|
|
14328
14899
|
return gone();
|
|
14329
14900
|
if (sub === "unlock" && req.method === "POST")
|
|
14330
14901
|
return handleUnlock(ctx, req, share, url);
|
|
14331
14902
|
const locked = !!share.pw_hash && !await readShareSession(ctx, req, share);
|
|
14332
14903
|
if (locked) {
|
|
14333
|
-
return wantsHtml2(req) ? new Response(passwordPage(share, false), { headers: HTML }) : unauthorizedJson();
|
|
14904
|
+
return wantsHtml2(req) ? new Response(passwordPage(locale, share, false), { headers: HTML }) : unauthorizedJson();
|
|
14334
14905
|
}
|
|
14335
14906
|
if (sub.startsWith("blob/"))
|
|
14336
14907
|
return serveScopedBlob(ctx, share, sub.slice("blob/".length));
|
|
14337
14908
|
if (share.kind === "doc")
|
|
14338
|
-
return serveDoc(ctx, req, share, sub);
|
|
14909
|
+
return serveDoc(ctx, req, share, sub, locale);
|
|
14339
14910
|
if (share.kind === "database")
|
|
14340
|
-
return serveTable(ctx, req, share, sub);
|
|
14911
|
+
return serveTable(ctx, req, share, sub, locale);
|
|
14341
14912
|
if (share.kind === "site")
|
|
14342
|
-
return serveSiteShare(ctx, req, share, sub, opts);
|
|
14913
|
+
return serveSiteShare(ctx, req, share, sub, opts, locale);
|
|
14343
14914
|
return notFound();
|
|
14344
14915
|
}
|
|
14345
|
-
async function serveDoc(ctx, req, share, sub) {
|
|
14916
|
+
async function serveDoc(ctx, req, share, sub, locale) {
|
|
14346
14917
|
const doc = getDocument(ctx.db, share.target_id);
|
|
14347
14918
|
if (!doc)
|
|
14348
14919
|
return notFound();
|
|
@@ -14371,26 +14942,27 @@ async function serveDoc(ctx, req, share, sub) {
|
|
|
14371
14942
|
});
|
|
14372
14943
|
const version = documentVersion(ctx.db, share.target_id);
|
|
14373
14944
|
const editable = share.permission === "edit";
|
|
14374
|
-
const inner = `<article class="doc">${rendered ||
|
|
14375
|
-
const script = editable ? docEditScript(share.slug, version, doc.body ?? "") : "";
|
|
14376
|
-
return new Response(pageShell(doc.title || "\u6587\u6863", inner, { editable, editLabel: "\u7F16\u8F91\u6587\u6863", script }), { headers: HTML });
|
|
14945
|
+
const inner = `<article class="doc">${rendered || `<p class="muted">${escapeHtml(pm(locale, "\uFF08\u7A7A\u6587\u6863\uFF09"))}</p>`}</article>`;
|
|
14946
|
+
const script = editable ? docEditScript(locale, share.slug, version, doc.body ?? "") : "";
|
|
14947
|
+
return new Response(pageShell(doc.title || pm(locale, "\u6587\u6863"), inner, { lang: locale, editable, editLabel: pm(locale, "\u7F16\u8F91\u6587\u6863"), script, copySource: doc.body ?? "" }), { headers: HTML });
|
|
14377
14948
|
}
|
|
14378
|
-
function docEditScript(slug, version, body) {
|
|
14949
|
+
function docEditScript(locale, slug, version, body) {
|
|
14950
|
+
const L = JSON.stringify({ save: pm(locale, "\u4FDD\u5B58"), saving: pm(locale, "\u4FDD\u5B58\u4E2D\u2026"), failed: pm(locale, "\u4FDD\u5B58\u5931\u8D25") });
|
|
14379
14951
|
return `
|
|
14380
|
-
let editing=false; const ver=${JSON.stringify(version)}; const body0=${JSON.stringify(body)};
|
|
14952
|
+
let editing=false; const ver=${JSON.stringify(version)}; const body0=${JSON.stringify(body)}; const L=${L};
|
|
14381
14953
|
const art=document.querySelector('article.doc');
|
|
14382
14954
|
const btn=document.getElementById('mh-edit');
|
|
14383
14955
|
let ta;
|
|
14384
14956
|
btn.addEventListener('click', async ()=>{
|
|
14385
14957
|
if(!editing){
|
|
14386
|
-
editing=true; btn.textContent=
|
|
14958
|
+
editing=true; btn.textContent=L.save;
|
|
14387
14959
|
ta=document.createElement('textarea'); ta.className='mh-raw'; ta.value=body0;
|
|
14388
14960
|
art.replaceWith(ta); ta.focus();
|
|
14389
14961
|
} else {
|
|
14390
|
-
btn.disabled=true; btn.textContent=
|
|
14962
|
+
btn.disabled=true; btn.textContent=L.saving;
|
|
14391
14963
|
const res=await fetch('/share/${slug}/doc',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({body:ta.value,ifMatch:ver})});
|
|
14392
14964
|
if(res.ok){ location.reload(); }
|
|
14393
|
-
else { const j=await res.json().catch(()=>({})); alert(j.error||
|
|
14965
|
+
else { const j=await res.json().catch(()=>({})); alert(j.error||L.failed); btn.disabled=false; btn.textContent=L.save; }
|
|
14394
14966
|
}
|
|
14395
14967
|
});`;
|
|
14396
14968
|
}
|
|
@@ -14419,7 +14991,7 @@ function cellHtml(prop, value, titles) {
|
|
|
14419
14991
|
function editableType(t) {
|
|
14420
14992
|
return t === "text" || t === "number" || t === "url" || t === "date";
|
|
14421
14993
|
}
|
|
14422
|
-
async function serveTable(ctx, req, share, sub) {
|
|
14994
|
+
async function serveTable(ctx, req, share, sub, locale) {
|
|
14423
14995
|
const dbRow = getDatabase(ctx.db, share.target_id);
|
|
14424
14996
|
if (!dbRow)
|
|
14425
14997
|
return notFound();
|
|
@@ -14470,13 +15042,18 @@ async function serveTable(ctx, req, share, sub) {
|
|
|
14470
15042
|
return `<tr>${tds}</tr>`;
|
|
14471
15043
|
}).join("");
|
|
14472
15044
|
const inner = `<div class="table-wrap"><table class="db"><thead><tr>${head}</tr></thead><tbody>${rows}</tbody></table></div>`;
|
|
14473
|
-
const script = editable ? tableEditScript(share.slug) : "";
|
|
14474
|
-
|
|
14475
|
-
|
|
14476
|
-
|
|
14477
|
-
|
|
14478
|
-
|
|
15045
|
+
const script = editable ? tableEditScript(locale, share.slug) : "";
|
|
15046
|
+
const copySource = markdownTable(props.map((p) => p.name), records.map((r) => props.map((p) => cellText(p, r.cells[p.id], titlesFor(p)))));
|
|
15047
|
+
return new Response(pageShell(dbRow.name || pm(locale, "\u8868\u683C"), inner, {
|
|
15048
|
+
lang: locale,
|
|
15049
|
+
script,
|
|
15050
|
+
copySource,
|
|
15051
|
+
hint: editable ? pm(locale, "\u53EF\u7F16\u8F91\u6587\u672C/\u6570\u5B57/URL/\u65E5\u671F\u5355\u5143\u683C\uFF0C\u5931\u7126\u81EA\u52A8\u4FDD\u5B58") : ""
|
|
15052
|
+
}), { headers: HTML });
|
|
15053
|
+
}
|
|
15054
|
+
function tableEditScript(locale, slug) {
|
|
14479
15055
|
return `
|
|
15056
|
+
const L=${JSON.stringify({ failed: pm(locale, "\u4FDD\u5B58\u5931\u8D25") })};
|
|
14480
15057
|
document.querySelectorAll('td[contenteditable]').forEach(td=>{
|
|
14481
15058
|
let orig=td.textContent;
|
|
14482
15059
|
td.addEventListener('blur', async ()=>{
|
|
@@ -14488,11 +15065,11 @@ function tableEditScript(slug) {
|
|
|
14488
15065
|
const res=await fetch('/share/${slug}/record',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({id:td.dataset.rec,propId:td.dataset.prop,value:v})});
|
|
14489
15066
|
td.classList.remove('saving');
|
|
14490
15067
|
if(res.ok){ orig=val; td.classList.add('saved'); setTimeout(()=>td.classList.remove('saved'),600); }
|
|
14491
|
-
else { td.textContent=orig; const j=await res.json().catch(()=>({})); alert(j.error||
|
|
15068
|
+
else { td.textContent=orig; const j=await res.json().catch(()=>({})); alert(j.error||L.failed); }
|
|
14492
15069
|
});
|
|
14493
15070
|
});`;
|
|
14494
15071
|
}
|
|
14495
|
-
async function serveSiteShare(ctx, req, share, sub, opts = {}) {
|
|
15072
|
+
async function serveSiteShare(ctx, req, share, sub, opts = {}, locale = "zh-CN") {
|
|
14496
15073
|
let site;
|
|
14497
15074
|
try {
|
|
14498
15075
|
site = resolveSite(ctx.db, share.target_id);
|
|
@@ -14515,7 +15092,7 @@ async function serveSiteShare(ctx, req, share, sub, opts = {}) {
|
|
|
14515
15092
|
}
|
|
14516
15093
|
if (share.permission === "edit") {
|
|
14517
15094
|
if (sub === "_files")
|
|
14518
|
-
return new Response(siteFilesPage(ctx, share, siteId), { headers: HTML });
|
|
15095
|
+
return new Response(siteFilesPage(ctx, share, siteId, locale), { headers: HTML });
|
|
14519
15096
|
if (sub === "_file" && req.method === "POST") {
|
|
14520
15097
|
const body = await req.json().catch(() => ({}));
|
|
14521
15098
|
if (!body.path || typeof body.content !== "string")
|
|
@@ -14547,34 +15124,35 @@ async function serveSiteShare(ctx, req, share, sub, opts = {}) {
|
|
|
14547
15124
|
}
|
|
14548
15125
|
return serveSiteFile(req, ctx.db, siteId, sub, { spa: site.spa === 1 });
|
|
14549
15126
|
}
|
|
14550
|
-
function siteFilesPage(ctx, share, siteId) {
|
|
15127
|
+
function siteFilesPage(ctx, share, siteId, locale) {
|
|
14551
15128
|
const files = listFiles(ctx.db, siteId);
|
|
15129
|
+
const L = JSON.stringify({ binary: pm(locale, "\uFF08\u4E8C\u8FDB\u5236\u6587\u4EF6\uFF0C\u4E0D\u53EF\u5728\u7EBF\u7F16\u8F91\uFF09"), saved: pm(locale, "\u5DF2\u4FDD\u5B58 \u2713"), save: pm(locale, "\u4FDD\u5B58"), failed: pm(locale, "\u4FDD\u5B58\u5931\u8D25") });
|
|
14552
15130
|
const list = files.map((f) => `<li><button class="file" data-path="${escapeHtml(f.path)}" data-text="${f.encoding === "utf8" ? "1" : "0"}">${escapeHtml(f.path)}</button> <span class="muted">${escapeHtml(f.content_type)}</span></li>`).join("");
|
|
14553
15131
|
const inner = `
|
|
14554
|
-
<p class="hint"
|
|
14555
|
-
<ul class="files">${list ||
|
|
15132
|
+
<p class="hint">${escapeHtml(pm(locale, "\u7F16\u8F91\u7AD9\u70B9\u6587\u4EF6\uFF08\u6587\u672C\u6587\u4EF6\u53EF\u5728\u7EBF\u7F16\u8F91\uFF1B\u4FDD\u5B58\u5373\u5BF9\u6240\u6709\u8BBF\u95EE\u8005\u751F\u6548\uFF09\u3002"))}</p>
|
|
15133
|
+
<ul class="files">${list || `<li class="muted">${escapeHtml(pm(locale, "\uFF08\u6682\u65E0\u6587\u4EF6\uFF09"))}</li>`}</ul>
|
|
14556
15134
|
<div id="editor" hidden>
|
|
14557
|
-
<div class="row"><strong id="fpath"></strong> <a id="open" target="_blank" rel="noreferrer"
|
|
15135
|
+
<div class="row"><strong id="fpath"></strong> <a id="open" target="_blank" rel="noreferrer">${escapeHtml(pm(locale, "\u9884\u89C8 \u2197"))}</a></div>
|
|
14558
15136
|
<textarea class="mh-raw" id="fbody"></textarea>
|
|
14559
|
-
<div class="row"><button id="save"
|
|
15137
|
+
<div class="row"><button id="save">${escapeHtml(pm(locale, "\u4FDD\u5B58"))}</button></div>
|
|
14560
15138
|
</div>`;
|
|
14561
15139
|
const script = `
|
|
14562
|
-
const slug=${JSON.stringify(share.slug)};
|
|
15140
|
+
const slug=${JSON.stringify(share.slug)}; const L=${L};
|
|
14563
15141
|
const ed=document.getElementById('editor'), fpath=document.getElementById('fpath'), fbody=document.getElementById('fbody'), open=document.getElementById('open');
|
|
14564
15142
|
let cur=null;
|
|
14565
15143
|
document.querySelectorAll('button.file').forEach(b=>b.addEventListener('click', async ()=>{
|
|
14566
15144
|
cur=b.dataset.path; fpath.textContent=cur; open.href='/share/'+slug+'/'+cur;
|
|
14567
15145
|
if(b.dataset.text==='1'){ const r=await fetch('/share/'+slug+'/_raw?path='+encodeURIComponent(cur)); const j=await r.json(); fbody.value=j.content||''; fbody.disabled=false; }
|
|
14568
|
-
else { fbody.value=
|
|
15146
|
+
else { fbody.value=L.binary; fbody.disabled=true; }
|
|
14569
15147
|
ed.hidden=false;
|
|
14570
15148
|
}));
|
|
14571
15149
|
document.getElementById('save').addEventListener('click', async ()=>{
|
|
14572
15150
|
if(!cur||fbody.disabled) return;
|
|
14573
15151
|
const res=await fetch('/share/'+slug+'/_file',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({path:cur,content:fbody.value})});
|
|
14574
|
-
if(res.ok){ const s=document.getElementById('save'); s.textContent=
|
|
14575
|
-
else { const j=await res.json().catch(()=>({})); alert(j.error||
|
|
15152
|
+
if(res.ok){ const s=document.getElementById('save'); s.textContent=L.saved; setTimeout(()=>s.textContent=L.save,900); }
|
|
15153
|
+
else { const j=await res.json().catch(()=>({})); alert(j.error||L.failed); }
|
|
14576
15154
|
});`;
|
|
14577
|
-
return pageShell(
|
|
15155
|
+
return pageShell(pm(locale, "{name} \xB7 \u6587\u4EF6", { name: resolveSite(ctx.db, share.target_id).name }), inner, { lang: locale, script });
|
|
14578
15156
|
}
|
|
14579
15157
|
function referencedHashes2(ctx, share) {
|
|
14580
15158
|
const out = new Set;
|
|
@@ -14662,7 +15240,7 @@ async function handleUnlock(ctx, req, share, url) {
|
|
|
14662
15240
|
pw = form ? String(form.get("password") ?? "") : "";
|
|
14663
15241
|
}
|
|
14664
15242
|
if (!await verifySharePassword(share, pw)) {
|
|
14665
|
-
return new Response(passwordPage(share, true), { headers: HTML, status: 401 });
|
|
15243
|
+
return new Response(passwordPage(pickLocale(req), share, true), { headers: HTML, status: 401 });
|
|
14666
15244
|
}
|
|
14667
15245
|
const { cookie } = await mintShareSession(ctx, share, url);
|
|
14668
15246
|
return new Response(null, {
|
|
@@ -14673,21 +15251,23 @@ async function handleUnlock(ctx, req, share, url) {
|
|
|
14673
15251
|
function ttlRemaining(share) {
|
|
14674
15252
|
return share.expires_at == null ? SHARE_SESSION_TTL_MS : Math.max(0, share.expires_at - Date.now());
|
|
14675
15253
|
}
|
|
14676
|
-
function passwordPage(share, error) {
|
|
15254
|
+
function passwordPage(locale, share, error) {
|
|
14677
15255
|
const inner = `
|
|
14678
15256
|
<form method="post" action="/share/${share.slug}/unlock" class="pw">
|
|
14679
|
-
<h1
|
|
14680
|
-
${error ?
|
|
14681
|
-
<input type="password" name="password" placeholder="\u53E3\u4EE4" autofocus autocomplete="current-password">
|
|
14682
|
-
<button type="submit"
|
|
15257
|
+
<h1>${escapeHtml(pm(locale, "\uD83D\uDD12 \u6B64\u5206\u4EAB\u53D7\u53E3\u4EE4\u4FDD\u62A4"))}</h1>
|
|
15258
|
+
${error ? `<p class="err">${escapeHtml(pm(locale, "\u53E3\u4EE4\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5\u3002"))}</p>` : ""}
|
|
15259
|
+
<input type="password" name="password" placeholder="${escapeHtml(pm(locale, "\u53E3\u4EE4"))}" autofocus autocomplete="current-password">
|
|
15260
|
+
<button type="submit">${escapeHtml(pm(locale, "\u89E3\u9501"))}</button>
|
|
14683
15261
|
</form>`;
|
|
14684
|
-
return pageShell("\u53D7\u4FDD\u62A4\u7684\u5206\u4EAB", inner, { bare: true });
|
|
15262
|
+
return pageShell(pm(locale, "\u53D7\u4FDD\u62A4\u7684\u5206\u4EAB"), inner, { lang: locale, bare: true });
|
|
14685
15263
|
}
|
|
14686
|
-
function pageShell(title, inner, opts
|
|
14687
|
-
const editBtn = opts.editable ? `<button id="mh-edit" class="edit-btn">${opts.editLabel ?? "\u7F16\u8F91"}</button>` : "";
|
|
15264
|
+
function pageShell(title, inner, opts) {
|
|
15265
|
+
const editBtn = opts.editable ? `<button id="mh-edit" class="edit-btn">${escapeHtml(opts.editLabel ?? pm(opts.lang, "\u7F16\u8F91"))}</button>` : "";
|
|
15266
|
+
const copyBtn = opts.copySource !== undefined ? copyButtonHtml(opts.lang) : "";
|
|
15267
|
+
const tools = copyBtn || editBtn ? `<div class="mh-tools">${copyBtn}${editBtn}</div>` : "";
|
|
14688
15268
|
const hint = opts.hint ? `<p class="hint">${escapeHtml(opts.hint)}</p>` : "";
|
|
14689
|
-
const script = opts.script ? `<script>(function(){${opts.script}})();</script>` : "";
|
|
14690
|
-
return `<!doctype html><html lang="
|
|
15269
|
+
const script = (opts.script ? `<script>(function(){${opts.script}})();</script>` : "") + (opts.copySource !== undefined ? `${copySourceHtml(opts.copySource)}<script>${copyScript(opts.lang)}</script>` : "");
|
|
15270
|
+
return `<!doctype html><html lang="${opts.lang}"><head>
|
|
14691
15271
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
14692
15272
|
<meta name="robots" content="noindex">
|
|
14693
15273
|
<title>${escapeHtml(title)}</title>
|
|
@@ -14699,7 +15279,8 @@ function pageShell(title, inner, opts = {}) {
|
|
|
14699
15279
|
.wrap{max-width:820px;margin:0 auto;padding:32px 20px 80px}
|
|
14700
15280
|
header.mh{display:flex;align-items:center;gap:12px;justify-content:space-between;margin-bottom:20px;border-bottom:1px solid var(--line);padding-bottom:14px}
|
|
14701
15281
|
header.mh h1.title{font-size:22px;margin:0}
|
|
14702
|
-
.edit-btn{background:var(--accent);color:#fff;border:0;border-radius:7px;padding:
|
|
15282
|
+
.edit-btn{background:var(--accent);color:#fff;border:0;border-radius:7px;height:32px;padding:0 14px;cursor:pointer;font-size:13px;font-weight:500;font-family:inherit}
|
|
15283
|
+
${COPY_CSS}
|
|
14703
15284
|
.hint{color:var(--muted);font-size:13px;margin:0 0 14px}
|
|
14704
15285
|
article.doc h1,article.doc h2,article.doc h3{line-height:1.3;margin:1.4em 0 .5em}
|
|
14705
15286
|
article.doc h1{font-size:1.7em} article.doc h2{font-size:1.4em} article.doc h3{font-size:1.2em}
|
|
@@ -14730,10 +15311,10 @@ function pageShell(title, inner, opts = {}) {
|
|
|
14730
15311
|
footer.mh{margin-top:48px;border-top:1px solid var(--line);padding-top:14px;color:var(--muted);font-size:12px;text-align:center}
|
|
14731
15312
|
</style></head><body>
|
|
14732
15313
|
${opts.bare ? `<div class="wrap">${inner}</div>` : `<div class="wrap">
|
|
14733
|
-
<header class="mh"><h1 class="title">${escapeHtml(title)}</h1>${
|
|
15314
|
+
<header class="mh"><h1 class="title">${escapeHtml(title)}</h1>${tools}</header>
|
|
14734
15315
|
${hint}
|
|
14735
15316
|
${inner}
|
|
14736
|
-
<footer class="mh"
|
|
15317
|
+
<footer class="mh">${escapeHtml(pm(opts.lang, "\u901A\u8FC7 metahub \u5206\u4EAB"))}</footer>
|
|
14737
15318
|
</div>`}
|
|
14738
15319
|
${script}
|
|
14739
15320
|
</body></html>`;
|
|
@@ -14756,6 +15337,8 @@ var init_share_serve = __esm(() => {
|
|
|
14756
15337
|
init_grants_routes();
|
|
14757
15338
|
init_rate_limit();
|
|
14758
15339
|
init_share_render();
|
|
15340
|
+
init_share_copy();
|
|
15341
|
+
init_page_messages();
|
|
14759
15342
|
HTML = { "content-type": "text/html; charset=utf-8" };
|
|
14760
15343
|
HASH_RE2 = /^[0-9a-f]{16,64}$/;
|
|
14761
15344
|
SHARE_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
|
|
@@ -15132,11 +15715,12 @@ var DROP_ENVELOPE_MAX_BYTES, DROP_HLC_SKEW_MS, GUEST_NODE_RE;
|
|
|
15132
15715
|
var init_drop_protocol = __esm(() => {
|
|
15133
15716
|
init_errors();
|
|
15134
15717
|
init_ids();
|
|
15718
|
+
init_hlc();
|
|
15135
15719
|
init_grants_core();
|
|
15136
15720
|
init_e2ee();
|
|
15137
15721
|
init_seal();
|
|
15138
15722
|
DROP_ENVELOPE_MAX_BYTES = 64 * 1024;
|
|
15139
|
-
DROP_HLC_SKEW_MS =
|
|
15723
|
+
DROP_HLC_SKEW_MS = HLC_MAX_SKEW_MS;
|
|
15140
15724
|
GUEST_NODE_RE = /^g[0-9a-z]{8}$/;
|
|
15141
15725
|
});
|
|
15142
15726
|
|
|
@@ -15190,12 +15774,12 @@ function applyDropV2(db, set, payload, now) {
|
|
|
15190
15774
|
};
|
|
15191
15775
|
const before = db.query("SELECT MAX(seq) AS s FROM crdt_changes").get().s ?? 0;
|
|
15192
15776
|
const ids = [];
|
|
15193
|
-
db
|
|
15777
|
+
writeTx(db, () => {
|
|
15194
15778
|
for (const intent of payload.intents) {
|
|
15195
15779
|
applyGuestIntent(db, policy, { guestNode: payload.guest_node }, intent, { clock: "submitted", now });
|
|
15196
15780
|
ids.push(intent.intentId);
|
|
15197
15781
|
}
|
|
15198
|
-
})
|
|
15782
|
+
});
|
|
15199
15783
|
const ingested = db.query("SELECT COUNT(*) AS n FROM crdt_changes WHERE seq > ?").get(before).n;
|
|
15200
15784
|
return { ackSeq: intentsMaxSeq(db, payload.guest_node, ids), ingested };
|
|
15201
15785
|
}
|
|
@@ -15373,24 +15957,48 @@ init_paths();
|
|
|
15373
15957
|
// src/core/db.ts
|
|
15374
15958
|
init_paths();
|
|
15375
15959
|
init_schema_init();
|
|
15960
|
+
init_node();
|
|
15376
15961
|
init_schema_init();
|
|
15377
15962
|
import { Database } from "bun:sqlite";
|
|
15378
15963
|
import { mkdirSync } from "fs";
|
|
15964
|
+
import { hostname } from "os";
|
|
15379
15965
|
function ensureDirs() {
|
|
15380
15966
|
mkdirSync(metahubHome(), { recursive: true });
|
|
15381
15967
|
mkdirSync(cacheDir(), { recursive: true });
|
|
15382
15968
|
}
|
|
15969
|
+
function appKind() {
|
|
15970
|
+
const v = process.env.METAHUB_APP;
|
|
15971
|
+
return v === "server" || v === "desktop" || v === "web" ? v : "cli";
|
|
15972
|
+
}
|
|
15383
15973
|
function openMetahub() {
|
|
15384
15974
|
ensureDirs();
|
|
15385
15975
|
const db = new Database(dbPath(), { create: true });
|
|
15976
|
+
db.exec("PRAGMA busy_timeout = 5000;");
|
|
15386
15977
|
db.exec("PRAGMA journal_mode = WAL;");
|
|
15387
15978
|
initSchema(db);
|
|
15979
|
+
const app = appKind();
|
|
15980
|
+
const platform = platformFromProcess(process.platform);
|
|
15981
|
+
describeSelf(db, {
|
|
15982
|
+
app,
|
|
15983
|
+
platform,
|
|
15984
|
+
form: defaultForm(platform, app),
|
|
15985
|
+
defaultLabel: safeHostname()
|
|
15986
|
+
});
|
|
15388
15987
|
return db;
|
|
15389
15988
|
}
|
|
15989
|
+
function safeHostname() {
|
|
15990
|
+
try {
|
|
15991
|
+
const h = hostname();
|
|
15992
|
+
return h ? h.replace(/\.local$/i, "") : null;
|
|
15993
|
+
} catch {
|
|
15994
|
+
return null;
|
|
15995
|
+
}
|
|
15996
|
+
}
|
|
15390
15997
|
|
|
15391
15998
|
// src/core/index.ts
|
|
15392
15999
|
init_ids();
|
|
15393
16000
|
init_resolve();
|
|
16001
|
+
init_hlc();
|
|
15394
16002
|
init_crdt();
|
|
15395
16003
|
init_node();
|
|
15396
16004
|
init_databases();
|
|
@@ -15517,18 +16125,18 @@ function ensureIndex(db) {
|
|
|
15517
16125
|
return false;
|
|
15518
16126
|
const version = readMeta(db, "search_index_version");
|
|
15519
16127
|
const seq = readMeta(db, "search_seq");
|
|
15520
|
-
db
|
|
16128
|
+
writeTx(db, () => {
|
|
15521
16129
|
if (version !== SEARCH_INDEX_VERSION || seq === null)
|
|
15522
16130
|
fullRebuild(db);
|
|
15523
16131
|
else
|
|
15524
16132
|
incrementalUpdate(db);
|
|
15525
|
-
})
|
|
16133
|
+
});
|
|
15526
16134
|
return true;
|
|
15527
16135
|
}
|
|
15528
16136
|
function rebuildSearchIndex(db) {
|
|
15529
16137
|
if (!ftsAvailable(db))
|
|
15530
16138
|
return false;
|
|
15531
|
-
db
|
|
16139
|
+
writeTx(db, () => fullRebuild(db));
|
|
15532
16140
|
return true;
|
|
15533
16141
|
}
|
|
15534
16142
|
function ftsSearch(db, query, limit) {
|
|
@@ -15677,6 +16285,7 @@ function scalarHtml(specUrl) {
|
|
|
15677
16285
|
}
|
|
15678
16286
|
function startServer(opts = {}) {
|
|
15679
16287
|
registerRoomBlobResolver(resolveBlob);
|
|
16288
|
+
process.env.METAHUB_APP ??= "server";
|
|
15680
16289
|
const tDb = Date.now();
|
|
15681
16290
|
const db = openMetahub();
|
|
15682
16291
|
const dbMs = Date.now() - tDb;
|
|
@@ -15687,6 +16296,7 @@ function startServer(opts = {}) {
|
|
|
15687
16296
|
allowRemoteSiteHosting: opts.allowRemoteSiteHosting ?? true
|
|
15688
16297
|
};
|
|
15689
16298
|
const allRoutes = opts.ui ? [...routes, ...opts.ui.routes] : routes;
|
|
16299
|
+
opts.ui?.onStart?.(db);
|
|
15690
16300
|
const forwardApi = async (req) => {
|
|
15691
16301
|
const path = new URL(req.url).pathname;
|
|
15692
16302
|
const route = allRoutes.find((r) => r.method === req.method && r.path === path);
|
|
@@ -15732,7 +16342,7 @@ function startServer(opts = {}) {
|
|
|
15732
16342
|
} else {
|
|
15733
16343
|
const exempt = url.pathname === HEALTH_PATH || url.pathname === RENEW_PATH || url.pathname === PAIR_PATH || url.pathname === "/manifest.webmanifest" || url.pathname === "/sw.js" || url.pathname === "/metahub-sdk.js" || url.pathname.startsWith("/icons/") || url.pathname.startsWith("/share/") || url.pathname.startsWith("/sites/");
|
|
15734
16344
|
if (!exempt && !hasValidToken(req, url, auth)) {
|
|
15735
|
-
return wantsHtml(req) ? new Response(unlockPage(), { headers: { ...HTML_HEADERS, "x-mh-unlock": "1" } }) : unauthorized();
|
|
16345
|
+
return wantsHtml(req) ? new Response(unlockPage(pickLocale(req)), { headers: { ...HTML_HEADERS, "x-mh-unlock": "1" } }) : unauthorized();
|
|
15736
16346
|
}
|
|
15737
16347
|
}
|
|
15738
16348
|
if (url.pathname === RENEW_PATH) {
|
|
@@ -15899,6 +16509,8 @@ var styles_default = ` :root {
|
|
|
15899
16509
|
--fs-body:14px; --fs-ui:13.5px; --fs-sm:12.5px; --fs-xs:11.5px; --fs-micro:11px;
|
|
15900
16510
|
--ui:"Hanken Grotesk",-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
|
|
15901
16511
|
--mono:"JetBrains Mono",ui-monospace,SFMono-Regular,monospace;
|
|
16512
|
+
--kbd-font:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;
|
|
16513
|
+
--tip-bg:#1f1f23; --tip-fg:#ffffff; --tip-kbd-bg:rgba(255,255,255,.14); --tip-kbd-fg:rgba(255,255,255,.92);
|
|
15902
16514
|
--note-lh:1.6;
|
|
15903
16515
|
--code-bg:#f8f8f6;
|
|
15904
16516
|
--hl-cmt:#9a9aa2; --hl-key:#a626a4; --hl-str:#28823f; --hl-num:#b76a00;
|
|
@@ -15917,6 +16529,7 @@ var styles_default = ` :root {
|
|
|
15917
16529
|
--hover:rgba(255,255,255,.05); --hover-2:rgba(255,255,255,.09);
|
|
15918
16530
|
--shadow-sm:0 1px 2px rgba(0,0,0,.4); --shadow-md:0 4px 14px rgba(0,0,0,.45); --shadow-lg:0 20px 56px rgba(0,0,0,.6);
|
|
15919
16531
|
--code-bg:#1d1d20;
|
|
16532
|
+
--tip-bg:#2e2e32; --tip-fg:var(--fg); --tip-kbd-bg:rgba(255,255,255,.08); --tip-kbd-fg:var(--fg-soft);
|
|
15920
16533
|
--hl-cmt:#7d7d86; --hl-key:#d291e4; --hl-str:#7ed492; --hl-num:#e0a566;
|
|
15921
16534
|
--hl-fn:#86a8ff; --hl-type:#5fc6e0; --hl-var:#f0936b;
|
|
15922
16535
|
}
|
|
@@ -15995,12 +16608,11 @@ var styles_default = ` :root {
|
|
|
15995
16608
|
background:linear-gradient(135deg,var(--accent),color-mix(in srgb,var(--accent) 60%,#fff));
|
|
15996
16609
|
display:grid; place-items:center; color:#fff; box-shadow:var(--shadow-sm); }
|
|
15997
16610
|
.brand .mark svg { width:15px; height:15px; stroke:#fff; }
|
|
15998
|
-
.sb-search { margin:0 12px 8px; display:flex; align-items:center; gap:7px; cursor:text;
|
|
16611
|
+
.sb-search { margin:0 12px 8px; width:calc(100% - 24px); display:flex; align-items:center; gap:7px; cursor:pointer; text-align:left;
|
|
15999
16612
|
background:var(--bg); border:1px solid var(--line); border-radius:var(--radius-sm); padding:6px 9px; color:var(--muted);
|
|
16000
|
-
box-shadow:var(--shadow-sm); transition:border-color .15s; }
|
|
16001
|
-
.sb-search:focus-
|
|
16002
|
-
.sb-search
|
|
16003
|
-
.sb-search kbd { font-family:var(--mono); font-size:10px; color:var(--muted); border:1px solid var(--line-strong); border-radius:4px; padding:1px 4px; }
|
|
16613
|
+
box-shadow:var(--shadow-sm); transition:border-color .15s; font:inherit; }
|
|
16614
|
+
.sb-search:hover, .sb-search:focus-visible { border-color:var(--line-strong); outline:none; }
|
|
16615
|
+
.sb-search-ph { flex:1; min-width:0; color:var(--muted); font-size:var(--fs-ui); }
|
|
16004
16616
|
.sb-scroll { flex:1; overflow-y:auto; padding:4px 8px 24px; }
|
|
16005
16617
|
/* Notion-style list switcher: the active tab is an icon+label pill, inactive
|
|
16006
16618
|
tabs are icon-only. One absolutely-positioned indicator slides & stretches
|
|
@@ -16171,9 +16783,34 @@ var styles_default = ` :root {
|
|
|
16171
16783
|
.set-row-line { display:flex; align-items:center; gap:14px; min-height:44px; padding:5px 0; }
|
|
16172
16784
|
.set-row.clickable > .set-row-line { cursor:pointer; }
|
|
16173
16785
|
.set-row-text { flex:1 1 auto; min-width:0; }
|
|
16174
|
-
.set-row-title { font-size:14px; font-weight:550; color:var(--fg); }
|
|
16175
|
-
.set-row-
|
|
16786
|
+
.set-row-title { font-size:14px; font-weight:550; color:var(--fg); display:flex; align-items:center; gap:8px; min-width:0; }
|
|
16787
|
+
.set-row-title > .set-row-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
16788
|
+
.set-row-caption { font-size:12px; color:var(--muted); margin-top:1px; display:flex; align-items:center; gap:6px;
|
|
16176
16789
|
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
16790
|
+
/* leading glyph (device form / bucket) \u2014 a soft tile so rows scan by shape */
|
|
16791
|
+
.set-row-lead { flex:none; width:28px; height:28px; display:grid; place-items:center; color:var(--muted);
|
|
16792
|
+
border-radius:7px; background:var(--surface-2); }
|
|
16793
|
+
.set-row-lead .ico { width:15px; height:15px; }
|
|
16794
|
+
.set-row.dim > .set-row-line { opacity:.62; }
|
|
16795
|
+
/* status dot in a caption: the only colour a row carries */
|
|
16796
|
+
.cap-dot { display:inline-block; flex:none; width:7px; height:7px; border-radius:50%; background:var(--muted); }
|
|
16797
|
+
.cap-dot.ok { background:var(--success); } .cap-dot.warn { background:var(--warning,#9a6700); } .cap-dot.err { background:var(--danger); }
|
|
16798
|
+
.cap-err { color:var(--danger); }
|
|
16799
|
+
/* tag variants next to a title: neutral outline (default), accent (roles), self (soft fill) */
|
|
16800
|
+
.set-row-title .set-row-tag { margin-left:0; vertical-align:0; flex:none; }
|
|
16801
|
+
.set-row-tag.accent { color:var(--accent); background:var(--accent-soft); border-color:transparent; }
|
|
16802
|
+
.set-row-tag.self { color:var(--fg-soft); background:var(--surface-2); border-color:transparent; }
|
|
16803
|
+
.set-row-tag.mono { font-family:var(--mono); letter-spacing:-.01em; }
|
|
16804
|
+
/* quiet "+ add" row closing a list, and a "N more\u2026" fold */
|
|
16805
|
+
.add-row { display:flex; align-items:center; gap:8px; width:100%; min-height:40px; margin-top:2px; padding:0 2px;
|
|
16806
|
+
color:var(--muted); font-size:var(--fs-ui); border-radius:var(--radius-sm); text-align:left; }
|
|
16807
|
+
.add-row:hover { color:var(--fg); background:var(--hover); }
|
|
16808
|
+
.add-row .ico { width:15px; height:15px; }
|
|
16809
|
+
.more-row { display:inline-flex; align-items:center; gap:6px; padding:8px 2px; font-size:var(--fs-sm); color:var(--muted); }
|
|
16810
|
+
.more-row:hover { color:var(--fg); }
|
|
16811
|
+
.set-section-head { display:flex; align-items:baseline; gap:10px; margin:0 0 4px 2px; }
|
|
16812
|
+
.set-section-head .set-section-label { margin:0; }
|
|
16813
|
+
.set-section-count { font-size:12px; color:var(--muted); font-variant-numeric:tabular-nums; }
|
|
16177
16814
|
.set-row-control { flex:none; display:flex; align-items:center; gap:8px; }
|
|
16178
16815
|
.set-row-detail { padding:2px 0 12px; }
|
|
16179
16816
|
.set-row.disabled > .set-row-line { opacity:.55; pointer-events:none; }
|
|
@@ -16189,6 +16826,9 @@ var styles_default = ` :root {
|
|
|
16189
16826
|
.set-row-tag { display:inline-block; margin-left:8px; font-size:11px; font-weight:500;
|
|
16190
16827
|
color:var(--muted); border:1px solid var(--line); border-radius:999px; padding:1px 7px; vertical-align:1px; }
|
|
16191
16828
|
.set-row-input { width:15em; min-width:0; }
|
|
16829
|
+
/* row dropdown (primitives.tsx RowSelect): current value + chevron */
|
|
16830
|
+
.row-select { gap:4px; padding:4px 6px 4px 10px; }
|
|
16831
|
+
.row-select .ico { width:13px; height:13px; color:var(--muted); }
|
|
16192
16832
|
|
|
16193
16833
|
/* Page chrome (primitives.tsx PageHeader / DangerZone) \u2014 every page opens
|
|
16194
16834
|
with its own PageHeader (the shell renders no title). */
|
|
@@ -16197,7 +16837,7 @@ var styles_default = ` :root {
|
|
|
16197
16837
|
.page-header-text { flex:1 1 auto; min-width:0; }
|
|
16198
16838
|
.page-header-title { font-size:26px; font-weight:700; letter-spacing:-.02em; color:var(--fg); margin:0; }
|
|
16199
16839
|
.page-header-sub { color:var(--muted); font-size:var(--fs-sm); margin-top:3px; }
|
|
16200
|
-
.page-header-action { flex:none; padding-top:4px; }
|
|
16840
|
+
.page-header-action { flex:none; display:flex; align-items:center; gap:8px; padding-top:4px; }
|
|
16201
16841
|
.page-header-banner { margin-top:8px; }
|
|
16202
16842
|
.danger-zone { margin-top:34px; }
|
|
16203
16843
|
.danger-zone-label { font-size:12px; font-weight:600; text-transform:uppercase; letter-spacing:.07em;
|
|
@@ -16238,15 +16878,14 @@ var styles_default = ` :root {
|
|
|
16238
16878
|
.audit-detail { padding:0 4px 10px 86px; }
|
|
16239
16879
|
.audit-detail-ent-name { font-size:var(--fs-sm); font-weight:600; margin-top:6px; }
|
|
16240
16880
|
.audit-blocks { font-size:var(--fs-sm); margin-top:2px; }
|
|
16241
|
-
|
|
16242
|
-
.audit-
|
|
16881
|
+
/* quiet tail: left-aligned text button while more exists; a rule once exhausted */
|
|
16882
|
+
.audit-more { display:inline-flex; align-items:center; gap:6px; margin:14px 0 0 2px; font-size:var(--fs-sm); color:var(--muted); }
|
|
16883
|
+
.audit-more:hover { color:var(--fg); }
|
|
16884
|
+
.audit-more.loading { opacity:.6; pointer-events:none; }
|
|
16885
|
+
.audit-end { display:flex; align-items:center; gap:12px; margin:18px 0 0; color:var(--muted); font-size:var(--fs-xs); }
|
|
16886
|
+
.audit-end::before, .audit-end::after { content:""; flex:1; height:1px; background:var(--line); }
|
|
16243
16887
|
@media (hover:none) { .audit-revert { opacity:1; } }
|
|
16244
16888
|
.set-callout.warn { border-color:color-mix(in srgb,var(--warning,#9a6700) 32%,var(--line)); }
|
|
16245
|
-
/* two-fact meta line under a block title (who publishes \xB7 where creds live) \u2014
|
|
16246
|
-
answers the questions hiding under "is this bucket for server or browser?" */
|
|
16247
|
-
.set-meta { display:flex; flex-wrap:wrap; gap:8px 16px; margin:0 0 12px; }
|
|
16248
|
-
.set-meta-item { display:inline-flex; align-items:center; gap:6px; font-size:var(--fs-sm); color:var(--fg-soft); }
|
|
16249
|
-
.set-meta-item svg { width:14px; height:14px; color:var(--accent); }
|
|
16250
16889
|
/* a quiet helper line under a field / chip group */
|
|
16251
16890
|
.set-hint { font-size:var(--fs-xs); color:var(--muted); line-height:1.5; margin:3px 0 4px; }
|
|
16252
16891
|
/* progressive disclosure ("\u5DE5\u4F5C\u539F\u7406" / "\u8FDB\u9636") \u2014 native details, themed */
|
|
@@ -16331,58 +16970,39 @@ var styles_default = ` :root {
|
|
|
16331
16970
|
.peer-sub { font-size:12px; color:var(--muted); margin-top:2px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
16332
16971
|
.peer-menu { flex:none; padding:6px; }
|
|
16333
16972
|
|
|
16334
|
-
/*
|
|
16335
|
-
|
|
16336
|
-
.sync-health { margin:0; }
|
|
16337
|
-
.sync-health-line { display:inline-flex; align-items:center; gap:8px; max-width:100%; padding:2px 0;
|
|
16338
|
-
background:none; border:0; cursor:pointer; text-align:left; font:inherit; color:var(--fg-soft); }
|
|
16339
|
-
.sync-health-line:hover { color:var(--fg); }
|
|
16340
|
-
.sync-health-line:hover .sync-health-more { color:var(--fg); }
|
|
16973
|
+
/* \u6570\u636E\u4E0E\u5907\u4EFD health sentence (PageHeader sub): the dot alone carries tone */
|
|
16974
|
+
.sync-health-sub { display:inline-flex; align-items:center; gap:8px; max-width:100%; min-width:0; }
|
|
16341
16975
|
.sync-health-dot { flex:none; width:8px; height:8px; border-radius:50%; }
|
|
16342
16976
|
.sync-health-dot.dot-ok { background:var(--success); }
|
|
16343
16977
|
.sync-health-dot.dot-warn { background:var(--warning,#9a6700); }
|
|
16344
16978
|
.sync-health-dot.dot-error { background:var(--danger); }
|
|
16345
|
-
|
|
16346
|
-
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
16347
|
-
.sync-health-more { flex:none; font-size:var(--fs-xs); color:var(--muted); }
|
|
16348
|
-
/* shared expander chevron (SyncHealthLine \u8BE6\u60C5, device roster rows) */
|
|
16979
|
+
/* shared expander chevron (bucket + device roster rows) */
|
|
16349
16980
|
.row-chev { flex:none; color:var(--muted); display:flex; transition:transform .15s; }
|
|
16350
16981
|
.row-chev .ico { width:14px; height:14px; }
|
|
16351
16982
|
.row-chev.open { transform:rotate(180deg); }
|
|
16352
16983
|
/* confirm dialogs: keep \\n line breaks (consequence lists) */
|
|
16353
16984
|
.cfm-msg { white-space: pre-line; }
|
|
16354
|
-
|
|
16355
|
-
|
|
16356
|
-
|
|
16357
|
-
.
|
|
16358
|
-
.
|
|
16359
|
-
.
|
|
16360
|
-
.
|
|
16361
|
-
|
|
16362
|
-
.
|
|
16363
|
-
|
|
16364
|
-
.
|
|
16365
|
-
|
|
16366
|
-
.
|
|
16367
|
-
|
|
16368
|
-
|
|
16369
|
-
/* unified device roster (DevicesPanel) */
|
|
16370
|
-
.device-detail { padding:2px 12px 10px 28px; display:flex; flex-direction:column; gap:4px; }
|
|
16371
|
-
.device-detail-line { display:flex; align-items:center; gap:8px; font-size:12px; min-width:0; }
|
|
16372
|
-
.device-ch-label { flex:none; color:var(--muted); border:1px solid var(--line); border-radius:999px; padding:1px 7px; font-size:11px; }
|
|
16373
|
-
.device-ch-ref { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--fg); }
|
|
16374
|
-
.device-ch-actions { margin-left:auto; display:inline-flex; gap:2px; flex:none; }
|
|
16375
|
-
.device-ch-actions .btn { padding:2px 8px; font-size:12px; }
|
|
16376
|
-
.device-danger { color:var(--danger); }
|
|
16377
|
-
.device-detail-note { font-size:12px; color:var(--muted); background:var(--surface-2);
|
|
16378
|
-
border-radius:8px; padding:8px 10px; margin-top:4px; line-height:1.6; }
|
|
16985
|
+
|
|
16986
|
+
/* device roster (settings/devices-page.tsx): expanded detail = key/value
|
|
16987
|
+
table + one action row + one plain sentence \u2014 no gray callout box. */
|
|
16988
|
+
.device-kv { display:grid; grid-template-columns:max-content 1fr; gap:6px 18px; padding:4px 0 10px 42px; font-size:var(--fs-sm); margin:0; }
|
|
16989
|
+
.device-kv dt { color:var(--muted); margin:0; }
|
|
16990
|
+
.device-kv dd { margin:0; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
16991
|
+
.device-kv .mono { font-family:var(--mono); font-size:var(--fs-xs); letter-spacing:-.01em; }
|
|
16992
|
+
.device-actions { display:flex; align-items:center; gap:4px; padding-left:42px; flex-wrap:wrap; }
|
|
16993
|
+
.device-actions .btn { padding:3px 8px; font-size:12px; }
|
|
16994
|
+
.device-actions .btn.danger { color:var(--danger); }
|
|
16995
|
+
.device-actions .btn.danger:hover { background:var(--danger-soft); color:var(--danger); }
|
|
16996
|
+
.device-note { font-size:12px; color:var(--muted); line-height:1.6; padding:4px 0 6px 42px; max-width:60ch; }
|
|
16997
|
+
.device-note b { color:var(--fg-soft); font-weight:600; }
|
|
16998
|
+
.device-note a { color:var(--accent); text-decoration:none; }
|
|
16999
|
+
.device-note a:hover { text-decoration:underline; }
|
|
16379
17000
|
|
|
16380
17001
|
/* cloud accessories panel (CloudPanel: bucket + edge in one glance) */
|
|
16381
17002
|
.cloud-cta { border:1px solid var(--line); border-radius:10px; padding:14px 16px; margin:4px 0 12px;
|
|
16382
17003
|
display:grid; grid-template-columns:1fr auto; gap:4px 12px; align-items:center; background:var(--surface-1); }
|
|
16383
17004
|
.cloud-cta-title { font-weight:650; font-size:var(--fs-body); }
|
|
16384
17005
|
.cloud-cta-sub { font-size:12px; color:var(--muted); margin-top:2px; line-height:1.6; }
|
|
16385
|
-
.cloud-cta-alt { grid-column:1 / -1; font-size:12px; }
|
|
16386
17006
|
|
|
16387
17007
|
/* printable recovery card */
|
|
16388
17008
|
.recovery-card { border:2px solid var(--fg); border-radius:12px; padding:16px 18px; margin-top:8px; background:var(--surface); }
|
|
@@ -16429,6 +17049,8 @@ var styles_default = ` :root {
|
|
|
16429
17049
|
.crumb-link { flex:none; padding:2px 5px; margin:-2px -3px; border-radius:var(--radius-sm); font-weight:500; }
|
|
16430
17050
|
.crumb-link:hover { background:var(--hover); }
|
|
16431
17051
|
.crumb-sep { flex:none; color:var(--muted); font-weight:400; }
|
|
17052
|
+
.crumb-title { flex:0 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
17053
|
+
.crumb-more { color:var(--muted); }
|
|
16432
17054
|
.iconbtn { width:30px; height:30px; display:grid; place-items:center; border-radius:var(--radius-sm); color:var(--fg-soft); transition:background .12s; }
|
|
16433
17055
|
.iconbtn:hover { background:var(--hover-2); color:var(--fg); }
|
|
16434
17056
|
/* Dead end of the history (topbar back/forward). pointer-events:none also
|
|
@@ -16450,6 +17072,7 @@ var styles_default = ` :root {
|
|
|
16450
17072
|
.btn-primary { background:var(--accent); color:var(--accent-fg); box-shadow:var(--shadow-sm); }
|
|
16451
17073
|
.btn-primary:hover { filter:brightness(1.07); }
|
|
16452
17074
|
.btn-danger { background:var(--danger); color:#fff; } .btn-danger:hover { filter:brightness(1.05); }
|
|
17075
|
+
.btn.sm { padding:3px 8px; font-size:12px; }
|
|
16453
17076
|
/* Notion-style ambient sync indicator: a quiet ghost control living in the
|
|
16454
17077
|
topbar icon row. State reads through the *text/icon colour* \u2014 no filled pill
|
|
16455
17078
|
or drop shadow \u2014 and only the error state escalates to a visible danger tone.
|
|
@@ -16479,6 +17102,26 @@ var styles_default = ` :root {
|
|
|
16479
17102
|
@keyframes save-pop { 0% { transform:scale(.7); } 100% { transform:scale(1); } }
|
|
16480
17103
|
@keyframes save-shake { 10%,90% { transform:translateX(-1px); } 20%,80% { transform:translateX(1.5px); } 30%,50%,70% { transform:translateX(-2px); } 40%,60% { transform:translateX(2px); } }
|
|
16481
17104
|
@keyframes save-kbd-pulse { 0% { transform:scale(1); } 45% { transform:scale(1.05); } 100% { transform:scale(1); } }
|
|
17105
|
+
/* Ambient replica-sync indicator (sync-indicator.tsx): same ghost-control
|
|
17106
|
+
idiom as .share-save \u2014 zero footprint when idle, colour carries state,
|
|
17107
|
+
only error escalates to danger. Lives in .sb-footer (desktop), .sb-head
|
|
17108
|
+
(mobile home) and the topbar (sidebar collapsed). */
|
|
17109
|
+
.sync-ind { display:inline-flex; align-items:center; gap:6px; height:28px; padding:0 8px; border-radius:var(--radius-sm);
|
|
17110
|
+
font-size:var(--fs-xs); color:var(--fg-soft); white-space:nowrap; font-variant-numeric:tabular-nums; flex:none;
|
|
17111
|
+
animation:save-rise .22s ease both; }
|
|
17112
|
+
.sync-ind:hover { background:var(--hover-2); color:var(--fg); }
|
|
17113
|
+
.sync-ind-ico { display:inline-flex; align-items:center; }
|
|
17114
|
+
.sync-ring { width:12px; height:12px; border-radius:50%; border:1.5px solid var(--line-strong);
|
|
17115
|
+
border-top-color:var(--accent); animation:mh-spin .8s linear infinite; }
|
|
17116
|
+
.sync-ind-done { color:var(--success); }
|
|
17117
|
+
.sync-ind-done .sync-ind-ico { animation:save-pop .24s cubic-bezier(.18,.9,.24,1.22) both; }
|
|
17118
|
+
.sync-ind-error { color:var(--danger); }
|
|
17119
|
+
.sync-ind-error:hover { background:var(--danger-soft); color:var(--danger); }
|
|
17120
|
+
/* footer: push right, version stays flush right after it */
|
|
17121
|
+
.sb-footer .sync-ind { margin-left:auto; }
|
|
17122
|
+
.sb-footer .sync-ind + .sbf-ver { margin-left:8px; }
|
|
17123
|
+
/* head slot is mobile-only (mirrors the .sb-head .iconbtn gating) */
|
|
17124
|
+
.sb-head .sync-ind { display:none; }
|
|
16482
17125
|
|
|
16483
17126
|
/* macOS desktop (hiddenInset frameless): drop the sidebar logo and reserve a
|
|
16484
17127
|
draggable strip so the inset traffic lights never overlap our controls.
|
|
@@ -16501,7 +17144,7 @@ var styles_default = ` :root {
|
|
|
16501
17144
|
/* document editor */
|
|
16502
17145
|
.doc { max-width:740px; margin:0 auto; padding:60px 60px 36vh; }
|
|
16503
17146
|
.doc-title { font-size:38px; font-weight:700; letter-spacing:-.02em; outline:none; line-height:1.15; margin-bottom:4px; }
|
|
16504
|
-
.doc-title:empty::before { content:
|
|
17147
|
+
.doc-title:empty::before { content:attr(data-placeholder); color:var(--muted); }
|
|
16505
17148
|
/* Backstop for the plain-text titles (see plain-edit.ts): should markup ever
|
|
16506
17149
|
slip into one of these hosts, its inline font-size can't shrink the title. */
|
|
16507
17150
|
.doc-title *, .db-title * { font:inherit; color:inherit; background:none; }
|
|
@@ -16991,11 +17634,16 @@ var styles_default = ` :root {
|
|
|
16991
17634
|
.db-icon { width:38px; height:38px; border-radius:9px; background:var(--surface-2); display:grid; place-items:center; font-size:20px; }
|
|
16992
17635
|
.db-title { font-size:27px; font-weight:700; letter-spacing:-.02em; outline:none; }
|
|
16993
17636
|
.db-desc { color:var(--muted); font-size:13px; margin-top:2px; }
|
|
16994
|
-
.views { display:flex; align-items:center; gap:2px; border-bottom:1px solid var(--line); margin:16px 0 0; }
|
|
16995
|
-
.
|
|
16996
|
-
|
|
17637
|
+
.views { display:flex; align-items:center; gap:2px; border-bottom:1px solid var(--line); margin:16px 0 0; overflow-x:auto; scrollbar-width:none; }
|
|
17638
|
+
.views::-webkit-scrollbar { display:none; }
|
|
17639
|
+
.view-tab { display:flex; align-items:center; gap:6px; padding:8px 9px; font-size:var(--fs-ui); font-weight:500; color:var(--muted); cursor:pointer;
|
|
17640
|
+
border-bottom:2px solid transparent; margin-bottom:-1px; transition:color .12s; white-space:nowrap; border-radius:var(--radius-sm) var(--radius-sm) 0 0; }
|
|
16997
17641
|
.view-tab.active { color:var(--fg); border-bottom-color:var(--accent); }
|
|
16998
|
-
.view-tab:hover { color:var(--fg); } .view-tab svg { width:14px; height:14px; }
|
|
17642
|
+
.view-tab:hover { color:var(--fg); background:var(--hover); } .view-tab svg { width:14px; height:14px; }
|
|
17643
|
+
.view-tab .caret { width:12px; height:12px; color:var(--muted); margin-left:-2px; opacity:0; transition:opacity .12s; }
|
|
17644
|
+
.view-tab.active:hover .caret { opacity:1; }
|
|
17645
|
+
.view-add { width:28px; height:28px; display:grid; place-items:center; color:var(--muted); border-radius:var(--radius-sm); margin-left:2px; flex:none; }
|
|
17646
|
+
.view-add:hover { background:var(--hover-2); color:var(--fg); }
|
|
16999
17647
|
/* \u6D6E\u7A97\u770B\u677F (right end of the toolbar / board bar, desktop only): a quiet
|
|
17000
17648
|
Notion-style secondary pill \u2014 visible as a feature entry, never louder
|
|
17001
17649
|
than \u65B0\u5EFA. Hover lifts the pill and nudges the icon's top kanban card up
|
|
@@ -17011,12 +17659,56 @@ var styles_default = ` :root {
|
|
|
17011
17659
|
.popout-btn:active { transform:translateY(0); box-shadow:var(--shadow-sm); }
|
|
17012
17660
|
.popout-btn .i-card { transition:transform .18s ease; }
|
|
17013
17661
|
.popout-btn:hover .i-card { transform:translateY(-2px); }
|
|
17014
|
-
.toolbar { display:flex; align-items:center; gap:2px; padding:
|
|
17662
|
+
.toolbar { display:flex; align-items:center; gap:2px; padding:8px 0 6px; flex-wrap:wrap; }
|
|
17015
17663
|
.toolbar .spacer { flex:1; }
|
|
17016
|
-
.tbtn { display:flex; align-items:center; gap:6px; padding:5px 9px; border-radius:var(--radius-sm); color:var(--fg-soft); font-size:13px; font-weight:500; }
|
|
17664
|
+
.tbtn { display:flex; align-items:center; gap:6px; padding:5px 9px; border-radius:var(--radius-sm); color:var(--fg-soft); font-size:13px; font-weight:500; white-space:nowrap; }
|
|
17017
17665
|
.tbtn:hover { background:var(--hover-2); color:var(--fg); }
|
|
17018
17666
|
.tbtn.on { color:var(--accent); }
|
|
17019
|
-
.
|
|
17667
|
+
.tbtn.on:hover { background:var(--accent-soft); }
|
|
17668
|
+
.tbtn.icon { padding:5px 7px; }
|
|
17669
|
+
.tb-n { display:inline-grid; place-items:center; min-width:17px; height:17px; padding:0 5px; border-radius:999px; background:var(--accent-soft); color:var(--accent);
|
|
17670
|
+
font-size:var(--fs-micro); font-weight:700; font-variant-numeric:tabular-nums; }
|
|
17671
|
+
.tb-search { display:flex; align-items:center; gap:6px; padding:4px 8px; margin-left:2px; border:1px solid var(--line-strong); border-radius:var(--radius-sm); background:var(--bg);
|
|
17672
|
+
color:var(--muted); min-width:220px; transition:border-color .12s, box-shadow .12s; animation:pal-in .13s ease; }
|
|
17673
|
+
.tb-search:focus-within { border-color:var(--accent); color:var(--accent); box-shadow:0 0 0 2.5px color-mix(in srgb, var(--accent) 16%, transparent); }
|
|
17674
|
+
.tb-search input { flex:1; width:0; min-width:0; border:0; outline:none; background:transparent; font:inherit; font-size:13px; color:var(--fg); }
|
|
17675
|
+
.tb-search .clear { display:grid; place-items:center; width:18px; height:18px; border-radius:50%; color:var(--muted); flex:none; }
|
|
17676
|
+
.tb-search .clear:hover { background:var(--hover-2); color:var(--fg); }
|
|
17677
|
+
/* active filter / sort chips under the toolbar (Notion's rule row) */
|
|
17678
|
+
.tb-chips { display:flex; align-items:center; flex-wrap:wrap; gap:6px; padding:0 0 10px; }
|
|
17679
|
+
.chip-rule { display:inline-flex; align-items:center; gap:5px; padding:3px 6px 3px 8px; border:1px solid var(--line-strong); border-radius:999px; background:var(--bg);
|
|
17680
|
+
font-size:var(--fs-sm); color:var(--fg-soft); max-width:360px; transition:border-color .12s, background .12s; }
|
|
17681
|
+
.chip-rule:hover { border-color:var(--accent); background:var(--accent-soft); }
|
|
17682
|
+
.chip-rule > svg { width:13px; height:13px; color:var(--muted); flex:none; }
|
|
17683
|
+
.chip-rule .k { font-weight:600; color:var(--fg); white-space:nowrap; }
|
|
17684
|
+
.chip-rule .v { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
17685
|
+
.chip-rule .x { display:grid; place-items:center; width:16px; height:16px; border-radius:50%; color:var(--muted); margin-left:2px; }
|
|
17686
|
+
.chip-rule .x:hover { background:var(--hover-2); color:var(--fg); }
|
|
17687
|
+
.chip-rule.op { color:var(--accent); border-style:dashed; padding:3px 9px; font-weight:600; }
|
|
17688
|
+
.chip-rule.sort .k::before { content:""; }
|
|
17689
|
+
/* filter rule editor popover */
|
|
17690
|
+
.fr { min-width:272px; }
|
|
17691
|
+
.fr .lbl { display:flex; align-items:center; gap:6px; text-transform:none; letter-spacing:0; font-size:var(--fs-sm); color:var(--fg); }
|
|
17692
|
+
.fr-conds { display:flex; flex-wrap:wrap; gap:4px; padding:2px 4px 8px; }
|
|
17693
|
+
.fr-cond { padding:3px 9px; border-radius:999px; border:1px solid var(--line); font-size:var(--fs-sm); color:var(--fg-soft); background:var(--bg); white-space:nowrap; }
|
|
17694
|
+
.fr-cond:hover { background:var(--hover-2); color:var(--fg); }
|
|
17695
|
+
.fr-cond.on { background:var(--accent); border-color:var(--accent); color:var(--accent-fg); }
|
|
17696
|
+
.fr .dp { padding:0; }
|
|
17697
|
+
.fr-refs { display:flex; flex-wrap:wrap; gap:4px; padding:4px 8px 6px; font-size:var(--fs-sm); }
|
|
17698
|
+
.pop input.field { font:inherit; font-size:var(--fs-ui); color:var(--fg); }
|
|
17699
|
+
/* sort rows + property visibility rows in their popovers */
|
|
17700
|
+
.sortrow .sortname { flex:1; display:flex; align-items:center; gap:6px; min-width:0; font-size:var(--fs-ui); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
17701
|
+
.sortrow .sortname svg { color:var(--muted); flex:none; }
|
|
17702
|
+
.sortrow .sortdir { display:inline-flex; align-items:center; gap:4px; padding:3px 7px; border-radius:var(--radius-sm); font-size:var(--fs-sm); color:var(--fg-soft); white-space:nowrap; }
|
|
17703
|
+
.sortrow .sortdir:hover { background:var(--hover-2); color:var(--fg); }
|
|
17704
|
+
.flipv { transform:scaleY(-1); }
|
|
17705
|
+
.proprow-v .optlabel { gap:6px; align-items:center; font-size:var(--fs-ui); color:var(--fg); }
|
|
17706
|
+
.proprow-v .optlabel svg { color:var(--muted); flex:none; }
|
|
17707
|
+
.proprow-v .optlabel .nm { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
17708
|
+
.proprow-v.off .optlabel { color:var(--muted); }
|
|
17709
|
+
.proprow-v .x:disabled { opacity:.35; cursor:default; }
|
|
17710
|
+
.proprow-v .x:disabled:hover { background:none; color:var(--muted); }
|
|
17711
|
+
.tablewrap { border-top:1px solid var(--line); border-bottom:1px solid var(--line); }
|
|
17020
17712
|
.tablescroll { overflow-x:auto; }
|
|
17021
17713
|
/* fixed layout + max-content: each <col> width is authoritative, so the table
|
|
17022
17714
|
is exactly the sum of its columns and resizing one column never redistributes
|
|
@@ -17025,12 +17717,26 @@ var styles_default = ` :root {
|
|
|
17025
17717
|
table.grid { border-collapse:collapse; table-layout:fixed; width:max-content; min-width:100%; }
|
|
17026
17718
|
table.grid th, table.grid td { border-right:1px solid var(--line); border-bottom:1px solid var(--line); padding:0; text-align:left; }
|
|
17027
17719
|
table.grid th:last-child, table.grid td:last-child { border-right:0; }
|
|
17028
|
-
table.grid tbody tr:last-child td { border-bottom:
|
|
17029
|
-
thead th { background:var(--
|
|
17030
|
-
.colhead { display:flex; align-items:center; gap:7px; padding:
|
|
17031
|
-
.colhead:hover { background:var(--hover); }
|
|
17032
|
-
.colhead .ti { color:var(--muted); } .colhead .ti svg { width:14px; height:14px; }
|
|
17720
|
+
table.grid tbody tr:last-child td { border-bottom:1px solid var(--line); }
|
|
17721
|
+
thead th { background:var(--bg); position:relative; user-select:none; font-weight:500; color:var(--muted); }
|
|
17722
|
+
.colhead { display:flex; align-items:center; gap:7px; padding:8px 11px; font-size:13px; cursor:pointer; min-height:36px; }
|
|
17723
|
+
.colhead:hover { background:var(--hover); color:var(--fg-soft); }
|
|
17724
|
+
.colhead .ti { color:var(--muted); display:flex; } .colhead .ti svg { width:14px; height:14px; }
|
|
17033
17725
|
.colhead .nm { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
17726
|
+
.colhead .sortmark { width:12px; height:12px; color:var(--accent); flex:none; }
|
|
17727
|
+
.colhead .caret { width:12px; height:12px; color:var(--muted); flex:none; opacity:0; transition:opacity .12s; }
|
|
17728
|
+
.colhead:hover .caret { opacity:1; }
|
|
17729
|
+
table.grid.nowrap .cell { flex-wrap:nowrap; overflow:hidden; }
|
|
17730
|
+
table.grid.nowrap .cell .txt { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
17731
|
+
table.grid tfoot td { border-right:0; border-bottom:0; background:var(--bg); }
|
|
17732
|
+
table.grid tfoot td.calc { height:32px; cursor:pointer; padding:0 11px; font-size:var(--fs-xs); color:var(--muted); text-align:right; white-space:nowrap; user-select:none; }
|
|
17733
|
+
table.grid tfoot td.calc:hover { background:var(--hover); }
|
|
17734
|
+
table.grid tfoot td.calc .k { display:inline-flex; align-items:center; gap:2px; }
|
|
17735
|
+
table.grid tfoot td.calc .k.ghost { opacity:0; transition:opacity .12s; }
|
|
17736
|
+
table.grid tfoot td.calc .k.ghost svg { width:11px; height:11px; }
|
|
17737
|
+
table.grid tfoot:hover td.calc .k.ghost { opacity:.75; }
|
|
17738
|
+
table.grid tfoot td.calc.on .k { margin-right:6px; }
|
|
17739
|
+
table.grid tfoot td.calc.on .v { color:var(--fg); font-weight:600; font-variant-numeric:tabular-nums; }
|
|
17034
17740
|
.col-resizer { position:absolute; top:0; right:-4px; width:7px; height:100%; cursor:col-resize; z-index:2; touch-action:none; }
|
|
17035
17741
|
.addcol { min-width:46px; } .addcol .colhead { justify-content:flex-start; color:var(--muted); }
|
|
17036
17742
|
/* trailing filler column: empty canvas to the right of the data, not a real cell */
|
|
@@ -17038,19 +17744,24 @@ var styles_default = ` :root {
|
|
|
17038
17744
|
body.col-resizing { user-select:none; cursor:col-resize; }
|
|
17039
17745
|
table.grid .selcell { width:38px; min-width:38px; max-width:38px; text-align:center; }
|
|
17040
17746
|
/* relative: anchors the .celledit overlay editor to the cell */
|
|
17041
|
-
td.cell-td { position:relative; vertical-align:top; }
|
|
17747
|
+
td.cell-td { position:relative; vertical-align:top; user-select:none; }
|
|
17748
|
+
td.cell-td .celledit, td.cell-td .celledit * { user-select:text; }
|
|
17042
17749
|
.cell { padding:8px 11px; min-height:37px; cursor:text; word-break:break-word; display:flex; flex-wrap:wrap; gap:3px; align-items:center; }
|
|
17043
17750
|
/* hover/active tint goes on the <td> (always full row height) not the inner
|
|
17044
17751
|
.cell (only as tall as its content), so a taller sibling row never leaves a
|
|
17045
17752
|
gap of un-tinted space at the bottom of the hovered cell. */
|
|
17046
17753
|
td.cell-td { transition:background .1s; }
|
|
17047
|
-
|
|
17754
|
+
.cell.center { justify-content:center; }
|
|
17048
17755
|
tbody tr { transition:background .08s; }
|
|
17049
17756
|
tbody tr:hover { background:var(--hover); }
|
|
17050
17757
|
tbody tr.sel { background:var(--accent-soft); }
|
|
17051
17758
|
/* cell range selection: per-cell fill; the accent rectangle outline is drawn via inline box-shadow on edge cells. Placed after :hover so it wins on equal specificity. */
|
|
17052
17759
|
td.cell-td.cellsel, td.cell-td.cellsel:hover { background:var(--accent-soft); }
|
|
17053
17760
|
body.cell-selecting { user-select:none; cursor:cell; }
|
|
17761
|
+
td.cell-td.fillsel { background:var(--accent-soft); box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent); }
|
|
17762
|
+
.cell-fill-handle { position:absolute; right:-4px; bottom:-4px; width:8px; height:8px; border-radius:2px; background:var(--accent);
|
|
17763
|
+
border:1.5px solid var(--bg); cursor:crosshair; z-index:4; touch-action:none; }
|
|
17764
|
+
.cell-fill-handle::after { content:""; position:absolute; inset:-6px; }
|
|
17054
17765
|
body.cell-selecting .cell { cursor:cell; }
|
|
17055
17766
|
/* column reorder still marks the target <th>; rows use the .rowdrop line below */
|
|
17056
17767
|
table.grid th.drop-before { box-shadow:inset 2px 0 0 0 var(--accent); }
|
|
@@ -17106,23 +17817,57 @@ var styles_default = ` :root {
|
|
|
17106
17817
|
/* data-resolved (not prefers-color-scheme): tracks a manual dark choice too. */
|
|
17107
17818
|
:root[data-resolved="dark"] .chip { background:color-mix(in srgb,var(--c) 22%,transparent); color:color-mix(in srgb,var(--c) 75%,#fff); }
|
|
17108
17819
|
/* relation chip: an <a> that jumps to the target record */
|
|
17109
|
-
a.chip.rel { text-decoration:none; cursor:pointer; }
|
|
17820
|
+
a.chip.rel { display:inline-block; max-width:100%; overflow:hidden; text-overflow:ellipsis; text-decoration:none; cursor:pointer; }
|
|
17110
17821
|
a.chip.rel:hover { background:color-mix(in srgb,var(--c) 26%,transparent); text-decoration:underline; text-decoration-style:dashed; text-underline-offset:2px; }
|
|
17111
17822
|
/* doc chip whose target is deleted/unsynced: muted + dashed, same cue as the
|
|
17112
17823
|
editor's .cm-doclink-missing pill */
|
|
17113
17824
|
a.chip.rel.doc-missing { background:transparent; color:var(--muted); outline:1px dashed var(--line-strong); outline-offset:-1px; }
|
|
17114
|
-
.addrow { display:flex; align-items:center; gap:7px; padding:
|
|
17825
|
+
.addrow { display:flex; align-items:center; gap:7px; padding:8px 12px; color:var(--muted); font-size:13px; cursor:pointer; border-radius:var(--radius-sm); margin-top:2px; }
|
|
17115
17826
|
.addrow:hover { background:var(--hover); color:var(--fg-soft); }
|
|
17827
|
+
.addrow svg { width:14px; height:14px; }
|
|
17116
17828
|
.gridfoot { display:flex; gap:18px; color:var(--muted); font-size:12px; padding:8px 2px; }
|
|
17117
|
-
|
|
17829
|
+
.inlineedit { width:100%; border:0; outline:2px solid var(--accent); border-radius:0; padding:7px 9px; background:var(--bg); color:var(--fg); font:inherit; }
|
|
17830
|
+
textarea.inlineedit { resize:none; overflow:hidden; line-height:1.45; display:block; }
|
|
17831
|
+
.inlineedit.num { text-align:right; font-variant-numeric:tabular-nums; }
|
|
17118
17832
|
/* Overlay cell editor: floats over the td; the display content underneath
|
|
17119
17833
|
stays in flow so the row height never changes while editing. Padding on the
|
|
17120
|
-
input matches .cell (8px 11px) so the text doesn't shift on entering edit
|
|
17834
|
+
input matches .cell (8px 11px) so the text doesn't shift on entering edit;
|
|
17835
|
+
a multi-line text editor grows downward over the rows below. */
|
|
17121
17836
|
.celledit { position:absolute; top:0; left:0; width:100%; min-height:100%; z-index:10;
|
|
17122
|
-
display:flex; align-items:stretch; background:var(--bg); border-radius:
|
|
17123
|
-
box-shadow:0 0 0 2px var(--accent), var(--shadow-md); }
|
|
17124
|
-
.celledit
|
|
17837
|
+
display:flex; align-items:stretch; background:var(--bg); border-radius:0;
|
|
17838
|
+
box-shadow:inset 0 0 0 2px var(--accent), var(--shadow-md); }
|
|
17839
|
+
.celledit .inlineedit { width:100%; min-width:0; border:0; outline:0; border-radius:inherit;
|
|
17125
17840
|
padding:8px 11px; background:transparent; }
|
|
17841
|
+
.celledit textarea.inlineedit { min-height:37px; }
|
|
17842
|
+
.cell.num { justify-content:flex-end; font-variant-numeric:tabular-nums; }
|
|
17843
|
+
.cell .txt { white-space:pre-wrap; }
|
|
17844
|
+
.cell-url { display:inline-flex; align-items:center; gap:4px; max-width:100%; min-width:0; color:var(--accent); text-decoration:none; }
|
|
17845
|
+
.cell-url span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
17846
|
+
.cell-url svg { flex:none; width:12px; height:12px; opacity:0; transition:opacity .1s; }
|
|
17847
|
+
.cell-url:hover span { text-decoration:underline; text-underline-offset:2px; }
|
|
17848
|
+
td.cell-td:hover .cell-url svg { opacity:.7; }
|
|
17849
|
+
/* loading skeleton: the real grid's cells, sheen bars for content */
|
|
17850
|
+
table.grid.skel { pointer-events:none; }
|
|
17851
|
+
table.grid.skel .skel-t { display:block; height:13px; width:calc(38% + var(--i, 0) * 11%); border-radius:4px; }
|
|
17852
|
+
table.grid.skel th .skel-t { height:12px; width:calc(30% + var(--i, 0) * 9%); }
|
|
17853
|
+
/* date picker (date-picker.tsx) */
|
|
17854
|
+
.dp { padding:2px; }
|
|
17855
|
+
.dp .selsearch.invalid { border-color:var(--danger); }
|
|
17856
|
+
.dp-nav { display:flex; align-items:center; gap:4px; padding:2px 2px 6px; }
|
|
17857
|
+
.dp-nav .iconbtn.sm { width:26px; height:26px; }
|
|
17858
|
+
.dp-title { flex:1; text-align:center; font-size:var(--fs-ui); font-weight:600; color:var(--fg); }
|
|
17859
|
+
.dp-grid { display:grid; grid-template-columns:repeat(7, 32px); gap:2px; justify-content:center; }
|
|
17860
|
+
.dp-head span { text-align:center; font-size:var(--fs-micro); color:var(--muted); font-weight:600; padding:2px 0 4px; }
|
|
17861
|
+
.dp-day { width:32px; height:30px; border-radius:var(--radius-sm); font-size:var(--fs-sm); color:var(--fg); display:grid; place-items:center;
|
|
17862
|
+
font-variant-numeric:tabular-nums; }
|
|
17863
|
+
.dp-day.dim { color:var(--muted); opacity:.55; }
|
|
17864
|
+
.dp-day:hover { background:var(--hover-2); }
|
|
17865
|
+
.dp-day.focus { box-shadow:inset 0 0 0 1.5px var(--accent); }
|
|
17866
|
+
.dp-day.today { font-weight:700; color:var(--accent); }
|
|
17867
|
+
.dp-day.picked { background:var(--accent); color:var(--accent-fg); }
|
|
17868
|
+
.dp-day.picked:hover { background:var(--accent); }
|
|
17869
|
+
.dp-foot { display:flex; align-items:center; gap:2px; padding:8px 2px 2px; margin-top:4px; border-top:1px solid var(--line); }
|
|
17870
|
+
.dp-hint { margin-left:auto; font-size:var(--fs-xs); color:var(--muted); padding-right:4px; }
|
|
17126
17871
|
|
|
17127
17872
|
/* ---- sites (\u7AD9\u70B9\u7BA1\u7406) ---- */
|
|
17128
17873
|
.sites-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(264px, 1fr)); gap:14px; margin-top:4px; }
|
|
@@ -17168,55 +17913,148 @@ var styles_default = ` :root {
|
|
|
17168
17913
|
.site-empty .et { font-size:15px; color:var(--fg); margin-bottom:5px; font-weight:650; letter-spacing:-.01em; }
|
|
17169
17914
|
.site-empty .ed { font-size:var(--fs-sm); margin-bottom:18px; max-width:42ch; line-height:1.55; }
|
|
17170
17915
|
|
|
17171
|
-
/* \u2500\u2500 \u5206\u4EAB (global ShareView) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
17172
|
-
|
|
17173
|
-
|
|
17916
|
+
/* \u2500\u2500 \u5206\u4EAB (global ShareView) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
17917
|
+
One row = object \xB7 status \xB7 one primary action \xB7 overflow. State is the
|
|
17918
|
+
organizing idea: colour lives only in the status dot and the \u53EF\u7F16\u8F91 word;
|
|
17919
|
+
expiry is a share's normal ending and reads muted, never danger. */
|
|
17920
|
+
.shares-page .db-head { align-items:flex-start; gap:16px; margin-bottom:6px; }
|
|
17921
|
+
.shares-page .db-desc { max-width:56ch; }
|
|
17922
|
+
.shv-toolbar { display:flex; align-items:center; gap:10px; flex-wrap:nowrap; padding:10px 0; margin-bottom:8px; }
|
|
17923
|
+
.shv-tools-right { display:flex; align-items:center; gap:10px; flex:none; margin-left:auto; }
|
|
17174
17924
|
.shv-seg { display:inline-flex; background:var(--surface-2); border:1px solid var(--line); border-radius:var(--radius-sm); padding:2px; }
|
|
17175
|
-
.shv-seg-btn { padding:5px 11px; border-radius:calc(var(--radius-sm) - 2px); font-size:13px; color:var(--fg-soft); }
|
|
17925
|
+
.shv-seg-btn { display:inline-flex; align-items:baseline; padding:5px 11px; border-radius:calc(var(--radius-sm) - 2px); font-size:13px; color:var(--fg-soft); }
|
|
17176
17926
|
.shv-seg-btn.active { background:var(--bg); color:var(--fg); box-shadow:var(--shadow-sm); }
|
|
17177
|
-
.shv-
|
|
17178
|
-
.shv-
|
|
17927
|
+
.shv-seg-btn .n { display:inline-flex; justify-content:flex-end; align-items:center; min-width:2ch; margin-left:5px; font-family:var(--mono); font-size:var(--fs-micro); color:var(--muted); font-variant-numeric:tabular-nums; }
|
|
17928
|
+
.shv-seg-btn .skel-n { width:2ch; height:.85em; border-radius:3px; animation:skel-in .25s ease .12s both; }
|
|
17929
|
+
.shv-seg-btn.active .n { color:var(--fg-soft); }
|
|
17930
|
+
.shv-src { gap:5px; color:var(--fg-soft); }
|
|
17931
|
+
.shv-src.on { color:var(--fg); }
|
|
17932
|
+
.shv-src .ico { width:14px; height:14px; opacity:.7; }
|
|
17933
|
+
.shv-search { display:flex; align-items:center; gap:6px; flex:1 1 220px; min-width:140px; max-width:360px; background:var(--bg); border:1px solid var(--line-strong); border-radius:var(--radius-sm); padding:5px 10px; color:var(--muted); }
|
|
17934
|
+
.shv-search:focus-within { border-color:var(--accent); }
|
|
17935
|
+
.shv-search input { border:0; background:none; outline:none; color:var(--fg); font-size:13px; width:100%; min-width:0; }
|
|
17179
17936
|
|
|
17180
17937
|
.shv-list { display:flex; flex-direction:column; gap:8px; list-style:none; padding:0; margin:0; }
|
|
17181
|
-
.shv-row { display:
|
|
17938
|
+
.shv-row { display:grid; grid-template-columns:38px minmax(0,1fr) auto auto; gap:6px 13px; align-items:center;
|
|
17939
|
+
padding:12px 15px; background:var(--bg); border:1px solid var(--line);
|
|
17182
17940
|
border-radius:var(--radius-lg); transition:border-color .12s, box-shadow .12s; }
|
|
17183
17941
|
.shv-row:hover { border-color:var(--line-strong); box-shadow:var(--shadow-sm); }
|
|
17184
17942
|
.shv-ico { width:38px; height:38px; flex:none; display:grid; place-items:center; border-radius:var(--radius-md, 10px);
|
|
17185
17943
|
background:var(--surface-2); color:var(--fg-soft); }
|
|
17186
17944
|
.shv-ico svg { width:19px; height:19px; }
|
|
17187
17945
|
.shv-ico.k-doc { color:var(--accent); background:var(--accent-soft); }
|
|
17946
|
+
.shv-ico.k-database { color:var(--fg-soft); }
|
|
17188
17947
|
.shv-ico.k-site { color:#1a7f5a; }
|
|
17189
|
-
.shv-main {
|
|
17190
|
-
.shv-
|
|
17191
|
-
|
|
17948
|
+
.shv-main { min-width:0; }
|
|
17949
|
+
.shv-title { display:block; max-width:100%; font-weight:600; color:var(--fg); letter-spacing:-.01em;
|
|
17950
|
+
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:left; }
|
|
17192
17951
|
.shv-title:hover { color:var(--accent); }
|
|
17193
|
-
.shv-
|
|
17194
|
-
|
|
17195
|
-
.shv-
|
|
17196
|
-
.shv-
|
|
17197
|
-
.shv-
|
|
17198
|
-
.shv-
|
|
17199
|
-
|
|
17952
|
+
.shv-sub { display:flex; align-items:center; gap:6px; margin-top:3px; min-width:0; font-size:var(--fs-sm); color:var(--muted);
|
|
17953
|
+
white-space:nowrap; overflow:hidden; }
|
|
17954
|
+
.shv-sub > * { flex:none; }
|
|
17955
|
+
.shv-sub > .shv-link { flex:0 1 auto; }
|
|
17956
|
+
.shv-link-text { min-width:0; overflow:hidden; text-overflow:ellipsis; }
|
|
17957
|
+
.shv-sep { color:var(--line-strong); }
|
|
17958
|
+
/* the link itself: mono, copy on click, copy glyph slides in on hover */
|
|
17959
|
+
.shv-link { display:inline-flex; align-items:center; gap:4px; min-width:0; max-width:46ch; font-family:var(--mono); font-size:var(--fs-xs);
|
|
17960
|
+
color:var(--fg-soft); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; border-radius:4px; }
|
|
17961
|
+
.shv-link .ico { width:12px; height:12px; opacity:0; transition:opacity .12s; flex:none; }
|
|
17962
|
+
.shv-link:not(:disabled):hover { color:var(--accent); }
|
|
17963
|
+
.shv-link:not(:disabled):hover .ico { opacity:1; }
|
|
17964
|
+
.shv-link:disabled { cursor:default; text-decoration:line-through; text-decoration-color:var(--line-strong); }
|
|
17965
|
+
.shv-snap { font-variant-numeric:tabular-nums; }
|
|
17966
|
+
.shv-perm.edit { color:var(--danger); font-weight:500; }
|
|
17967
|
+
.shv-lock { display:inline-flex; color:var(--fg-soft); }
|
|
17968
|
+
.shv-lock .ico { width:12px; height:12px; }
|
|
17969
|
+
/* status column: dot + short text, fixed floor so rows never jitter */
|
|
17970
|
+
.shv-status { display:inline-flex; align-items:center; gap:7px; min-width:9ch; font-size:var(--fs-sm); color:var(--fg-soft);
|
|
17971
|
+
font-variant-numeric:tabular-nums; white-space:nowrap; }
|
|
17972
|
+
.shv-dot { width:8px; height:8px; border-radius:50%; background:var(--muted); flex:none; }
|
|
17973
|
+
.shv-status.tone-ok .shv-dot { background:var(--success); }
|
|
17974
|
+
.shv-status.tone-warn { color:var(--warning,#9a6700); }
|
|
17975
|
+
.shv-status.tone-warn .shv-dot { background:var(--warning,#9a6700); }
|
|
17976
|
+
.shv-status.tone-muted { color:var(--muted); }
|
|
17977
|
+
.shv-status.tone-muted .shv-dot { background:transparent; border:1.5px solid var(--line-strong); }
|
|
17978
|
+
.shv-status .sync-ring { width:10px; height:10px; }
|
|
17200
17979
|
.shv-actions { display:flex; align-items:center; gap:6px; flex:none; }
|
|
17201
|
-
.shv-actions .btn { padding:5px 10px; font-size:12px; }
|
|
17980
|
+
.shv-actions .btn { padding:5px 10px; font-size:12px; min-width:76px; justify-content:center; }
|
|
17981
|
+
.shv-actions .btn.txt-danger { color:var(--danger); }
|
|
17982
|
+
.shv-row.st-expired .shv-title { color:var(--fg-soft); }
|
|
17983
|
+
/* expired group sinks below the live list */
|
|
17984
|
+
.shv-group-head { display:flex; align-items:center; gap:8px; margin:22px 2px 8px; font-size:var(--fs-sm); font-weight:600; color:var(--muted); }
|
|
17985
|
+
.shv-group-head .n { font-family:var(--mono); font-size:var(--fs-micro); font-weight:400; }
|
|
17986
|
+
.shv-group-head .btn { margin-left:auto; padding:4px 10px; font-size:12px; }
|
|
17987
|
+
/* list swap: data arriving over the skeleton fades in rather than hard-cutting */
|
|
17988
|
+
.shv-list { animation:skel-in .18s ease both; }
|
|
17989
|
+
|
|
17990
|
+
/* \u2500\u2500 skeletons (skeleton.tsx, primitives.tsx SetRowSkeleton, shares-view
|
|
17991
|
+
ShareListSkeleton). Rows are the REAL row markup; only content is swapped
|
|
17992
|
+
for sheen blocks. .skel-list fades in after 120ms so an instant load
|
|
17993
|
+
never flashes it; --i offsets each row's sheen into one rolling wave. */
|
|
17994
|
+
.skel-list { animation:skel-in .25s ease .12s both; }
|
|
17995
|
+
.skel-lines { display:flex; flex-direction:column; gap:8px; }
|
|
17996
|
+
.skel-lines .skel-s { width:calc(58% + var(--i, 0) * 9%); max-width:100%; }
|
|
17997
|
+
.page-header-sub .skel-s { display:block; width:22ch; }
|
|
17998
|
+
.set-row.skel { pointer-events:none; }
|
|
17999
|
+
.set-row.skel .set-row-text { display:flex; flex-direction:column; gap:5px; padding:3px 0; }
|
|
18000
|
+
.set-row.skel .skel-t { height:14px; width:calc(30% + var(--i, 0) * 8%); border-radius:5px; }
|
|
18001
|
+
.set-row.skel .skel-s { height:11px; width:calc(44% + var(--i, 0) * 7%); border-radius:4px; }
|
|
18002
|
+
.set-row.skel .set-row-lead.skel-b { border-radius:7px; }
|
|
18003
|
+
.skel-switch { width:38px; height:22px; border-radius:999px; }
|
|
18004
|
+
.skel-chevron { width:14px; height:14px; border-radius:4px; }
|
|
18005
|
+
.skel-menu { width:28px; height:28px; border-radius:var(--radius-sm); }
|
|
18006
|
+
.skel-button { width:76px; height:29px; border-radius:var(--radius-sm); }
|
|
18007
|
+
.audit-item.skel .audit-item-line { cursor:default; pointer-events:none; }
|
|
18008
|
+
.audit-item.skel .skel-when { width:56px; height:12px; border-radius:4px; flex:none; }
|
|
18009
|
+
.audit-item.skel .skel-t { height:14px; width:calc(34% + var(--i, 0) * 9%); border-radius:5px; }
|
|
18010
|
+
.site-card.skel { pointer-events:none; }
|
|
18011
|
+
.site-card.skel:hover { transform:none; box-shadow:none; border-color:var(--line); }
|
|
18012
|
+
.site-card.skel .si.skel-b { background:var(--surface-2); }
|
|
18013
|
+
.site-card.skel .skel-t { height:14px; width:calc(40% + var(--i, 0) * 8%); border-radius:5px; margin-bottom:6px; }
|
|
18014
|
+
.site-card.skel .skel-s { height:11px; width:calc(55% + var(--i, 0) * 6%); border-radius:4px; }
|
|
18015
|
+
.site-card.skel .skel-addr { height:30px; width:80%; border-radius:var(--radius-sm); }
|
|
18016
|
+
.filerow.skel { pointer-events:none; }
|
|
18017
|
+
.filerow.skel .fi.skel-b { background:var(--surface-2); }
|
|
18018
|
+
.filerow.skel .skel-t { height:13px; width:calc(36% + var(--i, 0) * 10%); border-radius:4px; }
|
|
18019
|
+
.blob-hero.skel .blob-legend .skel-s { height:12px; width:calc(70% + var(--i, 0) * 8%); }
|
|
18020
|
+
.blob-hero.skel .blob-total .skel-s { display:block; height:11px; width:14ch; }
|
|
18021
|
+
.skel-b { display:block; position:relative; overflow:hidden; background:var(--surface-2); border-radius:6px; }
|
|
18022
|
+
.skel-b::after { content:""; position:absolute; inset:0;
|
|
18023
|
+
background:linear-gradient(90deg, transparent 20%, color-mix(in srgb, var(--fg) 5%, transparent) 50%, transparent 80%);
|
|
18024
|
+
background-size:200% 100%; animation:skel-sweep 1.6s ease-in-out infinite;
|
|
18025
|
+
animation-delay:calc(var(--i, 0) * 120ms); }
|
|
18026
|
+
.shv-row.skel { pointer-events:none; }
|
|
18027
|
+
.shv-row.skel:hover { border-color:var(--line); box-shadow:none; }
|
|
18028
|
+
.skel-t { height:1.55em; height:1lh; width:calc(52% + var(--i, 0) * 9%); border-radius:5px; }
|
|
18029
|
+
.skel-s { height:1.55em; height:1lh; width:min(44ch, 60%); border-radius:4px; }
|
|
18030
|
+
.shv-status .skel-st { height:1.55em; height:1lh; width:7ch; border-radius:4px; }
|
|
18031
|
+
.skel-btn { width:76px; height:29px; border-radius:var(--radius-sm); }
|
|
18032
|
+
.skel-ib { width:30px; height:30px; border-radius:var(--radius-sm); }
|
|
18033
|
+
@keyframes skel-in { from { opacity:0; } to { opacity:1; } }
|
|
18034
|
+
@keyframes skel-sweep { from { background-position:100% 0; } to { background-position:-100% 0; } }
|
|
17202
18035
|
@media (max-width:640px){
|
|
17203
|
-
|
|
18036
|
+
.shv-actions .skel-btn { flex:1; width:auto; height:36px; }
|
|
18037
|
+
.shv-actions .skel-ib { width:40px; height:36px; }
|
|
18038
|
+
/* head stacks: title/desc full row, then segmented tabs, then [source][search] */
|
|
17204
18039
|
.shares-page .db-head { flex-direction:column; align-items:stretch; gap:12px; }
|
|
17205
|
-
.shv-
|
|
17206
|
-
.shv-
|
|
17207
|
-
.shv-seg
|
|
17208
|
-
.shv-
|
|
17209
|
-
.shv-
|
|
17210
|
-
|
|
17211
|
-
|
|
17212
|
-
|
|
17213
|
-
.shv-
|
|
17214
|
-
|
|
17215
|
-
.shv-
|
|
17216
|
-
|
|
17217
|
-
|
|
17218
|
-
.shv-actions {
|
|
17219
|
-
.shv-actions .btn { flex:1;
|
|
18040
|
+
.shv-toolbar { flex-wrap:wrap; }
|
|
18041
|
+
.shv-tools-right { display:contents; }
|
|
18042
|
+
.shv-seg { display:flex; width:100%; order:-1; }
|
|
18043
|
+
.shv-seg-btn { flex:1; justify-content:center; padding:6px 4px; }
|
|
18044
|
+
.shv-src { flex:1; justify-content:center; }
|
|
18045
|
+
.shv-search { flex:1; min-width:0; max-width:none; }
|
|
18046
|
+
|
|
18047
|
+
/* rows: ico + main on top; status, then the action row, each full width */
|
|
18048
|
+
.shv-row { grid-template-columns:38px minmax(0,1fr); gap:8px 13px; align-items:start; }
|
|
18049
|
+
.shv-title { white-space:normal; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; }
|
|
18050
|
+
.shv-sub { white-space:normal; flex-wrap:wrap; row-gap:2px; }
|
|
18051
|
+
.shv-link { max-width:100%; }
|
|
18052
|
+
.shv-status { grid-column:1 / -1; min-width:0; }
|
|
18053
|
+
.shv-actions { grid-column:1 / -1; }
|
|
18054
|
+
.shv-actions .btn { flex:1; padding:8px 10px; }
|
|
18055
|
+
.shv-actions .iconbtn { width:40px; height:36px; flex:none; }
|
|
18056
|
+
.shv-group-head { flex-wrap:wrap; }
|
|
18057
|
+
.shv-group-head .btn { margin-left:0; width:100%; justify-content:center; padding:7px 10px; }
|
|
17220
18058
|
}
|
|
17221
18059
|
|
|
17222
18060
|
/* in-context "\u5DF2\u5206\u4EAB" badge on docs / tables / site cards */
|
|
@@ -17378,6 +18216,11 @@ var styles_default = ` :root {
|
|
|
17378
18216
|
.proprow .v { flex:1; padding:7px 8px; border-radius:var(--radius-sm); cursor:text; min-height:33px; display:flex; flex-wrap:wrap; gap:3px; align-items:center; }
|
|
17379
18217
|
.proprow .v:hover { background:var(--hover); }
|
|
17380
18218
|
.peek-divider { height:1px; background:var(--line); margin:22px 0 18px; }
|
|
18219
|
+
.peek-hidden-toggle { display:flex; align-items:center; gap:6px; padding:7px 8px; margin:2px 0; border-radius:var(--radius-sm); color:var(--muted); font-size:13px; width:100%; text-align:left; }
|
|
18220
|
+
.peek-hidden-toggle:hover { background:var(--hover); color:var(--fg-soft); }
|
|
18221
|
+
.peek-hidden-toggle svg { transition:transform .15s; }
|
|
18222
|
+
.peek-hidden-toggle svg.down { transform:rotate(90deg); }
|
|
18223
|
+
.peek-head .peek-nav { display:flex; gap:2px; margin-left:4px; }
|
|
17381
18224
|
/* linked-document tabs + embedded editor below the peek's property list */
|
|
17382
18225
|
.peekdocs-tabs { display:flex; align-items:center; gap:4px; flex-wrap:wrap; margin-bottom:6px; }
|
|
17383
18226
|
.peekdocs-tab { border:0; background:none; font:inherit; font-size:var(--fs-sm); font-weight:500; color:var(--muted);
|
|
@@ -17408,7 +18251,7 @@ var styles_default = ` :root {
|
|
|
17408
18251
|
.hist-kind.repair { background:var(--danger-soft); color:var(--danger); }
|
|
17409
18252
|
.hist-now { font-size:var(--fs-xs); padding:1px 6px; border-radius:99px; background:var(--accent); color:var(--accent-fg); }
|
|
17410
18253
|
.hist-preview { flex:1; overflow:auto; padding:20px 24px; }
|
|
17411
|
-
.hist-doc-title { font-size:21px; font-weight:700; letter-spacing:-.02em; margin-bottom:14px; }
|
|
18254
|
+
.hist-doc-title { font-size:21px; font-weight:700; letter-spacing:-.02em; margin-bottom:14px; overflow-wrap:anywhere; }
|
|
17412
18255
|
.hist-foot { display:flex; justify-content:flex-end; padding:12px 16px; border-top:1px solid var(--line); }
|
|
17413
18256
|
.pad { padding:12px; }
|
|
17414
18257
|
.hist-rec-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:10px; }
|
|
@@ -17515,50 +18358,68 @@ var styles_default = ` :root {
|
|
|
17515
18358
|
button.fname.link { border:0; background:none; padding:0; text-align:left; cursor:pointer; font-size:inherit; }
|
|
17516
18359
|
button.fname.link:hover { color:var(--accent); text-decoration:underline; text-decoration-style:dashed; }
|
|
17517
18360
|
|
|
18361
|
+
/* key caps (kbd.tsx) */
|
|
18362
|
+
.kbd { display:inline-flex; align-items:center; gap:2px; vertical-align:middle; font-family:var(--kbd-font);
|
|
18363
|
+
font-variant-numeric:tabular-nums; color:var(--muted); white-space:nowrap; }
|
|
18364
|
+
.kbd .key { display:inline-grid; place-items:center; min-width:18px; height:18px; padding:0 4px;
|
|
18365
|
+
border:1px solid var(--line-strong); border-radius:5px; background:var(--surface); font-size:10.5px; font-weight:500; line-height:1; }
|
|
18366
|
+
.kbd .key.sym { font-size:12.5px; }
|
|
18367
|
+
|
|
18368
|
+
/* tooltip (TooltipHost in ui.tsx) */
|
|
18369
|
+
.tip { position:fixed; z-index:118; pointer-events:none; display:inline-flex; align-items:center; gap:8px;
|
|
18370
|
+
padding:5px 8px; border-radius:7px; background:var(--tip-bg); color:var(--tip-fg); font-size:12px; line-height:1.3;
|
|
18371
|
+
white-space:nowrap; box-shadow:var(--shadow-md); animation:tip-in .12s ease-out; }
|
|
18372
|
+
:root[data-resolved="dark"] .tip { border:1px solid var(--line-strong); }
|
|
18373
|
+
.tip[data-side="above"] { animation-name:tip-in-above; }
|
|
18374
|
+
.tip .kbd { color:var(--tip-kbd-fg); }
|
|
18375
|
+
.tip .kbd .key { min-width:16px; height:16px; padding:0 3px; background:var(--tip-kbd-bg); border-color:transparent; font-size:10px; }
|
|
18376
|
+
.tip .kbd .key.sym { font-size:11.5px; }
|
|
18377
|
+
@keyframes tip-in { from { opacity:0; transform:translateY(2px); } }
|
|
18378
|
+
@keyframes tip-in-above { from { opacity:0; transform:translateY(-2px); } }
|
|
18379
|
+
|
|
17518
18380
|
/* menu / popover */
|
|
17519
18381
|
.menu-layer { position:fixed; inset:0; z-index:115; }
|
|
17520
18382
|
.pop { position:fixed; background:var(--bg); border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow-lg);
|
|
17521
|
-
padding:6px; min-width:232px; max-height:70vh; overflow:auto; animation:pop .12s ease; }
|
|
18383
|
+
padding:6px; min-width:232px; max-width:min(420px, calc(100vw - 20px)); max-height:70vh; overflow:auto; animation:pop .12s ease; }
|
|
17522
18384
|
@keyframes pop { from { opacity:0; transform:translateY(-4px); } }
|
|
17523
18385
|
/* Action-sheet variant: MenuHost (ui.tsx) adds .sheet on phones, where an
|
|
17524
18386
|
anchored popover clips at the edges and sits under the finger. */
|
|
17525
18387
|
.pop.sheet { left:8px; right:8px; top:auto; bottom:calc(8px + env(safe-area-inset-bottom));
|
|
17526
|
-
min-width:0; max-height:60vh; max-height:60dvh; border-radius:var(--radius-lg);
|
|
18388
|
+
min-width:0; max-width:none; max-height:60vh; max-height:60dvh; border-radius:var(--radius-lg);
|
|
17527
18389
|
animation:sheet-in .18s ease; }
|
|
17528
18390
|
@keyframes sheet-in { from { opacity:0; transform:translateY(12px); } }
|
|
17529
|
-
.pop .lbl { font-size:var(--fs-micro); font-weight:600; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; padding:6px 8px 3px; }
|
|
17530
|
-
.pop .item { display:flex; align-items:center; gap:10px; padding:7px 8px; border-radius:var(--radius-sm); width:100%; text-align:left; color:var(--fg); font-size:var(--fs-ui); }
|
|
17531
|
-
.pop .item:hover, .pop .item.sel { background:var(--hover-2); }
|
|
17532
|
-
.pop .item
|
|
17533
|
-
.pop .item .lico
|
|
17534
|
-
.pop .item .
|
|
17535
|
-
|
|
17536
|
-
.pop .item .
|
|
17537
|
-
|
|
17538
|
-
|
|
17539
|
-
.pop .item
|
|
17540
|
-
.pop .
|
|
17541
|
-
.pop .sep { height:1px; background:var(--line); margin:5px 4px; }
|
|
18391
|
+
:is(.pop,.palette) .lbl { font-size:var(--fs-micro); font-weight:600; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; padding:6px 8px 3px; }
|
|
18392
|
+
:is(.pop,.palette) .item { display:flex; align-items:center; gap:10px; padding:7px 8px; border-radius:var(--radius-sm); width:100%; text-align:left; color:var(--fg); font-size:var(--fs-ui); }
|
|
18393
|
+
:is(.pop,.palette) .item:hover, :is(.pop,.palette) .item.sel { background:var(--hover-2); }
|
|
18394
|
+
:is(.pop,.palette) .item:focus-visible { outline:none; background:var(--hover-2); box-shadow:inset 0 0 0 1.5px var(--accent); }
|
|
18395
|
+
:is(.pop,.palette) .item .lico { width:28px; height:28px; display:grid; place-items:center; border:1px solid var(--line); border-radius:6px; color:var(--fg-soft); flex:none; }
|
|
18396
|
+
:is(.pop,.palette) .item .lico.plain { border:0; width:22px; height:22px; }
|
|
18397
|
+
:is(.pop,.palette) .item .meta { flex:1; min-width:0; }
|
|
18398
|
+
:is(.pop,.palette) .item .t { font-size:var(--fs-ui); display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } :is(.pop,.palette) .item .d { font-size:var(--fs-xs); color:var(--muted); display:block; }
|
|
18399
|
+
:is(.pop,.palette) .item .chk { color:var(--accent); margin-left:auto; }
|
|
18400
|
+
:is(.pop,.palette) .item .kbd { margin-left:auto; }
|
|
18401
|
+
:is(.pop,.palette) .item.danger { color:var(--danger); } :is(.pop,.palette) .item.danger .lico { color:var(--danger); }
|
|
18402
|
+
:is(.pop,.palette) .sep { height:1px; background:var(--line); margin:5px 4px; }
|
|
17542
18403
|
.pop .typegrid { display:grid; grid-template-columns:repeat(4,1fr); gap:4px; padding:2px 2px 4px; }
|
|
17543
18404
|
.pop .typegrid .item { flex-direction:column; gap:5px; padding:9px 4px; text-align:center; }
|
|
17544
18405
|
.pop .typegrid .item .lico { margin:0 auto; }
|
|
17545
18406
|
.pop input.field { width:100%; border:1px solid var(--line-strong); border-radius:var(--radius-sm); padding:7px 9px; outline:none; background:var(--surface); margin:2px; }
|
|
17546
18407
|
.pop input.field:focus { border-color:var(--accent); }
|
|
17547
18408
|
/* select-cell picker: search/create input at the top of the popover */
|
|
17548
|
-
.pop .selsearch { display:flex; align-items:center; gap:7px; margin:2px 2px 6px; padding:6px 9px;
|
|
18409
|
+
:is(.pop,.palette) .selsearch { display:flex; align-items:center; gap:7px; margin:2px 2px 6px; padding:6px 9px;
|
|
17549
18410
|
border:1px solid var(--line-strong); border-radius:var(--radius-sm); background:var(--surface); color:var(--muted);
|
|
17550
18411
|
transition:border-color .12s ease, box-shadow .12s ease, color .12s ease; }
|
|
17551
|
-
.pop .selsearch:focus-within { border-color:var(--accent); color:var(--accent);
|
|
18412
|
+
:is(.pop,.palette) .selsearch:focus-within { border-color:var(--accent); color:var(--accent);
|
|
17552
18413
|
box-shadow:0 0 0 2.5px color-mix(in srgb, var(--accent) 16%, transparent); }
|
|
17553
|
-
.pop .selsearch input { flex:1; width:0; min-width:0; border:0; background:transparent; outline:none;
|
|
18414
|
+
:is(.pop,.palette) .selsearch input { flex:1; width:0; min-width:0; border:0; background:transparent; outline:none;
|
|
17554
18415
|
font:inherit; font-size:var(--fs-ui); color:var(--fg); }
|
|
17555
|
-
.pop .selsearch input::placeholder { color:var(--muted); }
|
|
17556
|
-
.pop .selsearch .clear { display:grid; place-items:center; width:18px; height:18px; margin:-2px -3px -2px 0;
|
|
18416
|
+
:is(.pop,.palette) .selsearch input::placeholder { color:var(--muted); }
|
|
18417
|
+
:is(.pop,.palette) .selsearch .clear { display:grid; place-items:center; width:18px; height:18px; margin:-2px -3px -2px 0;
|
|
17557
18418
|
border-radius:50%; color:var(--muted); flex:none; }
|
|
17558
|
-
.pop .selsearch .clear:hover { background:var(--hover-2); color:var(--fg); }
|
|
18419
|
+
:is(.pop,.palette) .selsearch .clear:hover { background:var(--hover-2); color:var(--fg); }
|
|
17559
18420
|
/* relation record picker: fixed-height list \u2014 the popover is positioned once
|
|
17560
18421
|
on open, so async-loaded rows must scroll instead of growing off-screen */
|
|
17561
|
-
.pop .rellist { max-height:264px; overflow-y:auto; }
|
|
18422
|
+
:is(.pop,.palette) .rellist { max-height:264px; overflow-y:auto; }
|
|
17562
18423
|
.optrow { display:flex; align-items:center; gap:6px; padding:3px 4px; }
|
|
17563
18424
|
.optrow .chip { flex:1; }
|
|
17564
18425
|
.optrow .grip { width:18px; height:22px; display:grid; place-items:center; color:var(--muted); cursor:grab; touch-action:none; flex:none; }
|
|
@@ -17603,12 +18464,46 @@ var styles_default = ` :root {
|
|
|
17603
18464
|
/* opt-in (openModal aboveMenus): stack one dialog over the menu layer (115)
|
|
17604
18465
|
so a confirm can appear without dismissing an open popover */
|
|
17605
18466
|
.modal-scrim.above-menus { z-index:130; }
|
|
17606
|
-
.modal { background:var(--bg); border-radius:var(--radius-xl); box-shadow:var(--shadow-lg); width:440px; max-width:100%; max-height:100%; display:flex; flex-direction:column; overflow:hidden;
|
|
18467
|
+
.modal { background:var(--bg); border-radius:var(--radius-xl); box-shadow:var(--shadow-lg); width:440px; max-width:100%; max-height:100%; display:flex; flex-direction:column; overflow:hidden;
|
|
18468
|
+
animation:mo-in .13s ease-out; }
|
|
18469
|
+
@keyframes mo-in { from { opacity:0; transform:translateY(6px) scale(.98); } }
|
|
18470
|
+
@keyframes mo-out { to { opacity:0; transform:translateY(4px) scale(.985); } }
|
|
18471
|
+
.modal-scrim.closing .modal { animation:mo-out .12s ease-in forwards; }
|
|
18472
|
+
/* command palette (palette.tsx) */
|
|
18473
|
+
.palette { width:600px; align-self:flex-start; margin-top:11vh; padding:0 8px 0; max-height:min(64vh, 560px);
|
|
18474
|
+
animation:pal-in .13s ease-out; }
|
|
18475
|
+
@keyframes pal-in { from { opacity:0; transform:translateY(-6px) scale(.985); } }
|
|
18476
|
+
.pal-head { display:flex; align-items:center; gap:10px; padding:13px 8px 12px 10px; margin:0 0 6px; border-bottom:1px solid var(--line); color:var(--muted); }
|
|
18477
|
+
.pal-head input { flex:1; width:0; min-width:0; border:0; background:transparent; outline:none; font:inherit; font-size:16px; color:var(--fg); }
|
|
18478
|
+
.pal-head input::placeholder { color:var(--muted); }
|
|
18479
|
+
.pal-head .clear { display:grid; place-items:center; width:20px; height:20px; border-radius:50%; color:var(--muted); flex:none; }
|
|
18480
|
+
.pal-head .clear:hover { background:var(--hover-2); color:var(--fg); }
|
|
18481
|
+
.pal-close { display:none; place-items:center; width:28px; height:28px; border-radius:50%; color:var(--muted); flex:none; }
|
|
18482
|
+
.pal-close:active { background:var(--hover-2); }
|
|
18483
|
+
.pal-list { flex:1; min-height:0; overflow-y:auto; padding-bottom:4px; overscroll-behavior:contain; }
|
|
18484
|
+
.pal-sec + .pal-sec { margin-top:4px; }
|
|
18485
|
+
.palette .item { padding:7px 10px; }
|
|
18486
|
+
.palette .item .lico.plain { color:var(--muted); }
|
|
18487
|
+
:is(.pop,.palette) .item .lico .emo { font-size:15px; line-height:1; }
|
|
18488
|
+
:is(.pop,.palette) .item.sub-r .meta { display:flex; align-items:baseline; gap:12px; }
|
|
18489
|
+
:is(.pop,.palette) .item.sub-r .t { flex:1; }
|
|
18490
|
+
:is(.pop,.palette) .item.sub-r .d { flex:none; max-width:42%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
18491
|
+
.palette .item .d.snip { flex:1 1 100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
18492
|
+
.palette .item:has(.snip) .meta { flex-wrap:wrap; gap:1px 12px; }
|
|
18493
|
+
:is(.pop,.palette) mark { background:transparent; color:var(--accent); font-weight:600; }
|
|
18494
|
+
:is(.pop,.palette) .item .ret { margin-left:auto; flex:none; opacity:0; transition:opacity .1s; }
|
|
18495
|
+
:is(.pop,.palette) .item.sel .ret, :is(.pop,.palette) .item:focus-visible .ret { opacity:1; }
|
|
18496
|
+
:is(.pop,.palette) .pal-empty { padding:22px 12px 20px; text-align:center; color:var(--muted); font-size:var(--fs-ui); }
|
|
18497
|
+
.pal-foot { display:flex; align-items:center; gap:12px; padding:8px 10px 9px; margin:4px -8px 0; border-top:1px solid var(--line); color:var(--muted); font-size:var(--fs-xs); }
|
|
18498
|
+
.pal-foot .keys { display:flex; align-items:center; gap:4px 10px; flex-wrap:wrap; }
|
|
18499
|
+
.pal-foot .kbd { margin-right:2px; }
|
|
18500
|
+
.pal-foot .hint { margin-left:auto; white-space:nowrap; }
|
|
17607
18501
|
.modal-head { padding:20px 22px 0; }
|
|
17608
18502
|
.modal-head h3 { margin:0; font-size:18px; font-weight:700; letter-spacing:-.01em; }
|
|
17609
18503
|
.modal-head p { margin:6px 0 0; color:var(--muted); font-size:var(--fs-ui); }
|
|
17610
18504
|
.modal-body { padding:18px 22px; overflow-y:auto; }
|
|
17611
|
-
.modal-foot { display:flex; justify-content:flex-end; gap:8px; padding:14px 22px; background:var(--surface); border-top:1px solid var(--line); }
|
|
18505
|
+
.modal-foot { display:flex; justify-content:flex-end; align-items:center; gap:8px; padding:14px 22px; background:var(--surface); border-top:1px solid var(--line); }
|
|
18506
|
+
.modal-hint { margin-right:auto; display:flex; align-items:center; gap:4px 8px; color:var(--muted); font-size:var(--fs-xs); }
|
|
17612
18507
|
.field-label { font-size:12px; font-weight:600; color:var(--fg-soft); margin:12px 0 5px; }
|
|
17613
18508
|
.field-label:first-child { margin-top:0; }
|
|
17614
18509
|
.text-input { width:100%; border:1px solid var(--line-strong); border-radius:var(--radius-sm); padding:9px 11px; outline:none; background:var(--surface); font-size:var(--fs-body); }
|
|
@@ -17631,6 +18526,12 @@ var styles_default = ` :root {
|
|
|
17631
18526
|
.toast { background:var(--fg); color:var(--bg); padding:10px 14px; border-radius:9px; font-size:13px; box-shadow:var(--shadow-lg);
|
|
17632
18527
|
display:flex; align-items:center; gap:8px; animation:slidein .2s ease; }
|
|
17633
18528
|
@keyframes slidein { from { opacity:0; transform:translateX(-12px); } }
|
|
18529
|
+
.toast.error { background:var(--danger); color:#fff; }
|
|
18530
|
+
.toast.leaving { animation:toast-out .16s ease-in forwards; pointer-events:none; }
|
|
18531
|
+
@keyframes toast-out { to { opacity:0; transform:translateX(-8px); } }
|
|
18532
|
+
.toast-msg { min-width:0; }
|
|
18533
|
+
.toast-act { margin-left:4px; padding:3px 9px; border-radius:6px; font-weight:600; color:inherit; background:color-mix(in srgb, var(--bg) 16%, transparent); white-space:nowrap; }
|
|
18534
|
+
.toast-act:hover { background:color-mix(in srgb, var(--bg) 28%, transparent); }
|
|
17634
18535
|
|
|
17635
18536
|
/* misc */
|
|
17636
18537
|
.empty { display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%;
|
|
@@ -17698,9 +18599,74 @@ var styles_default = ` :root {
|
|
|
17698
18599
|
.kg-glow { opacity:.6 !important; }
|
|
17699
18600
|
.kg-particle { opacity:0 !important; }
|
|
17700
18601
|
}
|
|
17701
|
-
.error-bar { margin:12px 24px; padding:10px
|
|
18602
|
+
.error-bar { margin:12px 24px; padding:8px 10px 8px 12px; background:var(--danger-soft); color:var(--danger); border-radius:var(--radius-sm); font-size:13px;
|
|
18603
|
+
display:flex; align-items:center; gap:8px; animation:mo-in .13s ease-out; }
|
|
18604
|
+
.error-bar .error-msg { flex:1; min-width:0; overflow-wrap:anywhere; }
|
|
18605
|
+
.error-bar .error-x { display:grid; place-items:center; width:22px; height:22px; border-radius:50%; color:inherit; flex:none; }
|
|
18606
|
+
.error-bar .error-x:hover { background:color-mix(in srgb, var(--danger) 14%, transparent); }
|
|
18607
|
+
.sb-empty { color:var(--muted); gap:6px; width:100%; text-align:left; }
|
|
18608
|
+
.sb-empty:hover { color:var(--fg); }
|
|
18609
|
+
.tbl-empty { padding:36px 20px 28px; }
|
|
18610
|
+
.set-empty { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:6px 0; }
|
|
18611
|
+
.blob-mgr-empty { display:flex; flex-direction:column; align-items:center; gap:12px; }
|
|
18612
|
+
.doc-skel { max-width:720px; margin:40px auto; padding:0 24px; }
|
|
18613
|
+
|
|
18614
|
+
/* share / publish modal (share-modal.tsx) */
|
|
18615
|
+
.mhshare-body { display:flex; flex-direction:column; gap:10px; }
|
|
18616
|
+
.mhshare-section { font-size:var(--fs-micro); font-weight:600; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; margin-top:6px; }
|
|
18617
|
+
.mhshare-field { display:flex; align-items:center; gap:12px; }
|
|
18618
|
+
.mhshare-field > span { width:64px; color:var(--muted); flex:none; font-size:var(--fs-ui); }
|
|
18619
|
+
.mhshare-field select, .mhshare-field input { flex:1; padding:7px 9px; border:1px solid var(--line-strong); border-radius:var(--radius-sm); background:var(--surface); color:inherit; min-width:0; font:inherit; }
|
|
18620
|
+
.mhshare-field select:focus, .mhshare-field input:focus { border-color:var(--accent); outline:none; }
|
|
18621
|
+
.mhshare-field .row-select { flex:1; justify-content:space-between; border:1px solid var(--line-strong); background:var(--surface); }
|
|
18622
|
+
.mhshare-foot { display:flex; justify-content:flex-end; gap:8px; margin:4px 0 2px; }
|
|
18623
|
+
.mhshare-foot button, .mhshare-result button, .mhshare-result a, .mhshare-li-actions button, .mhshare-li-actions a, .mhshare-guide-actions button {
|
|
18624
|
+
display:inline-flex; align-items:center; gap:6px; padding:5px 11px; border:1px solid var(--line-strong); border-radius:var(--radius-sm);
|
|
18625
|
+
background:var(--surface); color:inherit; font-size:var(--fs-sm); font-weight:500; text-decoration:none; cursor:pointer; transition:all .12s; }
|
|
18626
|
+
.mhshare-foot button:hover, .mhshare-result button:hover, .mhshare-result a:hover, .mhshare-li-actions button:hover, .mhshare-li-actions a:hover, .mhshare-guide-actions button:hover { background:var(--hover-2); }
|
|
18627
|
+
.mhshare-primary { background:var(--accent) !important; color:var(--accent-fg) !important; border-color:var(--accent) !important; }
|
|
18628
|
+
.mhshare-primary:hover { filter:brightness(1.05); }
|
|
18629
|
+
.mhshare-primary:disabled { opacity:.6; cursor:default; }
|
|
18630
|
+
.mhshare-note { color:var(--muted); font-size:var(--fs-sm); margin:2px 0; line-height:1.5; }
|
|
18631
|
+
.mhshare-err { color:var(--danger); font-size:var(--fs-sm); margin:0; }
|
|
18632
|
+
.mhshare-ok { color:var(--success); font-size:var(--fs-sm); margin:0; }
|
|
18633
|
+
.mhshare-warn { color:var(--fg-soft); font-size:var(--fs-xs); }
|
|
18634
|
+
.mhshare-result { display:flex; gap:6px; align-items:center; padding:8px; border:1px solid var(--line); border-radius:var(--radius-sm); background:var(--surface); }
|
|
18635
|
+
.mhshare-result code { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:var(--fs-xs); font-family:var(--mono); }
|
|
18636
|
+
.mhshare-list { list-style:none; padding:0; margin:0; display:flex; flex-direction:column; gap:8px; }
|
|
18637
|
+
.mhshare-list li { border:1px solid var(--line); border-radius:var(--radius); padding:9px 11px; display:flex; flex-direction:column; gap:6px; }
|
|
18638
|
+
.mhshare-li-main { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
|
|
18639
|
+
.mhshare-badge { font-size:var(--fs-micro); background:var(--surface-2); border:1px solid var(--line); border-radius:999px; padding:1px 8px; }
|
|
18640
|
+
.mhshare-badge-pub { color:var(--accent); border-color:transparent; background:var(--accent-soft); }
|
|
18641
|
+
.mhshare-src { font-weight:600; overflow-wrap:anywhere; }
|
|
18642
|
+
.mhshare-meta { color:var(--muted); font-size:var(--fs-xs); }
|
|
18643
|
+
.mhshare-li-actions { display:flex; gap:6px; align-items:center; flex-wrap:wrap; }
|
|
18644
|
+
.mhshare-danger { color:var(--danger) !important; }
|
|
18645
|
+
.mhshare-form { display:flex; flex-direction:column; gap:10px; border:1px solid var(--line); border-radius:var(--radius); padding:12px; }
|
|
18646
|
+
.mhshare-addrow { display:flex; gap:8px; }
|
|
18647
|
+
.mhshare-addbtn { flex:1; display:flex; flex-direction:column; align-items:flex-start; gap:2px; padding:9px 12px; border:1px solid var(--line-strong); border-radius:var(--radius); background:none; color:inherit; cursor:pointer; text-align:left; transition:border-color .12s, box-shadow .12s; }
|
|
18648
|
+
.mhshare-addbtn:hover { background:var(--hover); }
|
|
18649
|
+
.mhshare-addbtn.sel { border-color:var(--accent); box-shadow:0 0 0 1px var(--accent) inset; }
|
|
18650
|
+
.mhshare-grants { display:flex; flex-direction:column; gap:6px; }
|
|
18651
|
+
.mhshare-grantlist { list-style:none; padding:0; margin:0; display:flex; flex-direction:column; gap:4px; }
|
|
18652
|
+
.mhshare-grantlist li { display:flex; align-items:center; gap:14px; padding:5px 8px; border:1px solid var(--line); border-radius:var(--radius-sm); }
|
|
18653
|
+
.mhshare-grantlist label { display:flex; align-items:center; gap:4px; font-size:var(--fs-sm); color:var(--muted); cursor:pointer; }
|
|
18654
|
+
.mhshare-grantdb { flex:1; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
18655
|
+
.mhshare-adv { align-self:flex-start; background:none; border:0; color:var(--accent); font-size:var(--fs-xs); cursor:pointer; padding:0; }
|
|
18656
|
+
.mhshare-acard-t { font-weight:600; font-size:var(--fs-ui); }
|
|
18657
|
+
.mhshare-acard-d { color:var(--muted); font-size:var(--fs-xs); }
|
|
18658
|
+
.mhshare-hostsum { flex:1; display:flex; align-items:center; gap:8px; padding:8px 10px; border:1px solid var(--line-strong); border-radius:var(--radius-sm); background:var(--surface); font-size:var(--fs-sm); }
|
|
18659
|
+
.mhshare-hostsum span { flex:1; }
|
|
18660
|
+
.mhshare-hostsum button { background:none; border:0; color:var(--accent); font-size:var(--fs-xs); cursor:pointer; padding:0; flex:none; }
|
|
18661
|
+
.mhshare-guide { border:1px solid var(--line-strong); border-left:3px solid var(--accent); border-radius:var(--radius-sm); padding:8px 11px; display:flex; flex-direction:column; gap:6px; }
|
|
18662
|
+
.mhshare-guide p { margin:0; font-size:var(--fs-sm); color:var(--fg); }
|
|
18663
|
+
.mhshare-guide-actions { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
|
18664
|
+
.mhshare-guide-link { border:0 !important; background:none !important; color:var(--accent) !important; padding:4px 0 !important; }
|
|
17702
18665
|
.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; }
|
|
17703
18666
|
.search-hit { margin:0 0 8px; padding:10px 14px; background:var(--surface); border-radius:var(--radius-sm); font-size:13px; cursor:pointer; }
|
|
18667
|
+
.search-hit-head { display:flex; align-items:baseline; gap:6px; min-width:0; }
|
|
18668
|
+
.search-hit-head strong { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
18669
|
+
.search-hit-head .muted { flex:none; }
|
|
17704
18670
|
.search-hit:hover { background:var(--surface-2); }
|
|
17705
18671
|
.search-hit mark { background:transparent; color:var(--accent); font-weight:600; }
|
|
17706
18672
|
.view-placeholder { padding:40px; text-align:center; border:1px dashed var(--line-strong); border-radius:var(--radius); color:var(--muted); margin-top:8px; }
|
|
@@ -17901,6 +18867,7 @@ var styles_default = ` :root {
|
|
|
17901
18867
|
html:has(body.mobile), body.mobile { background:var(--sidebar); }
|
|
17902
18868
|
html:has(body.mobile-content), body.mobile-content { background:var(--bg); }
|
|
17903
18869
|
.sb-head .iconbtn { display:none; } /* no collapse on mobile */
|
|
18870
|
+
.sb-head .sync-ind { display:inline-flex; height:32px; } /* .brand's flex:1 already pushes it right */
|
|
17904
18871
|
.sb-resizer { display:none; }
|
|
17905
18872
|
.topbar .hamburger { display:grid; } /* serves as the back button */
|
|
17906
18873
|
/* site config topbar: icon-only publish button so the crumb keeps its last segment */
|
|
@@ -17922,17 +18889,15 @@ var styles_default = ` :root {
|
|
|
17922
18889
|
.navitem { padding:9px 8px; font-size:16px; }
|
|
17923
18890
|
.navitem .emoji { font-size:18px; }
|
|
17924
18891
|
.navitem .tw { width:26px; height:26px; }
|
|
17925
|
-
.sb-search kbd, .pop .item kbd { display:none; } /* no hardware keyboard, no shortcut hint */
|
|
18892
|
+
.sb-search .kbd, :is(.pop,.palette) .item .kbd { display:none; } /* no hardware keyboard, no shortcut hint */
|
|
17926
18893
|
/* Search box is hidden until the header search button reveals it; it then
|
|
17927
18894
|
slides+fades down in. Desktop base (.sb-search{display:flex}) is untouched,
|
|
17928
18895
|
so search stays always-on there. reduced-motion is handled globally. */
|
|
17929
18896
|
.sb-search { display:none; }
|
|
17930
|
-
.sb-search.open { display:flex; animation:search-reveal .22s cubic-bezier(.32,.72,0,1); }
|
|
17931
|
-
@keyframes search-reveal { from { opacity:0; transform:translateY(-8px); } }
|
|
17932
18897
|
.btn { font-size:15px; }
|
|
17933
18898
|
.crumb { font-size:16px; }
|
|
17934
|
-
.pop .item { font-size:16px; }
|
|
17935
|
-
.pop .item .t { font-size:16px; }
|
|
18899
|
+
:is(.pop,.palette) .item { font-size:16px; }
|
|
18900
|
+
:is(.pop,.palette) .item .t { font-size:16px; }
|
|
17936
18901
|
|
|
17937
18902
|
/* Document: bigger body text, tighter side padding for narrow screens. */
|
|
17938
18903
|
.doc { padding:24px 18px calc(36vh + env(safe-area-inset-bottom)); }
|
|
@@ -17983,11 +18948,24 @@ var styles_default = ` :root {
|
|
|
17983
18948
|
border-radius:var(--radius-xl) var(--radius-xl) 0 0;
|
|
17984
18949
|
padding-bottom:env(safe-area-inset-bottom);
|
|
17985
18950
|
animation:sheet-up .22s cubic-bezier(.32,.72,0,1); }
|
|
18951
|
+
.modal-scrim.closing .modal { animation:sheet-out .16s ease-in forwards; }
|
|
18952
|
+
.modal-scrim.closing .modal.palette { animation:sheet-down-out .16s ease-in forwards; }
|
|
18953
|
+
.modal.palette { align-self:flex-start; margin-top:0; max-height:80vh; max-height:80dvh;
|
|
18954
|
+
padding:env(safe-area-inset-top) 8px 0; border-radius:0 0 var(--radius-xl) var(--radius-xl);
|
|
18955
|
+
animation:sheet-down .22s cubic-bezier(.32,.72,0,1); }
|
|
18956
|
+
:is(.pop,.palette) .item.sub-r .meta { display:block; }
|
|
18957
|
+
:is(.pop,.palette) .item.sub-r .d { max-width:none; }
|
|
18958
|
+
:is(.pop,.palette) .item .ret, .pal-foot .keys, .pal-head .kbd, .modal-hint { display:none; }
|
|
18959
|
+
.pal-foot .hint { margin-left:0; }
|
|
18960
|
+
.pal-close { display:grid; }
|
|
17986
18961
|
.toasts { left:12px; right:12px; bottom:calc(12px + env(safe-area-inset-bottom)); }
|
|
17987
18962
|
.toast { justify-content:center; }
|
|
17988
18963
|
.selbar { bottom:calc(16px + env(safe-area-inset-bottom)); max-width:calc(100vw - 24px); }
|
|
17989
18964
|
}
|
|
17990
18965
|
@keyframes sheet-up { from { transform:translateY(100%); } }
|
|
18966
|
+
@keyframes sheet-down { from { transform:translateY(-100%); } }
|
|
18967
|
+
@keyframes sheet-out { to { transform:translateY(100%); } }
|
|
18968
|
+
@keyframes sheet-down-out { to { transform:translateY(-100%); } }
|
|
17991
18969
|
/* In-flow stand-in for the old two-pane slide: only the entering view
|
|
17992
18970
|
animates (the other is display:none, document-scroll layout above). */
|
|
17993
18971
|
@keyframes page-in { from { transform:translateX(28px); opacity:0; } }
|
|
@@ -18004,7 +18982,7 @@ var styles_default = ` :root {
|
|
|
18004
18982
|
.sb-tabs .add { width:40px; height:40px; display:grid; place-items:center; padding:0; }
|
|
18005
18983
|
.iconbtn { width:40px; height:40px; }
|
|
18006
18984
|
.btn { padding:9px 14px; min-height:40px; }
|
|
18007
|
-
.pop .item { padding:11px 10px; }
|
|
18985
|
+
:is(.pop,.palette) .item { padding:11px 10px; }
|
|
18008
18986
|
/* Column/sidebar resize handles: 7px is a mouse affordance, not a finger's. */
|
|
18009
18987
|
.col-resizer, .doc-col-resizer, .sb-resizer { width:16px; right:-8px; }
|
|
18010
18988
|
/* cell-rectangle corner handle: big enough to grab with a finger (it is the
|
|
@@ -18078,6 +19056,8 @@ var styles_default = ` :root {
|
|
|
18078
19056
|
.qn-body { flex:1; overflow:auto; scrollbar-gutter:stable; scrollbar-width:thin; -webkit-app-region:no-drag; }
|
|
18079
19057
|
.qn-body .doc { max-width:none; margin:0; padding:14px 1px 30vh 12px; }
|
|
18080
19058
|
.qn-body .doc-title { font-size:24px; margin-bottom:2px; }
|
|
19059
|
+
.qn-body .doc-title { overflow-wrap:anywhere; }
|
|
19060
|
+
.qn-body .doc-title:not(:focus) { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
18081
19061
|
.qn-body .doc-meta { display:none; }
|
|
18082
19062
|
/* The CM6 hover gutter never fits the compact window: quick notes offer no
|
|
18083
19063
|
block insert/drag handles (Enter / slash commands cover insertion). */
|
|
@@ -18098,6 +19078,7 @@ var styles_default = ` :root {
|
|
|
18098
19078
|
share .pop/.item/.lico with the main app; scope under the quick window
|
|
18099
19079
|
body classes so the main app keeps its roomier density). */
|
|
18100
19080
|
body.quicknote .pop, body.quickboard .pop { padding:5px; border-radius:9px; }
|
|
19081
|
+
body.quicknote .menu-layer .pop, body.quickboard .menu-layer .pop { max-width:50vw; }
|
|
18101
19082
|
body.quicknote .pop .lbl, body.quickboard .pop .lbl { padding:4px 8px 2px; font-size:10px; }
|
|
18102
19083
|
body.quicknote .pop .item, body.quickboard .pop .item { padding:5px 9px; gap:8px; font-size:var(--fs-sm); }
|
|
18103
19084
|
body.quicknote .pop .item .t, body.quickboard .pop .item .t { font-size:var(--fs-sm); }
|
|
@@ -18332,17 +19313,7 @@ var styles_default = ` :root {
|
|
|
18332
19313
|
.switch.on .switch-knob { transform:translateX(16px); }
|
|
18333
19314
|
.ico.spin { animation:save-spin .8s linear infinite; }
|
|
18334
19315
|
|
|
18335
|
-
/* clear-policy segmented control (redundancy any/all) */
|
|
18336
19316
|
.seg { display:flex; gap:8px; }
|
|
18337
|
-
.seg-opt { flex:1 1 0; display:flex; flex-direction:column; gap:2px; align-items:flex-start;
|
|
18338
|
-
text-align:left; padding:9px 12px; border:1px solid var(--line-strong); border-radius:var(--radius);
|
|
18339
|
-
background:var(--surface); cursor:pointer; transition:border-color .14s, background .14s, box-shadow .14s; }
|
|
18340
|
-
.seg-opt:hover { border-color:var(--muted); }
|
|
18341
|
-
.seg-opt.on { border-color:var(--accent); background:var(--accent-soft); box-shadow:0 0 0 1px var(--accent) inset; }
|
|
18342
|
-
.seg-opt:disabled { opacity:.6; cursor:default; }
|
|
18343
|
-
.seg-opt-t { font-size:var(--fs-sm); font-weight:600; color:var(--fg); }
|
|
18344
|
-
.seg-opt-s { font-size:var(--fs-xs); color:var(--muted); }
|
|
18345
|
-
.seg-opt.on .seg-opt-s { color:var(--accent); }
|
|
18346
19317
|
|
|
18347
19318
|
/* cache-usage ring hero (settings/cache-ring.tsx CacheRingHero): legend
|
|
18348
19319
|
column left, 152px three-segment ring right. Arcs animate 0 \u2192 share on
|
|
@@ -18358,6 +19329,9 @@ var styles_default = ` :root {
|
|
|
18358
19329
|
.blob-dot.keep { background:var(--line-strong); }
|
|
18359
19330
|
.blob-dot.pin { background:var(--muted); opacity:.6; }
|
|
18360
19331
|
.blob-total { margin:13px 0 14px; font-size:var(--fs-xs); color:var(--muted); }
|
|
19332
|
+
/* a status sentence in the action row (never a disabled button) */
|
|
19333
|
+
.blob-actions .blob-status { display:inline-flex; align-items:center; gap:6px; font-size:var(--fs-sm); color:var(--muted); }
|
|
19334
|
+
.blob-actions .blob-status .ico { width:14px; height:14px; color:var(--success); }
|
|
18361
19335
|
|
|
18362
19336
|
.blob-ring { position:relative; width:152px; height:152px; flex:none; }
|
|
18363
19337
|
.blob-ring-svg { width:100%; height:100%; display:block; }
|
|
@@ -18392,7 +19366,7 @@ var styles_default = ` :root {
|
|
|
18392
19366
|
}
|
|
18393
19367
|
|
|
18394
19368
|
@media (prefers-reduced-motion: reduce) {
|
|
18395
|
-
.switch, .switch-knob, .ico.spin, .blob-seg, .blob-ring-track { transition:none; animation:none; }
|
|
19369
|
+
.switch, .switch-knob, .ico.spin, .sync-ring, .blob-seg, .blob-ring-track { transition:none; animation:none; }
|
|
18396
19370
|
}
|
|
18397
19371
|
|
|
18398
19372
|
/* Blob manager popup (Settings storage \u2192 Blob \u7BA1\u7406): a sortable/filterable list
|
|
@@ -18502,7 +19476,7 @@ var FMT_PROVIDERS = [
|
|
|
18502
19476
|
init_package();
|
|
18503
19477
|
var RUNNING_FROM_SOURCE = import.meta.url.includes("/src/webui/");
|
|
18504
19478
|
var HTML2 = `<!doctype html>
|
|
18505
|
-
<html lang="zh">
|
|
19479
|
+
<html lang="zh-CN">
|
|
18506
19480
|
<head>
|
|
18507
19481
|
<meta charset="utf-8" />
|
|
18508
19482
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
@@ -18526,6 +19500,9 @@ var HTML2 = `<!doctype html>
|
|
|
18526
19500
|
alone, so the dark palette exists exactly once \u2014 see styles.css. Dark also
|
|
18527
19501
|
pre-tints the status bar (hex mirrors --bg) so the first frame isn't white. -->
|
|
18528
19502
|
<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>
|
|
19503
|
+
<!-- Same for the UI language: mirror src/webui/i18n/locale.ts so <html lang>
|
|
19504
|
+
is right before the bundle runs. -->
|
|
19505
|
+
<script>try{var l=localStorage.getItem('mh-lang');if(l!=='zh-CN'&&l!=='en'){var ls=navigator.languages&&navigator.languages.length?navigator.languages:[navigator.language];l='en';for(var i=0;i<ls.length;i++){var g=ls[i]||'';if(/^zh\\b/i.test(g)){l='zh-CN';break}if(/^en\\b/i.test(g))break}}document.documentElement.lang=l}catch(e){}</script>
|
|
18529
19506
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
18530
19507
|
<link rel="apple-touch-icon" href="/icons/icon-180.png">
|
|
18531
19508
|
<link rel="stylesheet" href="/webui.css">
|
|
@@ -18776,6 +19753,358 @@ async function serveWebui(req) {
|
|
|
18776
19753
|
}
|
|
18777
19754
|
// src/webui/server/routes.ts
|
|
18778
19755
|
init_zod();
|
|
19756
|
+
|
|
19757
|
+
// src/core/integrity.ts
|
|
19758
|
+
init_crdt();
|
|
19759
|
+
init_properties();
|
|
19760
|
+
init_hlc();
|
|
19761
|
+
init_node();
|
|
19762
|
+
init_history_cache();
|
|
19763
|
+
var REF_RULES = [
|
|
19764
|
+
{ srcTable: "properties", fk: "database_id", tgtTable: "databases", fix: "tombstone-src" },
|
|
19765
|
+
{ srcTable: "records", fk: "database_id", tgtTable: "databases", fix: "tombstone-src" },
|
|
19766
|
+
{ srcTable: "documents", fk: "database_id", tgtTable: "databases", fix: "null-fk" },
|
|
19767
|
+
{ srcTable: "documents", fk: "parent_id", tgtTable: "documents", fix: "null-fk" },
|
|
19768
|
+
{ srcTable: "doc_blocks", fk: "doc_id", tgtTable: "documents", fix: "tombstone-src" },
|
|
19769
|
+
{ srcTable: "site_files", fk: "site_id", tgtTable: "sites", fix: "tombstone-src" }
|
|
19770
|
+
];
|
|
19771
|
+
function brokenRefRows(db, r) {
|
|
19772
|
+
return db.query(`SELECT s.id AS id, s.${r.fk} AS ref FROM ${r.srcTable} s
|
|
19773
|
+
JOIN ${r.tgtTable} t ON t.id = s.${r.fk} AND t.__deleted = 1
|
|
19774
|
+
WHERE s.__deleted = 0 AND s.${r.fk} IS NOT NULL
|
|
19775
|
+
ORDER BY s.id`).all();
|
|
19776
|
+
}
|
|
19777
|
+
function detectBrokenRefs(db) {
|
|
19778
|
+
const out = [];
|
|
19779
|
+
for (const r of REF_RULES) {
|
|
19780
|
+
for (const row of brokenRefRows(db, r)) {
|
|
19781
|
+
out.push({
|
|
19782
|
+
category: "broken_ref",
|
|
19783
|
+
entity: r.srcTable,
|
|
19784
|
+
id: row.id,
|
|
19785
|
+
detail: `${r.fk} \u2192 deleted ${r.tgtTable} ${row.ref} (${r.fix === "tombstone-src" ? "tombstone" : `clear ${r.fk}`})`,
|
|
19786
|
+
fixable: true
|
|
19787
|
+
});
|
|
19788
|
+
}
|
|
19789
|
+
}
|
|
19790
|
+
return out;
|
|
19791
|
+
}
|
|
19792
|
+
function repairBrokenRefs(db) {
|
|
19793
|
+
let n = 0;
|
|
19794
|
+
for (const r of REF_RULES) {
|
|
19795
|
+
for (const row of brokenRefRows(db, r)) {
|
|
19796
|
+
if (r.fix === "tombstone-src")
|
|
19797
|
+
emit(db, r.srcTable, row.id, "__deleted", 1);
|
|
19798
|
+
else
|
|
19799
|
+
emit(db, r.srcTable, row.id, r.fk, null);
|
|
19800
|
+
n++;
|
|
19801
|
+
}
|
|
19802
|
+
}
|
|
19803
|
+
return n;
|
|
19804
|
+
}
|
|
19805
|
+
function orphanCellRows(db) {
|
|
19806
|
+
return db.query(`SELECT r.id AS rec, j.key AS prop
|
|
19807
|
+
FROM records r, json_each(coalesce(r.data, '{}')) j
|
|
19808
|
+
JOIN properties p ON p.id = j.key AND p.__deleted = 1
|
|
19809
|
+
WHERE r.__deleted = 0
|
|
19810
|
+
ORDER BY r.id, j.key`).all();
|
|
19811
|
+
}
|
|
19812
|
+
function detectOrphanCells(db) {
|
|
19813
|
+
return orphanCellRows(db).map((row) => ({
|
|
19814
|
+
category: "orphan_cell",
|
|
19815
|
+
entity: "records",
|
|
19816
|
+
id: row.rec,
|
|
19817
|
+
detail: `cell for deleted property ${row.prop}`,
|
|
19818
|
+
fixable: true
|
|
19819
|
+
}));
|
|
19820
|
+
}
|
|
19821
|
+
function repairOrphanCells(db) {
|
|
19822
|
+
const rows = orphanCellRows(db);
|
|
19823
|
+
for (const row of rows)
|
|
19824
|
+
emit(db, "records", row.rec, row.prop, undefined);
|
|
19825
|
+
return rows.length;
|
|
19826
|
+
}
|
|
19827
|
+
function deadCellRefRows(db) {
|
|
19828
|
+
const props = db.query("SELECT id, type FROM properties WHERE __deleted = 0 AND type IN ('relation', 'doc') ORDER BY id").all();
|
|
19829
|
+
const out = [];
|
|
19830
|
+
for (const p of props) {
|
|
19831
|
+
const tgtTable = p.type === "relation" ? "records" : "documents";
|
|
19832
|
+
const pid = p.id.replace(/'/g, "''");
|
|
19833
|
+
const rows = db.query(`SELECT r.id AS rec, j.value AS dead
|
|
19834
|
+
FROM records r, json_each(r.data -> '${pid}') j
|
|
19835
|
+
JOIN ${tgtTable} t ON t.id = j.value AND t.__deleted = 1
|
|
19836
|
+
WHERE r.__deleted = 0 AND json_type(r.data -> '${pid}') = 'array'
|
|
19837
|
+
ORDER BY r.id, j.value`).all();
|
|
19838
|
+
let cur = null;
|
|
19839
|
+
for (const row of rows) {
|
|
19840
|
+
if (!cur || cur.rec !== row.rec) {
|
|
19841
|
+
cur = { rec: row.rec, prop: p.id, tgtTable, dead: [] };
|
|
19842
|
+
out.push(cur);
|
|
19843
|
+
}
|
|
19844
|
+
cur.dead.push(row.dead);
|
|
19845
|
+
}
|
|
19846
|
+
}
|
|
19847
|
+
return out;
|
|
19848
|
+
}
|
|
19849
|
+
function detectDeadCellRefs(db) {
|
|
19850
|
+
return deadCellRefRows(db).map((g) => ({
|
|
19851
|
+
category: "dead_cell_ref",
|
|
19852
|
+
entity: "records",
|
|
19853
|
+
id: g.rec,
|
|
19854
|
+
detail: `cell ${g.prop} references deleted ${g.tgtTable === "records" ? "record" : "document"}: ${g.dead.join(", ")}`,
|
|
19855
|
+
fixable: true
|
|
19856
|
+
}));
|
|
19857
|
+
}
|
|
19858
|
+
function repairDeadCellRefs(db) {
|
|
19859
|
+
const groups = deadCellRefRows(db);
|
|
19860
|
+
for (const g of groups) {
|
|
19861
|
+
const row = db.query("SELECT data FROM records WHERE id = ?").get(g.rec);
|
|
19862
|
+
if (!row)
|
|
19863
|
+
continue;
|
|
19864
|
+
const data = JSON.parse(row.data || "{}");
|
|
19865
|
+
const arr = data[g.prop];
|
|
19866
|
+
if (!Array.isArray(arr))
|
|
19867
|
+
continue;
|
|
19868
|
+
const deadSet = new Set(g.dead);
|
|
19869
|
+
emit(db, "records", g.rec, g.prop, arr.filter((v) => !deadSet.has(String(v))));
|
|
19870
|
+
}
|
|
19871
|
+
return groups.length;
|
|
19872
|
+
}
|
|
19873
|
+
function dupPathLosers(db) {
|
|
19874
|
+
const rows = db.query(`SELECT id, site_id, path FROM site_files
|
|
19875
|
+
WHERE __deleted = 0 AND site_id IS NOT NULL AND path IS NOT NULL
|
|
19876
|
+
ORDER BY site_id, path, created_hlc, id`).all();
|
|
19877
|
+
const losers = [];
|
|
19878
|
+
let group = null;
|
|
19879
|
+
for (const r of rows) {
|
|
19880
|
+
const key = `${r.site_id}\x00${r.path}`;
|
|
19881
|
+
if (key === group)
|
|
19882
|
+
losers.push({ id: r.id });
|
|
19883
|
+
else
|
|
19884
|
+
group = key;
|
|
19885
|
+
}
|
|
19886
|
+
return losers;
|
|
19887
|
+
}
|
|
19888
|
+
function detectDupPaths(db) {
|
|
19889
|
+
return dupPathLosers(db).map((row) => ({
|
|
19890
|
+
category: "dup_path",
|
|
19891
|
+
entity: "site_files",
|
|
19892
|
+
id: row.id,
|
|
19893
|
+
detail: "redundant duplicate (site_id, path); loser tombstoned",
|
|
19894
|
+
fixable: true
|
|
19895
|
+
}));
|
|
19896
|
+
}
|
|
19897
|
+
function repairDupPaths(db) {
|
|
19898
|
+
const losers = dupPathLosers(db);
|
|
19899
|
+
for (const row of losers)
|
|
19900
|
+
emit(db, "site_files", row.id, "__deleted", 1);
|
|
19901
|
+
return losers.length;
|
|
19902
|
+
}
|
|
19903
|
+
function detectParentCycles(db) {
|
|
19904
|
+
const rows = db.query(`SELECT d.id AS id, d.parent_id AS parent, d.created_hlc AS hlc
|
|
19905
|
+
FROM documents d
|
|
19906
|
+
WHERE d.__deleted = 0 AND d.parent_id IS NOT NULL
|
|
19907
|
+
AND EXISTS (SELECT 1 FROM documents p WHERE p.id = d.parent_id AND p.__deleted = 0)
|
|
19908
|
+
ORDER BY d.id`).all();
|
|
19909
|
+
const parent = new Map(rows.map((r) => [r.id, r.parent]));
|
|
19910
|
+
const hlc = new Map(rows.map((r) => [r.id, r.hlc]));
|
|
19911
|
+
const seen = new Set;
|
|
19912
|
+
const cycles = [];
|
|
19913
|
+
for (const start of parent.keys()) {
|
|
19914
|
+
if (seen.has(start))
|
|
19915
|
+
continue;
|
|
19916
|
+
const path = [];
|
|
19917
|
+
const pos = new Map;
|
|
19918
|
+
let cur = start;
|
|
19919
|
+
while (cur != null && parent.has(cur) && !seen.has(cur)) {
|
|
19920
|
+
if (pos.has(cur)) {
|
|
19921
|
+
cycles.push(path.slice(pos.get(cur)));
|
|
19922
|
+
break;
|
|
19923
|
+
}
|
|
19924
|
+
pos.set(cur, path.length);
|
|
19925
|
+
path.push(cur);
|
|
19926
|
+
cur = parent.get(cur);
|
|
19927
|
+
}
|
|
19928
|
+
for (const n of path)
|
|
19929
|
+
seen.add(n);
|
|
19930
|
+
}
|
|
19931
|
+
return { cycles, hlc };
|
|
19932
|
+
}
|
|
19933
|
+
function geHlcId(hlc, a, b) {
|
|
19934
|
+
const ha = hlc.get(a) ?? "";
|
|
19935
|
+
const hb = hlc.get(b) ?? "";
|
|
19936
|
+
if (ha !== hb)
|
|
19937
|
+
return ha > hb;
|
|
19938
|
+
return a >= b;
|
|
19939
|
+
}
|
|
19940
|
+
function detectCycleIssues(db) {
|
|
19941
|
+
const { cycles } = detectParentCycles(db);
|
|
19942
|
+
return cycles.map((cyc) => ({
|
|
19943
|
+
category: "parent_cycle",
|
|
19944
|
+
entity: "documents",
|
|
19945
|
+
id: [...cyc].sort()[0],
|
|
19946
|
+
detail: `parent cycle: ${[...cyc].sort().join(" \u2192 ")}`,
|
|
19947
|
+
fixable: true
|
|
19948
|
+
}));
|
|
19949
|
+
}
|
|
19950
|
+
function repairParentCycles(db) {
|
|
19951
|
+
const { cycles, hlc } = detectParentCycles(db);
|
|
19952
|
+
for (const cyc of cycles) {
|
|
19953
|
+
const brk = cyc.reduce((a, b) => geHlcId(hlc, a, b) ? a : b);
|
|
19954
|
+
emit(db, "documents", brk, "parent_id", null);
|
|
19955
|
+
}
|
|
19956
|
+
return cycles.length;
|
|
19957
|
+
}
|
|
19958
|
+
function detectDupNames(db) {
|
|
19959
|
+
const out = [];
|
|
19960
|
+
const props = db.query(`SELECT database_id AS db, lower(name) AS n, count(*) AS c, group_concat(id) AS ids
|
|
19961
|
+
FROM properties
|
|
19962
|
+
WHERE __deleted = 0 AND name IS NOT NULL AND database_id IS NOT NULL
|
|
19963
|
+
GROUP BY database_id, lower(name) HAVING c > 1
|
|
19964
|
+
ORDER BY database_id, n`).all();
|
|
19965
|
+
for (const r of props)
|
|
19966
|
+
out.push({
|
|
19967
|
+
category: "dup_name",
|
|
19968
|
+
entity: "properties",
|
|
19969
|
+
id: r.ids.split(",").sort()[0],
|
|
19970
|
+
detail: `${r.c} properties named "${r.n}" in database ${r.db}: ${r.ids}`,
|
|
19971
|
+
fixable: false
|
|
19972
|
+
});
|
|
19973
|
+
const dbs = db.query(`SELECT lower(name) AS n, count(*) AS c, group_concat(id) AS ids
|
|
19974
|
+
FROM databases WHERE __deleted = 0 AND name IS NOT NULL
|
|
19975
|
+
GROUP BY lower(name) HAVING c > 1 ORDER BY n`).all();
|
|
19976
|
+
for (const r of dbs)
|
|
19977
|
+
out.push({
|
|
19978
|
+
category: "dup_name",
|
|
19979
|
+
entity: "databases",
|
|
19980
|
+
id: r.ids.split(",").sort()[0],
|
|
19981
|
+
detail: `${r.c} databases named "${r.n}": ${r.ids}`,
|
|
19982
|
+
fixable: false
|
|
19983
|
+
});
|
|
19984
|
+
return out;
|
|
19985
|
+
}
|
|
19986
|
+
function configIssue(type, config) {
|
|
19987
|
+
if (!PROP_TYPES.has(type))
|
|
19988
|
+
return `unknown property type: ${type}`;
|
|
19989
|
+
if (type === "select" || type === "multi_select") {
|
|
19990
|
+
const opts = config?.options;
|
|
19991
|
+
if (!Array.isArray(opts) || opts.length === 0 || !opts.every((o) => typeof o === "string"))
|
|
19992
|
+
return `${type} missing config.options: string[]`;
|
|
19993
|
+
}
|
|
19994
|
+
if (type === "relation" && typeof config?.database !== "string")
|
|
19995
|
+
return "relation missing config.database";
|
|
19996
|
+
return null;
|
|
19997
|
+
}
|
|
19998
|
+
function detectBadConfig(db) {
|
|
19999
|
+
const rows = db.query("SELECT id, type, config FROM properties WHERE __deleted = 0 ORDER BY id").all();
|
|
20000
|
+
const out = [];
|
|
20001
|
+
for (const r of rows) {
|
|
20002
|
+
let config = null;
|
|
20003
|
+
try {
|
|
20004
|
+
config = r.config ? JSON.parse(r.config) : null;
|
|
20005
|
+
} catch {
|
|
20006
|
+
out.push({ category: "bad_config", entity: "properties", id: r.id, detail: "config is not valid JSON", fixable: false });
|
|
20007
|
+
continue;
|
|
20008
|
+
}
|
|
20009
|
+
const problem = configIssue(r.type, config);
|
|
20010
|
+
if (problem)
|
|
20011
|
+
out.push({ category: "bad_config", entity: "properties", id: r.id, detail: problem, fixable: false });
|
|
20012
|
+
}
|
|
20013
|
+
return out;
|
|
20014
|
+
}
|
|
20015
|
+
function detectClockSkew(db, now) {
|
|
20016
|
+
const rows = db.query("SELECT node_id, COUNT(*) AS n FROM crdt_changes WHERE hlc > ? GROUP BY node_id").all(hlcSkewBound(now));
|
|
20017
|
+
const issues = rows.map((r) => ({
|
|
20018
|
+
category: "clock_skew",
|
|
20019
|
+
entity: "nodes",
|
|
20020
|
+
id: r.node_id,
|
|
20021
|
+
detail: `${r.n} change(s) stamped more than 5 min in the future \u2014 run \`mh repair --clock\``,
|
|
20022
|
+
fixable: false
|
|
20023
|
+
}));
|
|
20024
|
+
const local = persistedHlc(db);
|
|
20025
|
+
if (local && local.millis > now + HLC_MAX_SKEW_MS)
|
|
20026
|
+
issues.push({
|
|
20027
|
+
category: "clock_skew",
|
|
20028
|
+
entity: "meta",
|
|
20029
|
+
id: "hlc",
|
|
20030
|
+
detail: `local logical clock is ${Math.ceil((local.millis - now) / 60000)} min ahead of wall time \u2014 writes refuse until \`mh repair --clock\``,
|
|
20031
|
+
fixable: false
|
|
20032
|
+
});
|
|
20033
|
+
return issues;
|
|
20034
|
+
}
|
|
20035
|
+
function repairClock(db, now = Date.now()) {
|
|
20036
|
+
return writeTx(db, () => {
|
|
20037
|
+
const rows = db.query(`SELECT seq, ${CHANGE_SELECT} FROM crdt_changes WHERE hlc > ? ORDER BY hlc, seq`).all(hlcSkewBound(now));
|
|
20038
|
+
const byNode = {};
|
|
20039
|
+
const local = persistedHlc(db);
|
|
20040
|
+
const clockReset = rows.length > 0 || local != null && local.millis > now + HLC_MAX_SKEW_MS;
|
|
20041
|
+
if (clockReset)
|
|
20042
|
+
resetHlc(db, getNodeId(db), now);
|
|
20043
|
+
if (rows.length === 0)
|
|
20044
|
+
return { restamped: 0, byNode, clockReset };
|
|
20045
|
+
const touched = new Map;
|
|
20046
|
+
for (const r of rows) {
|
|
20047
|
+
db.query("DELETE FROM crdt_changes WHERE seq = ?").run(r.seq);
|
|
20048
|
+
const { seq: _seq, ...c } = r;
|
|
20049
|
+
const change = { ...c, hlc: nextHlc(db, r.node_id, now) };
|
|
20050
|
+
applyChange(db, change);
|
|
20051
|
+
touched.set(`${c.dataset}\x00${c.row_id}\x00${c.col}`, change);
|
|
20052
|
+
byNode[r.node_id] = (byNode[r.node_id] ?? 0) + 1;
|
|
20053
|
+
}
|
|
20054
|
+
for (const c of touched.values()) {
|
|
20055
|
+
const winner = db.query(`SELECT ${CHANGE_SELECT} FROM crdt_changes WHERE dataset = ? AND row_id = ? AND col = ? ORDER BY hlc DESC LIMIT 1`).get(c.dataset, c.row_id, c.col);
|
|
20056
|
+
if (winner)
|
|
20057
|
+
applyChange(db, winner);
|
|
20058
|
+
}
|
|
20059
|
+
invalidateHistory(db);
|
|
20060
|
+
return { restamped: rows.length, byNode, clockReset };
|
|
20061
|
+
});
|
|
20062
|
+
}
|
|
20063
|
+
function validateHub(db, opts = {}) {
|
|
20064
|
+
const issues = [
|
|
20065
|
+
...detectBrokenRefs(db),
|
|
20066
|
+
...detectOrphanCells(db),
|
|
20067
|
+
...detectDeadCellRefs(db),
|
|
20068
|
+
...detectDupPaths(db),
|
|
20069
|
+
...detectCycleIssues(db),
|
|
20070
|
+
...detectDupNames(db),
|
|
20071
|
+
...detectBadConfig(db),
|
|
20072
|
+
...detectClockSkew(db, opts.now ?? Date.now())
|
|
20073
|
+
];
|
|
20074
|
+
const counts = {};
|
|
20075
|
+
for (const i of issues)
|
|
20076
|
+
counts[i.category] = (counts[i.category] ?? 0) + 1;
|
|
20077
|
+
return { ok: issues.length === 0, total: issues.length, counts, issues };
|
|
20078
|
+
}
|
|
20079
|
+
function repairPass(db) {
|
|
20080
|
+
const fixed = {};
|
|
20081
|
+
const bump = (cat, n) => {
|
|
20082
|
+
if (n)
|
|
20083
|
+
fixed[cat] = (fixed[cat] ?? 0) + n;
|
|
20084
|
+
};
|
|
20085
|
+
bump("broken_ref", repairBrokenRefs(db));
|
|
20086
|
+
bump("orphan_cell", repairOrphanCells(db));
|
|
20087
|
+
bump("dead_cell_ref", repairDeadCellRefs(db));
|
|
20088
|
+
bump("dup_path", repairDupPaths(db));
|
|
20089
|
+
bump("parent_cycle", repairParentCycles(db));
|
|
20090
|
+
const applied = Object.values(fixed).reduce((a, b) => a + b, 0);
|
|
20091
|
+
return { applied, fixed };
|
|
20092
|
+
}
|
|
20093
|
+
var repairHub = grouped(function repairHub(db) {
|
|
20094
|
+
const fixed = {};
|
|
20095
|
+
let applied = 0;
|
|
20096
|
+
for (let i = 0;i < 20; i++) {
|
|
20097
|
+
const pass = repairPass(db);
|
|
20098
|
+
if (pass.applied === 0)
|
|
20099
|
+
break;
|
|
20100
|
+
applied += pass.applied;
|
|
20101
|
+
for (const [cat, n] of Object.entries(pass.fixed))
|
|
20102
|
+
fixed[cat] = (fixed[cat] ?? 0) + n;
|
|
20103
|
+
}
|
|
20104
|
+
return { applied, fixed, remaining: validateHub(db) };
|
|
20105
|
+
}, "repair");
|
|
20106
|
+
|
|
20107
|
+
// src/webui/server/routes.ts
|
|
18779
20108
|
init_errors();
|
|
18780
20109
|
init_routes();
|
|
18781
20110
|
init_databases();
|
|
@@ -18788,6 +20117,7 @@ init_crdt();
|
|
|
18788
20117
|
init_documents();
|
|
18789
20118
|
init_properties();
|
|
18790
20119
|
init_errors();
|
|
20120
|
+
init_history_cache();
|
|
18791
20121
|
var REVISION_GAP_MS = 1500;
|
|
18792
20122
|
function hlcMillis(hlc) {
|
|
18793
20123
|
return Number(hlc.slice(0, 15));
|
|
@@ -18845,16 +20175,23 @@ function rowChanges(db, dataset, rowId) {
|
|
|
18845
20175
|
function everBlockIds(db, docId) {
|
|
18846
20176
|
return db.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);
|
|
18847
20177
|
}
|
|
18848
|
-
function
|
|
20178
|
+
function docHistory(db, docId) {
|
|
20179
|
+
const cached = getDocHistory(db, docId);
|
|
20180
|
+
if (cached)
|
|
20181
|
+
return cached;
|
|
18849
20182
|
const blocks = everBlockIds(db, docId);
|
|
18850
20183
|
const placeholders = blocks.map(() => "?").join(",");
|
|
18851
20184
|
const blockClause = blocks.length ? ` OR (dataset = 'doc_blocks' AND row_id IN (${placeholders}))` : "";
|
|
18852
|
-
|
|
20185
|
+
const changes = db.query(`SELECT ${CHANGE_SELECT} FROM crdt_changes
|
|
18853
20186
|
WHERE (dataset = 'documents' AND row_id = ?)${blockClause}
|
|
18854
20187
|
ORDER BY hlc`).all(docId, ...blocks);
|
|
20188
|
+
return setDocHistory(db, docId, changes, blocks);
|
|
18855
20189
|
}
|
|
18856
20190
|
function listDocumentRevisions(db, id) {
|
|
18857
|
-
const
|
|
20191
|
+
const entry = docHistory(db, id);
|
|
20192
|
+
if (entry.revisions)
|
|
20193
|
+
return entry.revisions.slice();
|
|
20194
|
+
const changes = entry.changes;
|
|
18858
20195
|
if (!changes.length)
|
|
18859
20196
|
throw new MhError("not_found", `no such document: ${id}`);
|
|
18860
20197
|
const reg = new Map;
|
|
@@ -18909,12 +20246,17 @@ function listDocumentRevisions(db, id) {
|
|
|
18909
20246
|
blocks_deleted: deletedBlocks.size
|
|
18910
20247
|
});
|
|
18911
20248
|
}
|
|
18912
|
-
|
|
20249
|
+
entry.revisions = out.reverse();
|
|
20250
|
+
return entry.revisions.slice();
|
|
18913
20251
|
}
|
|
18914
20252
|
function documentAtVersion(db, id, at) {
|
|
18915
20253
|
if (!at)
|
|
18916
20254
|
throw new MhError("invalid_input", "missing version cutoff");
|
|
18917
|
-
const
|
|
20255
|
+
const entry = docHistory(db, id);
|
|
20256
|
+
const hit = entry.states.get(at);
|
|
20257
|
+
if (hit)
|
|
20258
|
+
return { ...hit };
|
|
20259
|
+
const changes = entry.changes.filter((c) => c.hlc <= at);
|
|
18918
20260
|
if (!changes.length)
|
|
18919
20261
|
throw new MhError("not_found", `no version of document ${id} at or before ${at}`);
|
|
18920
20262
|
const docRegs = registersAt(changes.filter((c) => c.dataset === "documents"), at);
|
|
@@ -18960,13 +20302,15 @@ function documentAtVersion(db, id, at) {
|
|
|
18960
20302
|
return a.id < b.id ? -1 : 1;
|
|
18961
20303
|
});
|
|
18962
20304
|
const body = byBlock.size ? serializeDocBlocks(blocks) : parse("body") ?? "";
|
|
18963
|
-
|
|
20305
|
+
const state = {
|
|
18964
20306
|
id,
|
|
18965
20307
|
title: parse("title") ?? "",
|
|
18966
20308
|
body,
|
|
18967
20309
|
deleted: docRegs.get("__deleted") ? flagSet(docRegs.get("__deleted").value) : false,
|
|
18968
20310
|
version: changes[changes.length - 1].hlc
|
|
18969
20311
|
};
|
|
20312
|
+
rememberDocState(entry, at, state);
|
|
20313
|
+
return { ...state };
|
|
18970
20314
|
}
|
|
18971
20315
|
var revertDocument = grouped(function revertDocument(db, id, to, opts = {}) {
|
|
18972
20316
|
let cur = getDocument(db, id);
|
|
@@ -19270,15 +20614,49 @@ function recordFieldHistory(db, id, propId) {
|
|
|
19270
20614
|
// src/core/audit.ts
|
|
19271
20615
|
init_crdt();
|
|
19272
20616
|
init_errors();
|
|
20617
|
+
|
|
20618
|
+
// src/core/tables.ts
|
|
20619
|
+
var TABLES = {
|
|
20620
|
+
databases: { tier: "content", synced: true },
|
|
20621
|
+
properties: { tier: "content", synced: true },
|
|
20622
|
+
records: { tier: "content", synced: true },
|
|
20623
|
+
documents: { tier: "content", synced: true },
|
|
20624
|
+
doc_blocks: { tier: "content", synced: true },
|
|
20625
|
+
sites: { tier: "content", synced: true },
|
|
20626
|
+
site_files: { tier: "content", synced: true },
|
|
20627
|
+
site_channels: { tier: "system", synced: true },
|
|
20628
|
+
blob_policy: { tier: "system", synced: true },
|
|
20629
|
+
nodes: { tier: "system", synced: true },
|
|
20630
|
+
meta: { tier: "local", synced: false },
|
|
20631
|
+
crdt_changes: { tier: "local", synced: false },
|
|
20632
|
+
peers: { tier: "local", synced: false },
|
|
20633
|
+
storage_cursors: { tier: "local", synced: false },
|
|
20634
|
+
room_rows: { tier: "local", synced: false },
|
|
20635
|
+
peer_grants: { tier: "local", synced: false },
|
|
20636
|
+
pairing_codes: { tier: "local", synced: false },
|
|
20637
|
+
shares: { tier: "local", synced: false },
|
|
20638
|
+
site_channel_observations: { tier: "local", synced: false },
|
|
20639
|
+
blob_cache: { tier: "local", synced: false },
|
|
20640
|
+
drop_rejects: { tier: "local", synced: false },
|
|
20641
|
+
search_fts: { tier: "local", synced: false }
|
|
20642
|
+
};
|
|
20643
|
+
var names = (pred) => Object.entries(TABLES).filter(([, s]) => pred(s)).map(([n]) => n);
|
|
20644
|
+
var SYNCED_TABLES = names((s) => s.synced);
|
|
20645
|
+
var CONTENT_TABLES = names((s) => s.tier === "content");
|
|
20646
|
+
var SYSTEM_DATASETS = names((s) => s.tier === "system");
|
|
20647
|
+
var LOCAL_TABLES = names((s) => s.tier === "local");
|
|
20648
|
+
|
|
20649
|
+
// src/core/audit.ts
|
|
19273
20650
|
function isMetaCol(dataset, col) {
|
|
19274
20651
|
if (dataset === "records")
|
|
19275
20652
|
return RECORD_META.has(col);
|
|
19276
20653
|
return col === "created_hlc" || col === "__deleted";
|
|
19277
20654
|
}
|
|
19278
20655
|
function oplogExclude() {
|
|
20656
|
+
const excluded = [...OPLOG_ONLY_DATASETS, ...SYSTEM_DATASETS];
|
|
19279
20657
|
return {
|
|
19280
|
-
sql:
|
|
19281
|
-
params:
|
|
20658
|
+
sql: excluded.map(() => "dataset != ?"),
|
|
20659
|
+
params: excluded
|
|
19282
20660
|
};
|
|
19283
20661
|
}
|
|
19284
20662
|
function changesOfTxn(db, txn, beforeHlc) {
|
|
@@ -19564,6 +20942,7 @@ var revertChangeGroup = grouped(function revertChangeGroup(db, txn) {
|
|
|
19564
20942
|
}, "revert");
|
|
19565
20943
|
|
|
19566
20944
|
// src/webui/server/routes.ts
|
|
20945
|
+
init_resolve();
|
|
19567
20946
|
init_node();
|
|
19568
20947
|
init_blobs();
|
|
19569
20948
|
init_config();
|
|
@@ -19728,7 +21107,7 @@ async function createBucketShare(db, opts) {
|
|
|
19728
21107
|
contentType: "application/octet-stream"
|
|
19729
21108
|
});
|
|
19730
21109
|
await mergeViewerCors(opts.config, opts.viewerOrigin);
|
|
19731
|
-
return { manifestUrl, presignExp, keyB64: opts.password ? undefined : toB64(shareKey), saltB64, title };
|
|
21110
|
+
return { manifestUrl, presignExp, keyB64: opts.password ? undefined : toB64(shareKey), saltB64, title, meta };
|
|
19732
21111
|
}
|
|
19733
21112
|
async function renewBucketShare(db, config, slug, expiresSec = MAX_PRESIGN_SECONDS) {
|
|
19734
21113
|
const meta = await readMeta2(config, slug);
|
|
@@ -19795,7 +21174,7 @@ async function represignBucketShare(config, slug, expiresSec = MAX_PRESIGN_SECON
|
|
|
19795
21174
|
async function listBucketShares(config) {
|
|
19796
21175
|
const client = storageClientFor(config);
|
|
19797
21176
|
const root = sharesRoot(config.prefix);
|
|
19798
|
-
const entries = await client.list(`${root}/`, undefined, "/")
|
|
21177
|
+
const entries = await client.list(`${root}/`, undefined, "/");
|
|
19799
21178
|
const slugs = entries.map((e) => e.key.slice(`${root}/`.length).replace(/\/$/, "")).filter((s) => s && !s.includes("/"));
|
|
19800
21179
|
const out = [];
|
|
19801
21180
|
for (const slug of slugs) {
|
|
@@ -19824,10 +21203,156 @@ function clampExpiry(sec) {
|
|
|
19824
21203
|
|
|
19825
21204
|
// src/core/sync/share-actions.ts
|
|
19826
21205
|
init_storage_s3_sign();
|
|
21206
|
+
|
|
21207
|
+
// src/core/sync/remote-shares-cache.ts
|
|
21208
|
+
init_peers();
|
|
21209
|
+
var caches2 = new WeakMap;
|
|
21210
|
+
var listeners = new Set;
|
|
21211
|
+
var REMOTE_SHARES_STALE_MS = 60000;
|
|
21212
|
+
var PEER_LIST_TIMEOUT_MS = 4000;
|
|
21213
|
+
function absoluteShareUrl(url, base) {
|
|
21214
|
+
try {
|
|
21215
|
+
return new URL(url, `${base.replace(/\/+$/, "")}/`).toString();
|
|
21216
|
+
} catch {
|
|
21217
|
+
return url;
|
|
21218
|
+
}
|
|
21219
|
+
}
|
|
21220
|
+
function bucketShareItem(bucket, m) {
|
|
21221
|
+
return {
|
|
21222
|
+
slug: m.slug,
|
|
21223
|
+
kind: m.kind,
|
|
21224
|
+
target_id: m.target_id,
|
|
21225
|
+
title: m.title || m.target_id,
|
|
21226
|
+
permission: m.permission,
|
|
21227
|
+
transport: "s3",
|
|
21228
|
+
source: `\u6876 ${bucket.label ?? bucket.url}`,
|
|
21229
|
+
sourceKind: "bucket",
|
|
21230
|
+
hosting: "s3",
|
|
21231
|
+
expiresAt: m.presign_exp,
|
|
21232
|
+
hasPassword: m.has_password,
|
|
21233
|
+
contentUpdatedAt: m.content_updated_at ?? m.created_at
|
|
21234
|
+
};
|
|
21235
|
+
}
|
|
21236
|
+
async function fetchPeerShares(url, token, targetId) {
|
|
21237
|
+
const u = `${url.replace(/\/+$/, "")}/api/shares${targetId ? `?target=${encodeURIComponent(targetId)}` : ""}`;
|
|
21238
|
+
const res = await Promise.race([
|
|
21239
|
+
fetch(u, { headers: { authorization: `Bearer ${token}` } }),
|
|
21240
|
+
new Promise((_r, rej) => setTimeout(() => rej(new Error("timeout")), PEER_LIST_TIMEOUT_MS))
|
|
21241
|
+
]);
|
|
21242
|
+
if (!res.ok)
|
|
21243
|
+
return [];
|
|
21244
|
+
return (await res.json()).map((item) => ({
|
|
21245
|
+
...item,
|
|
21246
|
+
...item.url ? { url: absoluteShareUrl(item.url, url) } : {}
|
|
21247
|
+
}));
|
|
21248
|
+
}
|
|
21249
|
+
function remoteShareSources(db) {
|
|
21250
|
+
return listPeers(db).flatMap((p) => {
|
|
21251
|
+
if (p.kind === "s3" && p.config)
|
|
21252
|
+
return [{ url: p.url, kind: "bucket", peer: p }];
|
|
21253
|
+
if (p.kind === "http" && p.token)
|
|
21254
|
+
return [{ url: p.url, kind: "peer", peer: p }];
|
|
21255
|
+
return [];
|
|
21256
|
+
});
|
|
21257
|
+
}
|
|
21258
|
+
async function fetchSource(s) {
|
|
21259
|
+
if (s.kind === "bucket") {
|
|
21260
|
+
const config = JSON.parse(s.peer.config);
|
|
21261
|
+
return (await listBucketShares(config)).map((m) => bucketShareItem(s.peer, m));
|
|
21262
|
+
}
|
|
21263
|
+
return (await fetchPeerShares(s.url, s.peer.token)).map((it) => ({
|
|
21264
|
+
...it,
|
|
21265
|
+
source: s.peer.label ?? s.url,
|
|
21266
|
+
sourceKind: "peer",
|
|
21267
|
+
sourceUrl: s.url
|
|
21268
|
+
}));
|
|
21269
|
+
}
|
|
21270
|
+
function cacheFor2(db) {
|
|
21271
|
+
let m = caches2.get(db);
|
|
21272
|
+
if (!m) {
|
|
21273
|
+
m = new Map;
|
|
21274
|
+
caches2.set(db, m);
|
|
21275
|
+
}
|
|
21276
|
+
return m;
|
|
21277
|
+
}
|
|
21278
|
+
function entryFor(db, url) {
|
|
21279
|
+
const m = cacheFor2(db);
|
|
21280
|
+
let e = m.get(url);
|
|
21281
|
+
if (!e) {
|
|
21282
|
+
e = { items: [], fetchedAt: 0, error: null, inflight: null };
|
|
21283
|
+
m.set(url, e);
|
|
21284
|
+
}
|
|
21285
|
+
return e;
|
|
21286
|
+
}
|
|
21287
|
+
function notify(db) {
|
|
21288
|
+
for (const fn of [...listeners])
|
|
21289
|
+
fn(db);
|
|
21290
|
+
}
|
|
21291
|
+
function refreshRemoteSource(db, s) {
|
|
21292
|
+
const e = entryFor(db, s.url);
|
|
21293
|
+
if (e.inflight)
|
|
21294
|
+
return e.inflight;
|
|
21295
|
+
e.inflight = fetchSource(s).then((items) => {
|
|
21296
|
+
const changed = JSON.stringify(items) !== JSON.stringify(e.items);
|
|
21297
|
+
e.items = items;
|
|
21298
|
+
e.error = null;
|
|
21299
|
+
e.fetchedAt = Date.now();
|
|
21300
|
+
if (changed)
|
|
21301
|
+
notify(db);
|
|
21302
|
+
}).catch((err) => {
|
|
21303
|
+
e.error = err instanceof Error ? err.message : String(err);
|
|
21304
|
+
e.fetchedAt = Date.now();
|
|
21305
|
+
}).finally(() => {
|
|
21306
|
+
e.inflight = null;
|
|
21307
|
+
});
|
|
21308
|
+
return e.inflight;
|
|
21309
|
+
}
|
|
21310
|
+
function cachedRemoteShares(db, targetId, opts = {}) {
|
|
21311
|
+
const now = opts.now ?? Date.now();
|
|
21312
|
+
const sources = remoteShareSources(db);
|
|
21313
|
+
const m = cacheFor2(db);
|
|
21314
|
+
const live = new Set(sources.map((s) => s.url));
|
|
21315
|
+
for (const key of [...m.keys()])
|
|
21316
|
+
if (!live.has(key))
|
|
21317
|
+
m.delete(key);
|
|
21318
|
+
const out = [];
|
|
21319
|
+
for (const s of sources) {
|
|
21320
|
+
const e = entryFor(db, s.url);
|
|
21321
|
+
if (opts.refresh !== false && !e.inflight && now - e.fetchedAt >= REMOTE_SHARES_STALE_MS)
|
|
21322
|
+
refreshRemoteSource(db, s);
|
|
21323
|
+
for (const it of e.items)
|
|
21324
|
+
if (!targetId || it.target_id === targetId)
|
|
21325
|
+
out.push(it);
|
|
21326
|
+
}
|
|
21327
|
+
return out;
|
|
21328
|
+
}
|
|
21329
|
+
function primeRemoteShare(db, url, item) {
|
|
21330
|
+
const e = entryFor(db, url);
|
|
21331
|
+
e.items = [...e.items.filter((it) => it.slug !== item.slug), item];
|
|
21332
|
+
}
|
|
21333
|
+
function dropRemoteShare(db, url, slug) {
|
|
21334
|
+
const e = caches2.get(db)?.get(url);
|
|
21335
|
+
if (e)
|
|
21336
|
+
e.items = e.items.filter((it) => it.slug !== slug);
|
|
21337
|
+
}
|
|
21338
|
+
function findCachedBucketShare(db, slug) {
|
|
21339
|
+
const m = caches2.get(db);
|
|
21340
|
+
if (!m)
|
|
21341
|
+
return null;
|
|
21342
|
+
for (const s of remoteShareSources(db)) {
|
|
21343
|
+
if (s.kind !== "bucket")
|
|
21344
|
+
continue;
|
|
21345
|
+
const item = m.get(s.url)?.items.find((it) => it.slug === slug);
|
|
21346
|
+
if (item)
|
|
21347
|
+
return { source: s, item };
|
|
21348
|
+
}
|
|
21349
|
+
return null;
|
|
21350
|
+
}
|
|
21351
|
+
|
|
21352
|
+
// src/core/sync/share-actions.ts
|
|
19827
21353
|
init_site_channel_store();
|
|
19828
21354
|
var DEFAULT_SHARE_VIEWER = "https://share.mh.tensorix.org";
|
|
19829
21355
|
var DEFAULT_S3_EXPIRY_SEC = MAX_PRESIGN_SECONDS;
|
|
19830
|
-
var PEER_LIST_TIMEOUT_MS = 4000;
|
|
19831
21356
|
var PEER_SHARE_TIMEOUT_MS = 1e4;
|
|
19832
21357
|
var CreatedShareWireSchema = object({
|
|
19833
21358
|
slug: string2().min(1).max(128),
|
|
@@ -19924,13 +21449,6 @@ function bucketLink(base, manifestUrl, keyB64, saltB64) {
|
|
|
19924
21449
|
const frag = `m=${encodeURIComponent(manifestUrl)}&` + (keyB64 ? `k=${keyB64}` : `s=${saltB64}`);
|
|
19925
21450
|
return `${base}/#${frag}`;
|
|
19926
21451
|
}
|
|
19927
|
-
function absoluteShareUrl(url, base) {
|
|
19928
|
-
try {
|
|
19929
|
-
return new URL(url, `${base.replace(/\/+$/, "")}/`).toString();
|
|
19930
|
-
} catch {
|
|
19931
|
-
return url;
|
|
19932
|
-
}
|
|
19933
|
-
}
|
|
19934
21452
|
function assertShareCombo(req) {
|
|
19935
21453
|
const fail = (msg) => {
|
|
19936
21454
|
throw new MhError("invalid_input", msg);
|
|
@@ -19968,6 +21486,7 @@ async function createShareAction(db, req) {
|
|
|
19968
21486
|
expiresSec: req.expiresMs != null ? Math.floor(req.expiresMs / 1000) : DEFAULT_S3_EXPIRY_SEC,
|
|
19969
21487
|
viewerOrigin: viewerOriginOf(base)
|
|
19970
21488
|
});
|
|
21489
|
+
primeRemoteShare(db, bucketUrl, bucketShareItem({ url: bucketUrl, label }, out.meta));
|
|
19971
21490
|
return {
|
|
19972
21491
|
slug,
|
|
19973
21492
|
kind: req.kind,
|
|
@@ -20157,28 +21676,21 @@ function listServerSharesLocal(db, targetId) {
|
|
|
20157
21676
|
}
|
|
20158
21677
|
return out;
|
|
20159
21678
|
}
|
|
20160
|
-
async function listSharesLocal(db, targetId) {
|
|
21679
|
+
async function listSharesLocal(db, targetId, opts = {}) {
|
|
20161
21680
|
const out = listServerSharesLocal(db, targetId);
|
|
21681
|
+
if (opts.mode === "cached") {
|
|
21682
|
+
for (const it of cachedRemoteShares(db, targetId))
|
|
21683
|
+
if (it.sourceKind === "bucket")
|
|
21684
|
+
out.push(it);
|
|
21685
|
+
return out;
|
|
21686
|
+
}
|
|
20162
21687
|
for (const p of listPeers(db).filter((x) => x.kind === "s3" && x.config)) {
|
|
20163
21688
|
const config = JSON.parse(p.config);
|
|
20164
21689
|
const metas = await listBucketShares(config).catch(() => []);
|
|
20165
21690
|
for (const m of metas) {
|
|
20166
21691
|
if (targetId && m.target_id !== targetId)
|
|
20167
21692
|
continue;
|
|
20168
|
-
out.push(
|
|
20169
|
-
slug: m.slug,
|
|
20170
|
-
kind: m.kind,
|
|
20171
|
-
target_id: m.target_id,
|
|
20172
|
-
title: m.title || m.target_id,
|
|
20173
|
-
permission: m.permission,
|
|
20174
|
-
transport: "s3",
|
|
20175
|
-
source: `\u6876 ${p.label ?? p.url}`,
|
|
20176
|
-
sourceKind: "bucket",
|
|
20177
|
-
hosting: "s3",
|
|
20178
|
-
expiresAt: m.presign_exp,
|
|
20179
|
-
hasPassword: m.has_password,
|
|
20180
|
-
contentUpdatedAt: m.content_updated_at ?? m.created_at
|
|
20181
|
-
});
|
|
21693
|
+
out.push(bucketShareItem(p, m));
|
|
20182
21694
|
}
|
|
20183
21695
|
}
|
|
20184
21696
|
return out;
|
|
@@ -20208,10 +21720,16 @@ async function revokeShareByRequestId(db, requestId) {
|
|
|
20208
21720
|
const share = getShareByRequestId(db, requestId);
|
|
20209
21721
|
return share ? revokeShareAction(db, share.slug) : { ok: false, status: "not_found" };
|
|
20210
21722
|
}
|
|
20211
|
-
async function listSharesAggregated(db, targetId) {
|
|
21723
|
+
async function listSharesAggregated(db, targetId, opts = {}) {
|
|
20212
21724
|
const bySlug = new Map;
|
|
20213
|
-
for (const item of await listSharesLocal(db, targetId))
|
|
21725
|
+
for (const item of await listSharesLocal(db, targetId, opts))
|
|
20214
21726
|
bySlug.set(item.slug, item);
|
|
21727
|
+
if (opts.mode === "cached") {
|
|
21728
|
+
for (const it of cachedRemoteShares(db, targetId, { refresh: false }))
|
|
21729
|
+
if (it.sourceKind === "peer" && !bySlug.has(it.slug))
|
|
21730
|
+
bySlug.set(it.slug, it);
|
|
21731
|
+
return [...bySlug.values()];
|
|
21732
|
+
}
|
|
20215
21733
|
await Promise.all(listShareServers(db).map(async ({ url, label }) => {
|
|
20216
21734
|
const peer = getPeer2(db, url);
|
|
20217
21735
|
if (!peer?.token)
|
|
@@ -20229,19 +21747,6 @@ async function listSharesAggregated(db, targetId) {
|
|
|
20229
21747
|
}));
|
|
20230
21748
|
return [...bySlug.values()];
|
|
20231
21749
|
}
|
|
20232
|
-
async function fetchPeerShares(url, token, targetId) {
|
|
20233
|
-
const u = `${url.replace(/\/+$/, "")}/api/shares${targetId ? `?target=${encodeURIComponent(targetId)}` : ""}`;
|
|
20234
|
-
const res = await Promise.race([
|
|
20235
|
-
fetch(u, { headers: { authorization: `Bearer ${token}` } }),
|
|
20236
|
-
new Promise((_r, rej) => setTimeout(() => rej(new Error("timeout")), PEER_LIST_TIMEOUT_MS))
|
|
20237
|
-
]);
|
|
20238
|
-
if (!res.ok)
|
|
20239
|
-
return [];
|
|
20240
|
-
return (await res.json()).map((item) => ({
|
|
20241
|
-
...item,
|
|
20242
|
-
...item.url ? { url: absoluteShareUrl(item.url, url) } : {}
|
|
20243
|
-
}));
|
|
20244
|
-
}
|
|
20245
21750
|
async function revokeShareAction(db, slug) {
|
|
20246
21751
|
if (getShare(db, slug)) {
|
|
20247
21752
|
const channels = listSiteChannelRows(db).filter((channel) => channel.audience === "link" && channel.target_ref === slug && channel.desired_state === "active");
|
|
@@ -20270,37 +21775,50 @@ async function revokeShareAction(db, slug) {
|
|
|
20270
21775
|
status: "revoked"
|
|
20271
21776
|
};
|
|
20272
21777
|
}
|
|
21778
|
+
const owner = await bucketOwning(db, slug);
|
|
21779
|
+
if (!owner)
|
|
21780
|
+
return { ok: false, status: "not_found" };
|
|
21781
|
+
await deleteBucketShareObjects(JSON.parse(owner.peer.config), slug);
|
|
21782
|
+
dropRemoteShare(db, owner.peer.url, slug);
|
|
21783
|
+
return { ok: true, status: "revoked" };
|
|
21784
|
+
}
|
|
21785
|
+
async function bucketOwning(db, slug) {
|
|
21786
|
+
const cached = findCachedBucketShare(db, slug);
|
|
21787
|
+
if (cached)
|
|
21788
|
+
return { peer: cached.source.peer, kind: cached.item.kind, item: cached.item };
|
|
20273
21789
|
for (const p of listPeers(db).filter((x) => x.kind === "s3" && x.config)) {
|
|
20274
|
-
const
|
|
20275
|
-
const
|
|
20276
|
-
if (
|
|
20277
|
-
|
|
20278
|
-
return { ok: true, status: "revoked" };
|
|
20279
|
-
}
|
|
21790
|
+
const metas = await listBucketShares(JSON.parse(p.config)).catch(() => []);
|
|
21791
|
+
const m = metas.find((x) => x.slug === slug);
|
|
21792
|
+
if (m)
|
|
21793
|
+
return { peer: p, kind: m.kind, item: null };
|
|
20280
21794
|
}
|
|
20281
|
-
return
|
|
21795
|
+
return null;
|
|
20282
21796
|
}
|
|
20283
21797
|
async function renewShareAction(db, slug, viewerBaseOverride, opts) {
|
|
20284
|
-
|
|
20285
|
-
|
|
20286
|
-
|
|
20287
|
-
|
|
20288
|
-
|
|
20289
|
-
|
|
20290
|
-
|
|
20291
|
-
|
|
20292
|
-
|
|
20293
|
-
|
|
20294
|
-
|
|
20295
|
-
permission: "view",
|
|
20296
|
-
transport: "s3",
|
|
20297
|
-
hosting: "s3",
|
|
20298
|
-
url: bucketLink(base, out.manifestUrl, out.keyB64, out.saltB64),
|
|
21798
|
+
const owner = await bucketOwning(db, slug);
|
|
21799
|
+
if (!owner)
|
|
21800
|
+
throw new MhError("not_found", `no such object-storage share: ${slug}`);
|
|
21801
|
+
const { peer: p } = owner;
|
|
21802
|
+
const config = JSON.parse(p.config);
|
|
21803
|
+
const base = viewerBase(db, viewerBaseOverride);
|
|
21804
|
+
const out = opts?.refreshContent ? await renewBucketShare(db, config, slug) : await represignBucketShare(config, slug);
|
|
21805
|
+
if (owner.item)
|
|
21806
|
+
primeRemoteShare(db, p.url, {
|
|
21807
|
+
...owner.item,
|
|
21808
|
+
title: out.title || owner.item.title,
|
|
20299
21809
|
expiresAt: out.presignExp,
|
|
20300
|
-
|
|
20301
|
-
};
|
|
20302
|
-
|
|
20303
|
-
|
|
21810
|
+
...opts?.refreshContent ? { contentUpdatedAt: Date.now() } : {}
|
|
21811
|
+
});
|
|
21812
|
+
return {
|
|
21813
|
+
slug,
|
|
21814
|
+
kind: owner.kind,
|
|
21815
|
+
permission: "view",
|
|
21816
|
+
transport: "s3",
|
|
21817
|
+
hosting: "s3",
|
|
21818
|
+
url: bucketLink(base, out.manifestUrl, out.keyB64, out.saltB64),
|
|
21819
|
+
expiresAt: out.presignExp,
|
|
21820
|
+
source: `\u6876 ${p.label ?? p.url}`
|
|
21821
|
+
};
|
|
20304
21822
|
}
|
|
20305
21823
|
function freshSlug(db) {
|
|
20306
21824
|
let slug = randomSuffix(12);
|
|
@@ -20399,7 +21917,7 @@ var shareRoutes = [
|
|
|
20399
21917
|
handler: async (req, { db }) => {
|
|
20400
21918
|
try {
|
|
20401
21919
|
const target = new URL(req.url).searchParams.get("target") ?? undefined;
|
|
20402
|
-
return Response.json(await listSharesLocal(db, target));
|
|
21920
|
+
return Response.json(await listSharesLocal(db, target, { mode: "cached" }));
|
|
20403
21921
|
} catch (e) {
|
|
20404
21922
|
return errorResponse(e);
|
|
20405
21923
|
}
|
|
@@ -20413,7 +21931,7 @@ var shareRoutes = [
|
|
|
20413
21931
|
handler: async (req, { db }) => {
|
|
20414
21932
|
try {
|
|
20415
21933
|
const target = new URL(req.url).searchParams.get("target") ?? undefined;
|
|
20416
|
-
return Response.json(await listSharesAggregated(db, target));
|
|
21934
|
+
return Response.json(await listSharesAggregated(db, target, { mode: "cached" }));
|
|
20417
21935
|
} catch (e) {
|
|
20418
21936
|
return errorResponse(e);
|
|
20419
21937
|
}
|
|
@@ -20704,7 +22222,7 @@ function jsonHandler(fn) {
|
|
|
20704
22222
|
try {
|
|
20705
22223
|
return Response.json(await fn(req, ctx) ?? null);
|
|
20706
22224
|
} catch (e) {
|
|
20707
|
-
return errorResponse(e);
|
|
22225
|
+
return errorResponse(e, req);
|
|
20708
22226
|
}
|
|
20709
22227
|
};
|
|
20710
22228
|
}
|
|
@@ -20874,20 +22392,24 @@ async function verifyBase(input, expectedNode, allowRemote = true) {
|
|
|
20874
22392
|
headers: { accept: "application/json" }
|
|
20875
22393
|
});
|
|
20876
22394
|
if (res.status < 200 || res.status >= 300)
|
|
20877
|
-
throw
|
|
22395
|
+
throw mhError("network", "\u7AD9\u70B9\u5165\u53E3\u5065\u5EB7\u68C0\u67E5\u5931\u8D25\uFF08HTTP {status}\uFF09", { status: res.status });
|
|
20878
22396
|
const body = await limitedJson(res, HEALTH_RESPONSE_MAX);
|
|
20879
22397
|
if (!body?.ok || !body.node)
|
|
20880
22398
|
throw new MhError("network", "\u7AD9\u70B9\u5165\u53E3\u5065\u5EB7\u54CD\u5E94\u65E0\u6548");
|
|
20881
22399
|
if (body.node !== expectedNode)
|
|
20882
|
-
throw
|
|
22400
|
+
throw mhError("conflict", "\u7AD9\u70B9\u5165\u53E3\u8282\u70B9\u4E0D\u5339\u914D\uFF08\u671F\u671B {expected}\uFF0C\u5B9E\u9645 {actual}\uFF09\uFF1B\u8FD9\u662F\u9632\u8BEF\u914D\u68C0\u67E5\uFF0C\u4E0D\u662F\u8EAB\u4EFD\u8BA4\u8BC1", {
|
|
22401
|
+
expected: expectedNode,
|
|
22402
|
+
actual: body.node
|
|
22403
|
+
});
|
|
20883
22404
|
if (!Array.isArray(body.capabilities) || !body.capabilities.includes("site_channels"))
|
|
20884
22405
|
throw new MhError("conflict", "\u76EE\u6807\u8BBE\u5907\u7248\u672C\u4E0D\u652F\u6301\u5B9A\u5411\u53D1\u5E03\u6E20\u9053\uFF1B\u8BF7\u5148\u5347\u7EA7\u8BE5\u8BBE\u5907\uFF0C\u518D\u91CD\u65B0\u9A8C\u8BC1\u5165\u53E3");
|
|
20885
22406
|
return { ...base, node: body.node };
|
|
20886
22407
|
} catch (e) {
|
|
20887
22408
|
if (e instanceof MhError)
|
|
20888
22409
|
throw e;
|
|
20889
|
-
|
|
20890
|
-
|
|
22410
|
+
if (e.name === "AbortError")
|
|
22411
|
+
throw mhError("network", "\u65E0\u6CD5\u9A8C\u8BC1\u7AD9\u70B9\u5165\u53E3\uFF1A\u8FDE\u63A5\u8D85\u65F6");
|
|
22412
|
+
throw mhError("network", "\u65E0\u6CD5\u9A8C\u8BC1\u7AD9\u70B9\u5165\u53E3\uFF1A{msg}", { msg: e.message });
|
|
20891
22413
|
} finally {
|
|
20892
22414
|
clearTimeout(timer);
|
|
20893
22415
|
}
|
|
@@ -21208,6 +22730,7 @@ var siteHostingRoutes = [
|
|
|
21208
22730
|
// src/webui/server/edge-routes.ts
|
|
21209
22731
|
init_zod();
|
|
21210
22732
|
init_errors();
|
|
22733
|
+
init_page_messages();
|
|
21211
22734
|
|
|
21212
22735
|
// src/core/sync/edge-service.ts
|
|
21213
22736
|
init_errors();
|
|
@@ -21374,6 +22897,7 @@ init_errors();
|
|
|
21374
22897
|
init_http_status();
|
|
21375
22898
|
init_ids();
|
|
21376
22899
|
init_access_policy();
|
|
22900
|
+
init_page_messages();
|
|
21377
22901
|
init_grants_routes();
|
|
21378
22902
|
init_rate_limit();
|
|
21379
22903
|
init_sites_core();
|
|
@@ -21855,6 +23379,7 @@ function disconnectEdge(db) {
|
|
|
21855
23379
|
|
|
21856
23380
|
// src/core/sync/cf-oauth.ts
|
|
21857
23381
|
init_errors();
|
|
23382
|
+
init_page_messages();
|
|
21858
23383
|
init_e2ee();
|
|
21859
23384
|
var CF_OAUTH_CLIENT_ID = process.env.METAHUB_CF_OAUTH_CLIENT_ID || "0062224f5b80ba3a2074780e73a9d305";
|
|
21860
23385
|
var AUTH_ENDPOINT = "https://dash.cloudflare.com/oauth2/auth";
|
|
@@ -21904,7 +23429,7 @@ function buildTokenRequestBody(p) {
|
|
|
21904
23429
|
function parseTokenResponse(data) {
|
|
21905
23430
|
const d = data ?? {};
|
|
21906
23431
|
if (!d.access_token)
|
|
21907
|
-
throw
|
|
23432
|
+
throw mhError("auth", "Cloudflare \u6388\u6743\u5931\u8D25\uFF1A{detail}", { detail: d.error_description || d.error || "\u672A\u8FD4\u56DE access token" });
|
|
21908
23433
|
return {
|
|
21909
23434
|
accessToken: d.access_token,
|
|
21910
23435
|
refreshToken: d.refresh_token ?? null,
|
|
@@ -21924,11 +23449,11 @@ async function exchangeCode(p) {
|
|
|
21924
23449
|
body: buildTokenRequestBody(p)
|
|
21925
23450
|
});
|
|
21926
23451
|
} catch (e) {
|
|
21927
|
-
throw
|
|
23452
|
+
throw mhError("network", "\u65E0\u6CD5\u8FDE\u63A5 Cloudflare \u6388\u6743\u670D\u52A1\uFF1A{msg}", { msg: e.message });
|
|
21928
23453
|
}
|
|
21929
23454
|
const data = await res.json().catch(() => null);
|
|
21930
23455
|
if (!res.ok && !data?.access_token)
|
|
21931
|
-
throw
|
|
23456
|
+
throw mhError("auth", "Cloudflare \u4EE4\u724C\u4EA4\u6362\u5931\u8D25\uFF08HTTP {status}\uFF09", { status: res.status });
|
|
21932
23457
|
return parseTokenResponse(data);
|
|
21933
23458
|
}
|
|
21934
23459
|
async function discoverAccounts(accessToken) {
|
|
@@ -21938,11 +23463,11 @@ async function discoverAccounts(accessToken) {
|
|
|
21938
23463
|
headers: { authorization: `Bearer ${accessToken}`, accept: "application/json" }
|
|
21939
23464
|
});
|
|
21940
23465
|
} catch (e) {
|
|
21941
|
-
throw
|
|
23466
|
+
throw mhError("network", "\u65E0\u6CD5\u8BFB\u53D6 Cloudflare \u8D26\u53F7\u5217\u8868\uFF1A{msg}", { msg: e.message });
|
|
21942
23467
|
}
|
|
21943
23468
|
const data = await res.json().catch(() => null);
|
|
21944
23469
|
if (!res.ok)
|
|
21945
|
-
throw
|
|
23470
|
+
throw mhError("auth", "\u8BFB\u53D6 Cloudflare \u8D26\u53F7\u5931\u8D25\uFF08HTTP {status}\uFF09", { status: res.status });
|
|
21946
23471
|
return parseAccounts(data);
|
|
21947
23472
|
}
|
|
21948
23473
|
async function startCfLogin(opts = {}) {
|
|
@@ -21961,7 +23486,9 @@ async function startCfLogin(opts = {}) {
|
|
|
21961
23486
|
if (url.pathname !== CALLBACK_PATH)
|
|
21962
23487
|
return new Response("not found", { status: 404 });
|
|
21963
23488
|
settle(url.searchParams);
|
|
21964
|
-
return new Response(
|
|
23489
|
+
return new Response(callbackHtml(localeFromAcceptLanguage(req.headers.get("accept-language"))), {
|
|
23490
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
23491
|
+
});
|
|
21965
23492
|
}
|
|
21966
23493
|
});
|
|
21967
23494
|
const redirectUri = `http://127.0.0.1:${server.port}${CALLBACK_PATH}`;
|
|
@@ -21992,7 +23519,7 @@ async function startCfLogin(opts = {}) {
|
|
|
21992
23519
|
try {
|
|
21993
23520
|
const q = await Promise.race([caught, timeout]);
|
|
21994
23521
|
if (q.get("error"))
|
|
21995
|
-
throw
|
|
23522
|
+
throw mhError("auth", "Cloudflare \u6388\u6743\u88AB\u62D2\u7EDD\uFF1A{detail}", { detail: q.get("error_description") || q.get("error") || "" });
|
|
21996
23523
|
if (q.get("state") !== state)
|
|
21997
23524
|
throw new MhError("auth", "OAuth state \u4E0D\u5339\u914D\uFF0C\u7591\u4F3C\u4F2A\u9020\u56DE\u8C03\uFF0C\u5DF2\u4E2D\u6B62");
|
|
21998
23525
|
const code = q.get("code");
|
|
@@ -22013,9 +23540,9 @@ async function startCfLogin(opts = {}) {
|
|
|
22013
23540
|
}
|
|
22014
23541
|
};
|
|
22015
23542
|
}
|
|
22016
|
-
var
|
|
23543
|
+
var callbackHtml = (locale) => `<!doctype html><html lang="${locale}"><meta charset="utf-8"><title>MetaHub</title>
|
|
22017
23544
|
<body style="font:15px system-ui;display:grid;place-items:center;height:100vh;margin:0">
|
|
22018
|
-
<div style="text-align:center"><h2
|
|
23545
|
+
<div style="text-align:center"><h2>${pm(locale, "\u2705 \u5DF2\u6388\u6743")}</h2><p>${pm(locale, "\u53EF\u4EE5\u5173\u95ED\u6B64\u6807\u7B7E\u9875\uFF0C\u56DE\u5230 MetaHub \u7EE7\u7EED\u90E8\u7F72\u3002")}</p></div>`;
|
|
22019
23546
|
|
|
22020
23547
|
// src/webui/server/edge-routes.ts
|
|
22021
23548
|
init_ids();
|
|
@@ -22086,7 +23613,7 @@ function resolveCfCreds(body) {
|
|
|
22086
23613
|
if (!flow)
|
|
22087
23614
|
throw new MhError("not_found", "OAuth \u6D41\u7A0B\u4E0D\u5B58\u5728\u6216\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55");
|
|
22088
23615
|
if (flow.state === "error")
|
|
22089
|
-
throw new MhError("auth", flow.error || "Cloudflare \u6388\u6743\u5931\u8D25");
|
|
23616
|
+
throw new MhError("auth", flow.error || "Cloudflare \u6388\u6743\u5931\u8D25", flow.errorI18n);
|
|
22090
23617
|
if (flow.state !== "ready" || !flow.token)
|
|
22091
23618
|
throw new MhError("conflict", "Cloudflare \u6388\u6743\u5C1A\u672A\u5B8C\u6210\uFF0C\u8BF7\u7A0D\u5019");
|
|
22092
23619
|
apiToken = flow.token;
|
|
@@ -22130,6 +23657,7 @@ var edgeRoutes = [
|
|
|
22130
23657
|
}).catch((e) => {
|
|
22131
23658
|
flow.state = "error";
|
|
22132
23659
|
flow.error = e.message;
|
|
23660
|
+
flow.errorI18n = e instanceof MhError ? e.i18n : undefined;
|
|
22133
23661
|
});
|
|
22134
23662
|
return { flowId, authUrl: handle.authUrl };
|
|
22135
23663
|
})
|
|
@@ -22148,7 +23676,8 @@ var edgeRoutes = [
|
|
|
22148
23676
|
const flow = flows.get(flowId);
|
|
22149
23677
|
if (!flow)
|
|
22150
23678
|
throw new MhError("not_found", "OAuth \u6D41\u7A0B\u4E0D\u5B58\u5728\u6216\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u8BD5");
|
|
22151
|
-
|
|
23679
|
+
const error = flow.error === undefined ? undefined : flow.errorI18n ? pmIfKnown(pickLocale(req), flow.errorI18n.key, flow.errorI18n.params) : pmIfKnown(pickLocale(req), flow.error);
|
|
23680
|
+
return { state: flow.state, accounts: flow.accounts, error };
|
|
22152
23681
|
})
|
|
22153
23682
|
},
|
|
22154
23683
|
{
|
|
@@ -22250,6 +23779,7 @@ var DatabaseSchema = object({
|
|
|
22250
23779
|
id: string2(),
|
|
22251
23780
|
name: string2(),
|
|
22252
23781
|
icon: string2().nullable(),
|
|
23782
|
+
meta: record(string2(), any()).nullable(),
|
|
22253
23783
|
created_hlc: string2()
|
|
22254
23784
|
});
|
|
22255
23785
|
var PropertySchema = object({
|
|
@@ -22293,11 +23823,18 @@ var SearchHitSchema = object({
|
|
|
22293
23823
|
title: string2().optional(),
|
|
22294
23824
|
snippet: string2()
|
|
22295
23825
|
});
|
|
23826
|
+
var LookupHitSchema = object({
|
|
23827
|
+
kind: _enum(["db", "doc", "rec"]),
|
|
23828
|
+
id: string2(),
|
|
23829
|
+
label: string2(),
|
|
23830
|
+
database_id: string2().nullable()
|
|
23831
|
+
});
|
|
22296
23832
|
var OkSchema2 = object({ ok: boolean2() });
|
|
22297
23833
|
var CreateDatabaseReq = object({ name: string2(), icon: string2().optional() });
|
|
22298
23834
|
var UpdateDatabaseReq = object({
|
|
22299
23835
|
name: string2().optional(),
|
|
22300
|
-
icon: string2().nullable().optional()
|
|
23836
|
+
icon: string2().nullable().optional(),
|
|
23837
|
+
meta: record(string2(), any()).nullable().optional().describe("Merge patch over per-key metadata; null value deletes a key, null object clears all")
|
|
22301
23838
|
});
|
|
22302
23839
|
var DuplicateDatabaseReq = object({
|
|
22303
23840
|
name: string2().optional().describe("Name for the copy (defaults to the source name)"),
|
|
@@ -22468,8 +24005,11 @@ var FieldHistoryEntrySchema = object({
|
|
|
22468
24005
|
});
|
|
22469
24006
|
var NodeInfoSchema = object({
|
|
22470
24007
|
node_id: string2(),
|
|
22471
|
-
label: string2().nullable().describe("
|
|
22472
|
-
self: boolean2().describe("True for the node serving this request")
|
|
24008
|
+
label: string2().nullable().describe("Synced roster name \u2192 pairing label \u2192 null when unnamed"),
|
|
24009
|
+
self: boolean2().describe("True for the node serving this request"),
|
|
24010
|
+
platform: _enum(["macos", "windows", "linux", "ios", "android", "web"]).nullable(),
|
|
24011
|
+
form: _enum(["laptop", "desktop", "phone", "server", "browser"]).nullable(),
|
|
24012
|
+
app: _enum(["cli", "server", "desktop", "web"]).nullable()
|
|
22473
24013
|
});
|
|
22474
24014
|
var CacheStatsSchema = object({
|
|
22475
24015
|
totalBytes: number2(),
|
|
@@ -22553,7 +24093,7 @@ function handle3(fn) {
|
|
|
22553
24093
|
return out;
|
|
22554
24094
|
return Response.json(out ?? null);
|
|
22555
24095
|
} catch (e) {
|
|
22556
|
-
return errorResponse(e);
|
|
24096
|
+
return errorResponse(e, req);
|
|
22557
24097
|
}
|
|
22558
24098
|
};
|
|
22559
24099
|
}
|
|
@@ -22616,7 +24156,7 @@ var webuiRoutes = [
|
|
|
22616
24156
|
{
|
|
22617
24157
|
method: "PATCH",
|
|
22618
24158
|
path: "/api/database",
|
|
22619
|
-
summary: "Rename a database or
|
|
24159
|
+
summary: "Rename a database, change its icon or patch its metadata. Query: ?id=<id>",
|
|
22620
24160
|
request: UpdateDatabaseReq,
|
|
22621
24161
|
response: DatabaseSchema,
|
|
22622
24162
|
handler: handle3(async (req, { db }) => {
|
|
@@ -22979,13 +24519,17 @@ var webuiRoutes = [
|
|
|
22979
24519
|
{
|
|
22980
24520
|
method: "PATCH",
|
|
22981
24521
|
path: "/api/node",
|
|
22982
|
-
summary: "
|
|
22983
|
-
request: object({
|
|
24522
|
+
summary: "Rename a device (default: this one). Writes the synced roster; empty/null clears",
|
|
24523
|
+
request: object({
|
|
24524
|
+
label: string2().nullable().optional(),
|
|
24525
|
+
node_id: string2().optional().describe("Target node; omitted = this device")
|
|
24526
|
+
}),
|
|
22984
24527
|
response: NodeInfoSchema,
|
|
22985
24528
|
handler: handle3(async (req, { db }) => {
|
|
22986
24529
|
const body = await req.json();
|
|
22987
|
-
|
|
22988
|
-
|
|
24530
|
+
const id = body.node_id ?? getNodeId(db);
|
|
24531
|
+
setNodeLabel(db, body.label ?? null, id);
|
|
24532
|
+
return displayNodes(db).find((n) => n.node_id === id);
|
|
22989
24533
|
})
|
|
22990
24534
|
},
|
|
22991
24535
|
{
|
|
@@ -22998,6 +24542,16 @@ var webuiRoutes = [
|
|
|
22998
24542
|
return search(db, need3(req, "q"), { limit: limit ? Number(limit) : undefined });
|
|
22999
24543
|
})
|
|
23000
24544
|
},
|
|
24545
|
+
{
|
|
24546
|
+
method: "GET",
|
|
24547
|
+
path: "/api/resolve",
|
|
24548
|
+
summary: "Navigable entities (db/doc/rec) matching a full id, id prefix, bare slug or name \u2014 the `mh get <ref>` rule. Query: ?ref=<text>&limit=<n>",
|
|
24549
|
+
response: array(LookupHitSchema),
|
|
24550
|
+
handler: handle3((req, { db }) => {
|
|
24551
|
+
const limit = opt(req, "limit");
|
|
24552
|
+
return lookupCandidates(db, need3(req, "ref"), { limit: limit ? Number(limit) : undefined });
|
|
24553
|
+
})
|
|
24554
|
+
},
|
|
23001
24555
|
{
|
|
23002
24556
|
method: "GET",
|
|
23003
24557
|
path: "/api/blob-cache",
|
|
@@ -23022,6 +24576,13 @@ var webuiRoutes = [
|
|
|
23022
24576
|
response: ClearResultSchema,
|
|
23023
24577
|
handler: handle3((_req, { db }) => clearCache(db))
|
|
23024
24578
|
},
|
|
24579
|
+
{
|
|
24580
|
+
method: "POST",
|
|
24581
|
+
path: "/api/repair/clock",
|
|
24582
|
+
summary: "Restamp oplog rows dated more than 5 min in the future with this clock and reset the logical clock (fixes clock_skew; local-only).",
|
|
24583
|
+
response: object({ restamped: number2(), byNode: record(string2(), number2()) }),
|
|
24584
|
+
handler: handle3(async (_req, { db }) => repairClock(db))
|
|
24585
|
+
},
|
|
23025
24586
|
{
|
|
23026
24587
|
method: "POST",
|
|
23027
24588
|
path: "/api/blob-cache/pin",
|
|
@@ -23095,6 +24656,7 @@ export {
|
|
|
23095
24656
|
DOCUMENT_COLS,
|
|
23096
24657
|
DOMAIN,
|
|
23097
24658
|
HEALTH_PATH,
|
|
24659
|
+
HLC_MAX_SKEW_MS,
|
|
23098
24660
|
HealthResponseSchema,
|
|
23099
24661
|
INLINE_LIMIT,
|
|
23100
24662
|
MAX_HLC_COUNTER,
|
|
@@ -23120,6 +24682,7 @@ export {
|
|
|
23120
24682
|
advanceAckedPrefix,
|
|
23121
24683
|
appendDocument,
|
|
23122
24684
|
applyChange,
|
|
24685
|
+
asMhError,
|
|
23123
24686
|
backfillDocumentOrderKeys,
|
|
23124
24687
|
backfillRecordOrderKeys,
|
|
23125
24688
|
base64ToBytes,
|
|
@@ -23139,12 +24702,14 @@ export {
|
|
|
23139
24702
|
createSite,
|
|
23140
24703
|
dbPath,
|
|
23141
24704
|
decodeFileRow,
|
|
24705
|
+
defaultForm,
|
|
23142
24706
|
deleteBlob,
|
|
23143
24707
|
deleteDatabase,
|
|
23144
24708
|
deleteDocument,
|
|
23145
24709
|
deleteFile,
|
|
23146
24710
|
deleteRecord,
|
|
23147
24711
|
deleteSite,
|
|
24712
|
+
describeSelf,
|
|
23148
24713
|
displayNodes,
|
|
23149
24714
|
documentTitleMap,
|
|
23150
24715
|
documentVersion,
|
|
@@ -23178,28 +24743,36 @@ export {
|
|
|
23178
24743
|
getSiteByName,
|
|
23179
24744
|
grouped,
|
|
23180
24745
|
hasIndex,
|
|
24746
|
+
hlcSkewBound,
|
|
23181
24747
|
idKind,
|
|
23182
24748
|
inferContentType,
|
|
23183
24749
|
ingest,
|
|
24750
|
+
ingestDetailed,
|
|
23184
24751
|
initSchema,
|
|
24752
|
+
interpolateMsg,
|
|
23185
24753
|
isImageType,
|
|
23186
24754
|
isSitePublic,
|
|
24755
|
+
isSkewedHlc,
|
|
23187
24756
|
isTextType,
|
|
23188
24757
|
keyBetween,
|
|
23189
24758
|
keysBetween,
|
|
23190
24759
|
listDatabases,
|
|
23191
24760
|
listDocuments,
|
|
23192
24761
|
listFiles,
|
|
24762
|
+
listNodeMeta,
|
|
23193
24763
|
listProperties,
|
|
23194
24764
|
listRecords,
|
|
23195
24765
|
listSites,
|
|
23196
24766
|
liveFileRowIfSame,
|
|
24767
|
+
lookupCandidates,
|
|
23197
24768
|
loopbackUiRejection,
|
|
23198
24769
|
makeId,
|
|
23199
24770
|
maybeAutoIndex,
|
|
23200
24771
|
metahubHome,
|
|
24772
|
+
mhError,
|
|
23201
24773
|
migrateDocBlocks,
|
|
23202
24774
|
migrateDocuments,
|
|
24775
|
+
migrateNodes,
|
|
23203
24776
|
migrateOplog,
|
|
23204
24777
|
migratePeers,
|
|
23205
24778
|
migrateRecords,
|
|
@@ -23208,12 +24781,15 @@ export {
|
|
|
23208
24781
|
moveRecord,
|
|
23209
24782
|
newId,
|
|
23210
24783
|
nextHlc,
|
|
24784
|
+
nodeLabelOf,
|
|
23211
24785
|
normalizeSiteName,
|
|
23212
24786
|
normalizeSitePath,
|
|
23213
24787
|
observeHlc,
|
|
23214
24788
|
openMetahub,
|
|
23215
24789
|
parseDocBlocks,
|
|
23216
24790
|
parseHlc,
|
|
24791
|
+
persistedHlc,
|
|
24792
|
+
platformFromProcess,
|
|
23217
24793
|
prepareRecordCells,
|
|
23218
24794
|
prependDocument,
|
|
23219
24795
|
publishDirectory,
|
|
@@ -23222,12 +24798,14 @@ export {
|
|
|
23222
24798
|
putFile,
|
|
23223
24799
|
putFileInline,
|
|
23224
24800
|
randomSuffix,
|
|
24801
|
+
readNodeMeta,
|
|
23225
24802
|
rebuildSearchIndex,
|
|
23226
24803
|
reconcile,
|
|
23227
24804
|
recordTitleMap,
|
|
23228
24805
|
removeProperty,
|
|
23229
24806
|
removeSelectOption,
|
|
23230
24807
|
renameSelectOption,
|
|
24808
|
+
resetHlc,
|
|
23231
24809
|
resolveCandidates,
|
|
23232
24810
|
resolveData,
|
|
23233
24811
|
resolveEntity,
|
|
@@ -23247,6 +24825,7 @@ export {
|
|
|
23247
24825
|
sha256Hex,
|
|
23248
24826
|
siteCacheControl,
|
|
23249
24827
|
siteLifecycle,
|
|
24828
|
+
skewWarnings,
|
|
23250
24829
|
slugify,
|
|
23251
24830
|
startServer,
|
|
23252
24831
|
syncWithPeer,
|
|
@@ -23264,7 +24843,8 @@ export {
|
|
|
23264
24843
|
withChangeGroup,
|
|
23265
24844
|
withNodeId,
|
|
23266
24845
|
withTxnId,
|
|
23267
|
-
writeFileRow
|
|
24846
|
+
writeFileRow,
|
|
24847
|
+
writeTx
|
|
23268
24848
|
};
|
|
23269
24849
|
|
|
23270
|
-
//# debugId=
|
|
24850
|
+
//# debugId=23C620FE660E43F464756E2164756E21
|