@tensorix/metahub 0.5.2 → 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 +4945 -3979
- package/dist/cli.js.map +63 -57
- 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.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 +15 -3
- package/dist/core/integrity.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 +1 -1
- package/dist/core/schema-init.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/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.d.ts +1 -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 +1 -1
- package/dist/core/sync/storage.d.ts.map +1 -1
- package/dist/db-worker.js +115 -88
- package/dist/edge-worker.js +319 -84
- package/dist/index.js +1597 -384
- package/dist/index.js.map +52 -45
- 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 -215
- 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 (
|
|
@@ -466,7 +496,7 @@ function setNodeLabel(db, label, nodeId) {
|
|
|
466
496
|
return;
|
|
467
497
|
if (!cur && next == null)
|
|
468
498
|
return;
|
|
469
|
-
withChangeGroup("node", () => {
|
|
499
|
+
withChangeGroup(db, "node", () => {
|
|
470
500
|
emit(db, "nodes", id, "label", next);
|
|
471
501
|
});
|
|
472
502
|
}
|
|
@@ -488,7 +518,7 @@ function describeSelf(db, info = {}) {
|
|
|
488
518
|
fields.label = seed;
|
|
489
519
|
}
|
|
490
520
|
if (Object.keys(fields).length > 0) {
|
|
491
|
-
withChangeGroup("node", () => {
|
|
521
|
+
withChangeGroup(db, "node", () => {
|
|
492
522
|
for (const [col, v] of Object.entries(fields))
|
|
493
523
|
emit(db, "nodes", id, col, v);
|
|
494
524
|
});
|
|
@@ -556,6 +586,12 @@ var init_node = __esm(() => {
|
|
|
556
586
|
});
|
|
557
587
|
|
|
558
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
|
+
}
|
|
559
595
|
function carryCounter(millis, counter) {
|
|
560
596
|
if (counter <= MAX_HLC_COUNTER)
|
|
561
597
|
return { millis, counter };
|
|
@@ -586,6 +622,8 @@ function writeLast(db, h) {
|
|
|
586
622
|
}
|
|
587
623
|
function nextHlc(db, node, now = Date.now()) {
|
|
588
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) });
|
|
589
627
|
const millis = Math.max(last.millis, now);
|
|
590
628
|
const counter = millis === last.millis ? last.counter + 1 : 0;
|
|
591
629
|
const h = { ...carryCounter(millis, counter), node };
|
|
@@ -597,6 +635,8 @@ function observeHlc(db, node, remote, now = Date.now()) {
|
|
|
597
635
|
const r = parseHlc(remote);
|
|
598
636
|
if (!Number.isFinite(r.millis) || !Number.isFinite(r.counter))
|
|
599
637
|
return;
|
|
638
|
+
if (r.millis > now + HLC_MAX_SKEW_MS)
|
|
639
|
+
return;
|
|
600
640
|
const millis = Math.max(last.millis, r.millis, now);
|
|
601
641
|
let counter;
|
|
602
642
|
if (millis === last.millis && millis === r.millis)
|
|
@@ -609,7 +649,18 @@ function observeHlc(db, node, remote, now = Date.now()) {
|
|
|
609
649
|
counter = 0;
|
|
610
650
|
writeLast(db, { ...carryCounter(millis, counter), node });
|
|
611
651
|
}
|
|
612
|
-
|
|
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
|
+
});
|
|
613
664
|
|
|
614
665
|
// src/core/blocks.ts
|
|
615
666
|
function parseDocBlocks(md) {
|
|
@@ -712,6 +763,78 @@ function lcs(a, b) {
|
|
|
712
763
|
return pairs;
|
|
713
764
|
}
|
|
714
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
|
+
|
|
715
838
|
// src/core/intent-retention.ts
|
|
716
839
|
function safeNow(now) {
|
|
717
840
|
return Math.max(0, Math.trunc(Number.isFinite(now) ? now : Date.now()));
|
|
@@ -760,27 +883,27 @@ function withNodeId(node, fn) {
|
|
|
760
883
|
currentNodeOverride = prev;
|
|
761
884
|
}
|
|
762
885
|
}
|
|
763
|
-
function withChangeGroup(label, fn) {
|
|
886
|
+
function withChangeGroup(db, label, fn) {
|
|
764
887
|
if (currentTxn !== null)
|
|
765
888
|
return fn();
|
|
766
889
|
currentTxn = (currentActor ? currentActor + "/" : "") + (label ? label + ":" : "") + randomSuffix(8);
|
|
767
890
|
try {
|
|
768
|
-
return fn
|
|
891
|
+
return writeTx(db, fn);
|
|
769
892
|
} finally {
|
|
770
893
|
currentTxn = null;
|
|
771
894
|
}
|
|
772
895
|
}
|
|
773
|
-
function withTxnId(txn, fn) {
|
|
896
|
+
function withTxnId(db, txn, fn) {
|
|
774
897
|
const prev = currentTxn;
|
|
775
898
|
currentTxn = txn;
|
|
776
899
|
try {
|
|
777
|
-
return fn
|
|
900
|
+
return writeTx(db, fn);
|
|
778
901
|
} finally {
|
|
779
902
|
currentTxn = prev;
|
|
780
903
|
}
|
|
781
904
|
}
|
|
782
905
|
function grouped(fn, label = null) {
|
|
783
|
-
return (...args) => withChangeGroup(label, () => fn(...args));
|
|
906
|
+
return (db, ...args) => withChangeGroup(db, label, () => fn(db, ...args));
|
|
784
907
|
}
|
|
785
908
|
function decodeMaterializeValue(dataset, col, valueJson) {
|
|
786
909
|
if (valueJson !== null)
|
|
@@ -827,6 +950,16 @@ function materialize(db, dataset, rowId, col, valueJson) {
|
|
|
827
950
|
}
|
|
828
951
|
return;
|
|
829
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
|
+
}
|
|
830
963
|
const d = DOMAIN[dataset];
|
|
831
964
|
if (!d || !d.cols.has(col))
|
|
832
965
|
return;
|
|
@@ -844,6 +977,8 @@ function materialize(db, dataset, rowId, col, valueJson) {
|
|
|
844
977
|
function applyChange(db, c) {
|
|
845
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;
|
|
846
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);
|
|
847
982
|
if (cur.h !== c.hlc)
|
|
848
983
|
return { inserted, winner: false };
|
|
849
984
|
try {
|
|
@@ -857,18 +992,21 @@ function applyChange(db, c) {
|
|
|
857
992
|
return { inserted, winner: true };
|
|
858
993
|
}
|
|
859
994
|
function emit(db, dataset, rowId, col, value) {
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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;
|
|
869
1008
|
};
|
|
870
|
-
|
|
871
|
-
return change;
|
|
1009
|
+
return currentTxn !== null ? mint() : writeTx(db, mint);
|
|
872
1010
|
}
|
|
873
1011
|
function emitFields(db, dataset, rowId, fields) {
|
|
874
1012
|
const out = [];
|
|
@@ -878,16 +1016,24 @@ function emitFields(db, dataset, rowId, fields) {
|
|
|
878
1016
|
return out;
|
|
879
1017
|
}
|
|
880
1018
|
function ingest(db, changes, opts = {}) {
|
|
1019
|
+
return ingestDetailed(db, changes, opts).received;
|
|
1020
|
+
}
|
|
1021
|
+
function ingestDetailed(db, changes, opts = {}) {
|
|
881
1022
|
const now = opts.now ?? Date.now();
|
|
882
1023
|
pruneExpiredIntentReceipts(db, now);
|
|
883
1024
|
const node = getNodeId(db);
|
|
884
1025
|
let received = 0;
|
|
885
1026
|
let poisoned = 0;
|
|
886
|
-
const
|
|
887
|
-
|
|
1027
|
+
const skewed = {};
|
|
1028
|
+
const tx = () => {
|
|
1029
|
+
for (const c of changes) {
|
|
888
1030
|
if (isExpiredIntentReceipt(c, now))
|
|
889
1031
|
continue;
|
|
890
|
-
|
|
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);
|
|
891
1037
|
try {
|
|
892
1038
|
if (applyChange(db, c).inserted)
|
|
893
1039
|
received++;
|
|
@@ -896,11 +1042,13 @@ function ingest(db, changes, opts = {}) {
|
|
|
896
1042
|
console.error(`[ingest] skipped a change that failed to apply (dataset=${c.dataset} col=${c.col} row=${c.row_id}): ${e.message}`);
|
|
897
1043
|
}
|
|
898
1044
|
}
|
|
899
|
-
}
|
|
900
|
-
tx
|
|
1045
|
+
};
|
|
1046
|
+
writeTx(db, tx);
|
|
901
1047
|
if (poisoned)
|
|
902
1048
|
console.error(`[ingest] ${poisoned} change(s) skipped this batch (see above)`);
|
|
903
|
-
|
|
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 };
|
|
904
1052
|
}
|
|
905
1053
|
function changesSince(db, since, opts = {}) {
|
|
906
1054
|
const rows = db.query(`SELECT ${CHANGE_SELECT} FROM crdt_changes WHERE hlc > ? ORDER BY hlc`).all(since);
|
|
@@ -936,7 +1084,9 @@ function changesAfterSeq(db, seq, opts = {}) {
|
|
|
936
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;
|
|
937
1085
|
var init_crdt = __esm(() => {
|
|
938
1086
|
init_node();
|
|
1087
|
+
init_hlc();
|
|
939
1088
|
init_ids();
|
|
1089
|
+
init_history_cache();
|
|
940
1090
|
init_intent_retention();
|
|
941
1091
|
CHANGE_COLS = ["hlc", "node_id", "dataset", "row_id", "col", "value", "txn"];
|
|
942
1092
|
CHANGE_SELECT = CHANGE_COLS.join(", ");
|
|
@@ -1243,7 +1393,19 @@ var init_properties = __esm(() => {
|
|
|
1243
1393
|
function rowOut(r) {
|
|
1244
1394
|
if (!r)
|
|
1245
1395
|
return null;
|
|
1246
|
-
|
|
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
|
+
}
|
|
1247
1409
|
}
|
|
1248
1410
|
function getDatabase(db, id) {
|
|
1249
1411
|
return rowOut(db.query(`SELECT ${DATABASE_SELECT} FROM databases WHERE id = ? AND __deleted = 0`).get(id));
|
|
@@ -1282,7 +1444,9 @@ var init_databases = __esm(() => {
|
|
|
1282
1444
|
if (fields.meta !== undefined) {
|
|
1283
1445
|
if (fields.meta !== null && (typeof fields.meta !== "object" || Array.isArray(fields.meta)))
|
|
1284
1446
|
throw new MhError("invalid_input", "meta must be a JSON object or null");
|
|
1285
|
-
|
|
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);
|
|
1286
1450
|
}
|
|
1287
1451
|
return getDatabase(db, id);
|
|
1288
1452
|
});
|
|
@@ -1296,8 +1460,7 @@ var init_databases = __esm(() => {
|
|
|
1296
1460
|
});
|
|
1297
1461
|
if (src.meta !== null) {
|
|
1298
1462
|
const { collapsed: _hidden, ...meta } = src.meta;
|
|
1299
|
-
|
|
1300
|
-
emit(db, "databases", dup.id, "meta", meta);
|
|
1463
|
+
emitMetaPatch(db, dup.id, null, meta);
|
|
1301
1464
|
}
|
|
1302
1465
|
const propIdMap = new Map;
|
|
1303
1466
|
const selfRelNew = new Set;
|
|
@@ -1425,7 +1588,37 @@ ${fmt(shown)}
|
|
|
1425
1588
|
function resolveRef(db, ref, opts = {}) {
|
|
1426
1589
|
return resolveEntity(db, ref, opts).id;
|
|
1427
1590
|
}
|
|
1428
|
-
|
|
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;
|
|
1429
1622
|
var init_resolve = __esm(() => {
|
|
1430
1623
|
init_ids();
|
|
1431
1624
|
init_errors();
|
|
@@ -2126,25 +2319,6 @@ var init_documents = __esm(() => {
|
|
|
2126
2319
|
throw new MhError("invalid_input", `anchor not found: no match for --old`);
|
|
2127
2320
|
if (count > 1 && !opts.replaceAll)
|
|
2128
2321
|
throw new MhError("ambiguous", `${count} matches for --old; add surrounding context or use --replace-all`);
|
|
2129
|
-
if (!opts.old.includes(`
|
|
2130
|
-
|
|
2131
|
-
`)) {
|
|
2132
|
-
const affected = blocks.filter((b) => b.text.includes(opts.old));
|
|
2133
|
-
const next = affected.map((b) => opts.replaceAll ? b.text.replaceAll(opts.old, () => opts.new) : b.text.replace(opts.old, () => opts.new));
|
|
2134
|
-
if (next.every((t) => t.length > 0 && !t.includes(`
|
|
2135
|
-
|
|
2136
|
-
`))) {
|
|
2137
|
-
const targets = opts.replaceAll ? affected : affected.slice(0, 1);
|
|
2138
|
-
let wrote = false;
|
|
2139
|
-
targets.forEach((b, i) => {
|
|
2140
|
-
if (next[i] !== b.text) {
|
|
2141
|
-
emit(db, "doc_blocks", b.id, "text", next[i]);
|
|
2142
|
-
wrote = true;
|
|
2143
|
-
}
|
|
2144
|
-
});
|
|
2145
|
-
return { id, changed: wrote, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
2322
|
const newBody = opts.replaceAll ? body.replaceAll(opts.old, () => opts.new) : body.replace(opts.old, () => opts.new);
|
|
2149
2323
|
reconcileBody(db, id, newBody);
|
|
2150
2324
|
return { id, changed: newBody !== body, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
|
|
@@ -2342,7 +2516,7 @@ function knownNodes(db) {
|
|
|
2342
2516
|
return [...ids].map((nodeId) => ({ nodeId, label: nodeLabelOf(db, nodeId), self: nodeId === self }));
|
|
2343
2517
|
}
|
|
2344
2518
|
var POLICY_ID = "default", blobBytesResolver = null, setFullNodes = (db, nodeIds) => {
|
|
2345
|
-
withChangeGroup(null, () => {
|
|
2519
|
+
withChangeGroup(db, null, () => {
|
|
2346
2520
|
const seen = new Set;
|
|
2347
2521
|
const list = nodeIds.filter((n) => n && !seen.has(n) && seen.add(n) != null);
|
|
2348
2522
|
emit(db, "blob_policy", POLICY_ID, "full_nodes", list);
|
|
@@ -2405,7 +2579,7 @@ function migrateRecords(db) {
|
|
|
2405
2579
|
db.exec("ALTER TABLE records ADD COLUMN order_key TEXT");
|
|
2406
2580
|
if (!tableExists3(db, "record_values"))
|
|
2407
2581
|
return;
|
|
2408
|
-
|
|
2582
|
+
writeTx(db, () => {
|
|
2409
2583
|
db.exec(`
|
|
2410
2584
|
UPDATE records SET data = coalesce((
|
|
2411
2585
|
SELECT json_group_object(rv.property_id, json(rv.value))
|
|
@@ -2416,7 +2590,6 @@ function migrateRecords(db) {
|
|
|
2416
2590
|
db.exec("DROP TABLE record_values");
|
|
2417
2591
|
backfillRecordOrderKeys(db);
|
|
2418
2592
|
});
|
|
2419
|
-
tx();
|
|
2420
2593
|
backfillRecordOrderKeys(db);
|
|
2421
2594
|
}
|
|
2422
2595
|
function migrateShares(db) {
|
|
@@ -2474,7 +2647,7 @@ function migrateOplog(db) {
|
|
|
2474
2647
|
function migrateCrdtChangesSeq(db) {
|
|
2475
2648
|
if (hasColumn(db, "crdt_changes", "seq"))
|
|
2476
2649
|
return;
|
|
2477
|
-
|
|
2650
|
+
writeTx(db, () => {
|
|
2478
2651
|
db.exec(`
|
|
2479
2652
|
DROP TABLE IF EXISTS crdt_changes_new;
|
|
2480
2653
|
CREATE TABLE crdt_changes_new (
|
|
@@ -2504,7 +2677,6 @@ function migrateCrdtChangesSeq(db) {
|
|
|
2504
2677
|
if (tableExists3(db, "peers"))
|
|
2505
2678
|
db.exec("UPDATE peers SET push_cursor = 0, pull_cursor = 0");
|
|
2506
2679
|
});
|
|
2507
|
-
tx();
|
|
2508
2680
|
}
|
|
2509
2681
|
function migrateStoragePeerUrls(db) {
|
|
2510
2682
|
if (!tableExists3(db, "peers"))
|
|
@@ -2537,7 +2709,7 @@ function migrateStoragePeerUrls(db) {
|
|
|
2537
2709
|
if (renames.length === 0)
|
|
2538
2710
|
return;
|
|
2539
2711
|
const map = new Map(renames.map(({ oldUrl, newUrl }) => [oldUrl, newUrl]));
|
|
2540
|
-
|
|
2712
|
+
writeTx(db, () => {
|
|
2541
2713
|
for (const { oldUrl, newUrl } of renames) {
|
|
2542
2714
|
if (tableExists3(db, "storage_cursors"))
|
|
2543
2715
|
db.query("UPDATE storage_cursors SET peer_url = ? WHERE peer_url = ?").run(newUrl, oldUrl);
|
|
@@ -2549,7 +2721,6 @@ function migrateStoragePeerUrls(db) {
|
|
|
2549
2721
|
setFullNodes(db, fullNodes.map((n) => map.get(n) ?? n));
|
|
2550
2722
|
}
|
|
2551
2723
|
});
|
|
2552
|
-
tx();
|
|
2553
2724
|
}
|
|
2554
2725
|
function migrateDatabases(db) {
|
|
2555
2726
|
if (hasColumn(db, "databases", "meta"))
|
|
@@ -2602,11 +2773,11 @@ function replayDataset(db, dataset) {
|
|
|
2602
2773
|
WHERE dataset = ? AND seq > ? ORDER BY seq`).all(dataset, since);
|
|
2603
2774
|
if (changes.length === 0)
|
|
2604
2775
|
return;
|
|
2605
|
-
db
|
|
2606
|
-
for (const change of
|
|
2776
|
+
writeTx(db, () => {
|
|
2777
|
+
for (const change of changes)
|
|
2607
2778
|
applyChange(db, change);
|
|
2608
|
-
db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, String(
|
|
2609
|
-
})
|
|
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
|
+
});
|
|
2610
2781
|
}
|
|
2611
2782
|
function migrateSiteChannels(db) {
|
|
2612
2783
|
replayDataset(db, "site_channels");
|
|
@@ -3080,7 +3251,7 @@ function writeFileRow(db, siteId, cleanPath, contentType, encoding, content) {
|
|
|
3080
3251
|
return { ...same, changed: false };
|
|
3081
3252
|
const existing = fileIdFor(db, siteId, cleanPath);
|
|
3082
3253
|
const id = existing ?? newId("sf", cleanPath);
|
|
3083
|
-
withChangeGroup(null, () => {
|
|
3254
|
+
withChangeGroup(db, null, () => {
|
|
3084
3255
|
if (!existing) {
|
|
3085
3256
|
const first = emit(db, "site_files", id, "site_id", siteId);
|
|
3086
3257
|
emit(db, "site_files", id, "path", cleanPath);
|
|
@@ -8287,7 +8458,8 @@ var init_protocol = __esm(() => {
|
|
|
8287
8458
|
SyncResponseSchema = object({
|
|
8288
8459
|
node_id: string2().describe("Server's node id"),
|
|
8289
8460
|
changes: array(ChangeSchema).describe("Server changes after `since`"),
|
|
8290
|
-
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")
|
|
8291
8463
|
});
|
|
8292
8464
|
HealthResponseSchema = object({
|
|
8293
8465
|
ok: boolean2(),
|
|
@@ -8315,6 +8487,9 @@ function getPeer(db, url) {
|
|
|
8315
8487
|
function setPeer(db, url, c) {
|
|
8316
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);
|
|
8317
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
|
+
}
|
|
8318
8493
|
function advanceAckedPrefix(db, fromSeq, received) {
|
|
8319
8494
|
if (received.length === 0)
|
|
8320
8495
|
return fromSeq;
|
|
@@ -8353,12 +8528,21 @@ async function syncWithPeer(db, url, opts = {}) {
|
|
|
8353
8528
|
throw new MhError(res.status === 401 || res.status === 403 ? "auth" : "network", `sync failed: ${res.status} ${await res.text()}`);
|
|
8354
8529
|
const data = await res.json();
|
|
8355
8530
|
const changes = data.changes ?? [];
|
|
8356
|
-
const
|
|
8531
|
+
const r = ingestDetailed(db, changes);
|
|
8532
|
+
const pulled = r.received;
|
|
8357
8533
|
setPeer(db, url, {
|
|
8358
8534
|
pull_cursor: data.cursor,
|
|
8359
8535
|
push_cursor: advanceAckedPrefix(db, toPush.cursor, changes)
|
|
8360
8536
|
});
|
|
8361
|
-
|
|
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
|
+
};
|
|
8362
8546
|
}
|
|
8363
8547
|
var changeKey = (c) => `${c.hlc}\x00${c.node_id}\x00${c.dataset}\x00${c.row_id}\x00${c.col}`;
|
|
8364
8548
|
var init_client = __esm(() => {
|
|
@@ -8857,9 +9041,10 @@ async function syncWithStorage(db, peerUrl, client, config, opts = {}) {
|
|
|
8857
9041
|
}
|
|
8858
9042
|
let snapKeys = [];
|
|
8859
9043
|
let pulled = 0;
|
|
9044
|
+
const skewed = {};
|
|
8860
9045
|
if (opts.pull ?? true) {
|
|
8861
9046
|
snapKeys = (await client.list(snapshotRoot(base))).map((o) => o.key).filter((k) => k.endsWith(".snap"));
|
|
8862
|
-
pulled += await pullSnapshots(db, peerUrl, snapKeys, client, key);
|
|
9047
|
+
pulled += await pullSnapshots(db, peerUrl, snapKeys, client, key, skewed);
|
|
8863
9048
|
for (const remote of await listRemoteNodes(client, base)) {
|
|
8864
9049
|
if (remote === node)
|
|
8865
9050
|
continue;
|
|
@@ -8876,7 +9061,7 @@ async function syncWithStorage(db, peerUrl, client, config, opts = {}) {
|
|
|
8876
9061
|
if (!bytes)
|
|
8877
9062
|
continue;
|
|
8878
9063
|
const changes = await decodeSegment(bytes, key);
|
|
8879
|
-
pulled +=
|
|
9064
|
+
pulled += collectSkew(skewed, ingestDetailed(db, changes));
|
|
8880
9065
|
setStorageCursor(db, peerUrl, remote, seg.key);
|
|
8881
9066
|
}
|
|
8882
9067
|
}
|
|
@@ -8903,9 +9088,15 @@ async function syncWithStorage(db, peerUrl, client, config, opts = {}) {
|
|
|
8903
9088
|
if (ownCount >= threshold)
|
|
8904
9089
|
await publishSnapshot(db, client, config, opts.snapshotRetainSegments);
|
|
8905
9090
|
}
|
|
8906
|
-
|
|
9091
|
+
const warnings = skewWarnings(peerUrl, skewed);
|
|
9092
|
+
return { pushed, pulled, pendingPush, ...warnings.length ? { warnings } : {} };
|
|
9093
|
+
}
|
|
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;
|
|
8907
9098
|
}
|
|
8908
|
-
async function pullSnapshots(db, peerUrl, present, client, key) {
|
|
9099
|
+
async function pullSnapshots(db, peerUrl, present, client, key, skewed = {}) {
|
|
8909
9100
|
if (present.length === 0)
|
|
8910
9101
|
return 0;
|
|
8911
9102
|
const consumed = getSnapshotConsumed(db, peerUrl);
|
|
@@ -8924,7 +9115,7 @@ async function pullSnapshots(db, peerUrl, present, client, key) {
|
|
|
8924
9115
|
if (!bytes)
|
|
8925
9116
|
continue;
|
|
8926
9117
|
const changes = await decodeSegment(bytes, key);
|
|
8927
|
-
pulled +=
|
|
9118
|
+
pulled += collectSkew(skewed, ingestDetailed(db, changes));
|
|
8928
9119
|
consumed.add(k);
|
|
8929
9120
|
for (const c of changes)
|
|
8930
9121
|
if (!local[c.node_id] || c.hlc > local[c.node_id])
|
|
@@ -8945,6 +9136,7 @@ var init_storage = __esm(() => {
|
|
|
8945
9136
|
init_crdt();
|
|
8946
9137
|
init_publisher_lease();
|
|
8947
9138
|
init_errors();
|
|
9139
|
+
init_client();
|
|
8948
9140
|
init_e2ee();
|
|
8949
9141
|
init_intent_retention();
|
|
8950
9142
|
DEFAULT_SNAPSHOT_MAX_INTERVAL_MS = 30 * 60000;
|
|
@@ -9156,7 +9348,7 @@ var package_default;
|
|
|
9156
9348
|
var init_package = __esm(() => {
|
|
9157
9349
|
package_default = {
|
|
9158
9350
|
name: "@tensorix/metahub",
|
|
9159
|
-
version: "0.5.
|
|
9351
|
+
version: "0.5.3",
|
|
9160
9352
|
description: "Metahub core library and CLI",
|
|
9161
9353
|
license: "AGPL-3.0-only",
|
|
9162
9354
|
type: "module",
|
|
@@ -9528,7 +9720,7 @@ function applyGuestIntent(db, policy, session, intent, opts) {
|
|
|
9528
9720
|
kind: policy.audience,
|
|
9529
9721
|
guestNode: session.guestNode
|
|
9530
9722
|
};
|
|
9531
|
-
|
|
9723
|
+
return writeTx(db, () => {
|
|
9532
9724
|
const prepared = prepareIntent(db, policy, principal, intent, opts.clock);
|
|
9533
9725
|
const txn = intentTxnPrefix(session.guestNode, intent.intentId) + prepared.fingerprint;
|
|
9534
9726
|
const receipt = readReceipt(db, session.guestNode, intent.intentId, acceptedNow);
|
|
@@ -9548,7 +9740,6 @@ function applyGuestIntent(db, policy, session, intent, opts) {
|
|
|
9548
9740
|
return applySubmitted(db, policy, principal, intent, prepared, txn, acceptedNow);
|
|
9549
9741
|
return applyAuthority(db, policy, principal, intent, prepared, txn, acceptedNow);
|
|
9550
9742
|
});
|
|
9551
|
-
return tx();
|
|
9552
9743
|
}
|
|
9553
9744
|
function prepareIntent(db, policy, principal, intent, clock) {
|
|
9554
9745
|
if (typeof intent.payload !== "object" || intent.payload === null || Array.isArray(intent.payload))
|
|
@@ -9586,7 +9777,7 @@ function prepareIntent(db, policy, principal, intent, clock) {
|
|
|
9586
9777
|
};
|
|
9587
9778
|
}
|
|
9588
9779
|
function applyAuthority(db, policy, principal, intent, prepared, txn, acceptedNow) {
|
|
9589
|
-
return withTxnId(txn, () => withNodeId(principal.guestNode, () => {
|
|
9780
|
+
return withTxnId(db, txn, () => withNodeId(principal.guestNode, () => {
|
|
9590
9781
|
if (intent.action === "createRecord")
|
|
9591
9782
|
assertGuestCreateCapacity(db, prepared.database, policy.limits);
|
|
9592
9783
|
const current = intent.action === "createRecord" ? createRecordPrepared(db, prepared.database, prepared.cells) : updateRecordPrepared(db, intent.recordId, prepared.cells);
|
|
@@ -9710,7 +9901,7 @@ function isRecordRow(value) {
|
|
|
9710
9901
|
}
|
|
9711
9902
|
function clampMillis(submittedAt, now) {
|
|
9712
9903
|
const safeNow = Math.max(0, Math.trunc(Number.isFinite(now) ? now : Date.now()));
|
|
9713
|
-
const ceil = safeNow +
|
|
9904
|
+
const ceil = safeNow + HLC_MAX_SKEW_MS;
|
|
9714
9905
|
if (!Number.isFinite(submittedAt))
|
|
9715
9906
|
return safeNow;
|
|
9716
9907
|
return Math.max(0, Math.min(Math.trunc(submittedAt), ceil));
|
|
@@ -9749,16 +9940,16 @@ function stableStringify(value) {
|
|
|
9749
9940
|
}
|
|
9750
9941
|
return JSON.stringify(value) ?? "null";
|
|
9751
9942
|
}
|
|
9752
|
-
var
|
|
9943
|
+
var INTENT_ID_RE, RECEIPT_COL = "result";
|
|
9753
9944
|
var init_guest_intent = __esm(() => {
|
|
9754
9945
|
init_errors();
|
|
9755
9946
|
init_records();
|
|
9947
|
+
init_hlc();
|
|
9756
9948
|
init_crdt();
|
|
9757
9949
|
init_ids();
|
|
9758
9950
|
init_intent_retention();
|
|
9759
9951
|
init_grants_core();
|
|
9760
9952
|
init_intent_retention();
|
|
9761
|
-
HLC_SKEW_MS = 5 * 60000;
|
|
9762
9953
|
INTENT_ID_RE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
9763
9954
|
});
|
|
9764
9955
|
|
|
@@ -9766,6 +9957,7 @@ var init_guest_intent = __esm(() => {
|
|
|
9766
9957
|
var ROOM_PROTOCOL_VERSION = 1, ROOM_BLOB_CHUNK_LIMIT, STATE_TABLE;
|
|
9767
9958
|
var init_room_protocol = __esm(() => {
|
|
9768
9959
|
init_errors();
|
|
9960
|
+
init_history_cache();
|
|
9769
9961
|
init_crdt();
|
|
9770
9962
|
init_schema_init();
|
|
9771
9963
|
init_ids();
|
|
@@ -9867,13 +10059,13 @@ async function syncWithRoom(db, cfg, transport, opts = {}) {
|
|
|
9867
10059
|
}
|
|
9868
10060
|
assertGuestAuthored(resp.changes, cfg.guestBase);
|
|
9869
10061
|
const pulled = ingest(db, resp.changes);
|
|
9870
|
-
db
|
|
10062
|
+
writeTx(db, () => {
|
|
9871
10063
|
applyPartitionDiff(db, cfg.peerKey, {
|
|
9872
10064
|
entered: [...prep.shipped, ...pulledMemberRows(db, cfg, resp.changes)],
|
|
9873
10065
|
left: prep.diff.left
|
|
9874
10066
|
});
|
|
9875
10067
|
writeCursors(db, cfg.peerKey, { pull: resp.cursor, push: prep.pushCursor });
|
|
9876
|
-
})
|
|
10068
|
+
});
|
|
9877
10069
|
if (resp.need_baseline.length > 0)
|
|
9878
10070
|
await healBaselines(db, cfg, transport, resp.need_baseline);
|
|
9879
10071
|
let healed = false;
|
|
@@ -9909,14 +10101,14 @@ async function healBaselines(db, cfg, transport, keys) {
|
|
|
9909
10101
|
const resp = await roundTrip(db, cfg, transport, changes, evict);
|
|
9910
10102
|
if (resp.share_state === "expired")
|
|
9911
10103
|
return;
|
|
9912
|
-
db
|
|
10104
|
+
writeTx(db, () => {
|
|
9913
10105
|
applyPartitionDiff(db, cfg.peerKey, {
|
|
9914
10106
|
entered: [...entered, ...pulledMemberRows(db, cfg, resp.changes)],
|
|
9915
10107
|
left: evict
|
|
9916
10108
|
});
|
|
9917
10109
|
const cur = readCursors(db, cfg.peerKey);
|
|
9918
10110
|
writeCursors(db, cfg.peerKey, { pull: resp.cursor, push: cur.push });
|
|
9919
|
-
})
|
|
10111
|
+
});
|
|
9920
10112
|
}
|
|
9921
10113
|
async function fullReconcile(db, cfg, transport) {
|
|
9922
10114
|
const members = computePartitionMembers(db, cfg.scope);
|
|
@@ -9924,11 +10116,11 @@ async function fullReconcile(db, cfg, transport) {
|
|
|
9924
10116
|
const resp = await roundTrip(db, cfg, transport, winners, [], members);
|
|
9925
10117
|
if (resp.share_state === "expired")
|
|
9926
10118
|
return;
|
|
9927
|
-
db
|
|
10119
|
+
writeTx(db, () => {
|
|
9928
10120
|
resetPartitionShadow(db, cfg.peerKey, members);
|
|
9929
10121
|
const cur = readCursors(db, cfg.peerKey);
|
|
9930
10122
|
writeCursors(db, cfg.peerKey, { pull: resp.cursor, push: cur.push });
|
|
9931
|
-
})
|
|
10123
|
+
});
|
|
9932
10124
|
}
|
|
9933
10125
|
async function roundTrip(db, cfg, transport, changes, evict, members) {
|
|
9934
10126
|
const resp = await transport({
|
|
@@ -10330,7 +10522,7 @@ function updatePublicSiteChannelUrls(db, siteId, siteName) {
|
|
|
10330
10522
|
return changed;
|
|
10331
10523
|
}
|
|
10332
10524
|
function applySiteUpdate(db, siteId, opts, hooks) {
|
|
10333
|
-
return db
|
|
10525
|
+
return withChangeGroup(db, "site.update", () => {
|
|
10334
10526
|
const updated = updateSite(db, siteId, {
|
|
10335
10527
|
...opts,
|
|
10336
10528
|
visibility: opts.visibility === "public" ? "private" : opts.visibility
|
|
@@ -10342,15 +10534,15 @@ function applySiteUpdate(db, siteId, opts, hooks) {
|
|
|
10342
10534
|
if (opts.name !== undefined)
|
|
10343
10535
|
updatePublicSiteChannelUrls(db, siteId, updated.name);
|
|
10344
10536
|
return getSite(db, siteId);
|
|
10345
|
-
})
|
|
10537
|
+
});
|
|
10346
10538
|
}
|
|
10347
10539
|
function applySiteDelete(db, siteId) {
|
|
10348
|
-
return db
|
|
10540
|
+
return withChangeGroup(db, "site.delete", () => {
|
|
10349
10541
|
if (!deleteSite(db, siteId))
|
|
10350
10542
|
return false;
|
|
10351
10543
|
revokeAllSiteChannels(db, siteId);
|
|
10352
10544
|
return true;
|
|
10353
|
-
})
|
|
10545
|
+
});
|
|
10354
10546
|
}
|
|
10355
10547
|
function revokeAllSiteChannels(db, siteId) {
|
|
10356
10548
|
const active = listSiteChannelRows(db, siteId).filter((channel) => channel.desired_state === "active");
|
|
@@ -10475,7 +10667,7 @@ var init_site_channel_store = __esm(() => {
|
|
|
10475
10667
|
policy_json: input.policy == null ? null : JSON.stringify(input.policy),
|
|
10476
10668
|
desired_state: input.desiredState ?? "active"
|
|
10477
10669
|
};
|
|
10478
|
-
db
|
|
10670
|
+
writeTx(db, () => {
|
|
10479
10671
|
if (!existing) {
|
|
10480
10672
|
const first = emit(db, "site_channels", id, "site_id", input.siteId);
|
|
10481
10673
|
emit(db, "site_channels", id, "created_hlc", first.hlc);
|
|
@@ -10485,7 +10677,7 @@ var init_site_channel_store = __esm(() => {
|
|
|
10485
10677
|
continue;
|
|
10486
10678
|
emit(db, "site_channels", id, col, value);
|
|
10487
10679
|
}
|
|
10488
|
-
})
|
|
10680
|
+
});
|
|
10489
10681
|
return getSiteChannelRow(db, id);
|
|
10490
10682
|
});
|
|
10491
10683
|
setSiteChannelDesiredState = grouped(function setSiteChannelDesiredState(db, id, desired) {
|
|
@@ -10763,14 +10955,13 @@ async function rotateStoragePeer(db, url, input) {
|
|
|
10763
10955
|
};
|
|
10764
10956
|
}
|
|
10765
10957
|
function removePeer(db, url) {
|
|
10766
|
-
|
|
10958
|
+
return writeTx(db, () => {
|
|
10767
10959
|
const changed = db.query("DELETE FROM peers WHERE url = ?").run(url).changes > 0;
|
|
10768
10960
|
db.query("DELETE FROM peer_grants WHERE peer_url = ?").run(url);
|
|
10769
10961
|
db.query("DELETE FROM storage_cursors WHERE peer_url = ?").run(url);
|
|
10770
10962
|
db.query("DELETE FROM room_rows WHERE peer_key = ?").run(url);
|
|
10771
10963
|
return changed;
|
|
10772
10964
|
});
|
|
10773
|
-
return tx();
|
|
10774
10965
|
}
|
|
10775
10966
|
function setPeerEnabled(db, url, enabled) {
|
|
10776
10967
|
return db.query("UPDATE peers SET enabled = ? WHERE url = ?").run(enabled ? 1 : 0, url).changes > 0;
|
|
@@ -10845,7 +11036,7 @@ async function syncPeerOnce(db, url, opts) {
|
|
|
10845
11036
|
result = await syncWithPeer(db, url, { timeoutMs: opts?.timeoutMs });
|
|
10846
11037
|
}
|
|
10847
11038
|
updatePeerStatus(db, url, "ok", null);
|
|
10848
|
-
const warnings = [];
|
|
11039
|
+
const warnings = [...result.warnings ?? []];
|
|
10849
11040
|
try {
|
|
10850
11041
|
if (peer?.kind !== "s3" && peer?.kind !== "room")
|
|
10851
11042
|
clearCoveredSiteRollbacks(db, url);
|
|
@@ -11014,20 +11205,20 @@ function normalizePublicBaseUrl(input) {
|
|
|
11014
11205
|
try {
|
|
11015
11206
|
parsed = new URL(input.trim());
|
|
11016
11207
|
} catch {
|
|
11017
|
-
throw new
|
|
11208
|
+
throw new MhError("invalid_input", "\u8BF7\u8F93\u5165\u5B8C\u6574\u7684 http(s) \u5730\u5740");
|
|
11018
11209
|
}
|
|
11019
11210
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
11020
|
-
throw new
|
|
11211
|
+
throw new MhError("invalid_input", "\u7AD9\u70B9\u5165\u53E3\u53EA\u652F\u6301 HTTP \u6216 HTTPS");
|
|
11021
11212
|
if (parsed.username || parsed.password || parsed.search || parsed.hash)
|
|
11022
|
-
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");
|
|
11023
11214
|
if (parsed.pathname !== "/" && parsed.pathname !== "")
|
|
11024
|
-
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");
|
|
11025
11216
|
const host = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
11026
11217
|
const scope = hostScope(host);
|
|
11027
11218
|
if (scope === "invalid")
|
|
11028
|
-
throw new
|
|
11219
|
+
throw new MhError("invalid_input", "\u7AD9\u70B9\u5165\u53E3\u5FC5\u987B\u662F\u53EF\u8BBF\u95EE\u7684\u4E3B\u673A\u5730\u5740");
|
|
11029
11220
|
if (parsed.protocol === "http:" && scope === "public")
|
|
11030
|
-
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");
|
|
11031
11222
|
parsed.pathname = "";
|
|
11032
11223
|
const url = parsed.toString().replace(/\/+$/, "");
|
|
11033
11224
|
return { url, scope };
|
|
@@ -11075,6 +11266,7 @@ function setServerConfig(db, partial) {
|
|
|
11075
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;
|
|
11076
11267
|
var init_config = __esm(() => {
|
|
11077
11268
|
init_token();
|
|
11269
|
+
init_errors();
|
|
11078
11270
|
MAX_BLOB_UPLOAD_BYTES = Number(process.env.METAHUB_MAX_BLOB_UPLOAD) || 100 * 1024 * 1024;
|
|
11079
11271
|
DEFAULT_CONFIG = {
|
|
11080
11272
|
host: "127.0.0.1",
|
|
@@ -12099,6 +12291,8 @@ function redeemPairingCode(db, code) {
|
|
|
12099
12291
|
}
|
|
12100
12292
|
function mintGrant(db, peerUrl, nodeId) {
|
|
12101
12293
|
const token = randomSuffix(32);
|
|
12294
|
+
if (nodeId)
|
|
12295
|
+
db.query("DELETE FROM peer_grants WHERE node_id = ?").run(nodeId);
|
|
12102
12296
|
db.query("INSERT INTO peer_grants (token, peer_url, node_id, created_at) VALUES (?, ?, ?, ?)").run(token, peerUrl, nodeId, Date.now());
|
|
12103
12297
|
return token;
|
|
12104
12298
|
}
|
|
@@ -12433,6 +12627,7 @@ async function refreshBucketPresence(db, url) {
|
|
|
12433
12627
|
}
|
|
12434
12628
|
var init_devices = __esm(() => {
|
|
12435
12629
|
init_errors();
|
|
12630
|
+
init_hlc();
|
|
12436
12631
|
init_node();
|
|
12437
12632
|
init_peers();
|
|
12438
12633
|
init_pairing();
|
|
@@ -12979,12 +13174,171 @@ var init_blob_routes = __esm(() => {
|
|
|
12979
13174
|
];
|
|
12980
13175
|
});
|
|
12981
13176
|
|
|
12982
|
-
// src/core/sync/
|
|
12983
|
-
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);
|
|
12984
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);
|
|
12985
13338
|
const code = errorCode(e);
|
|
12986
13339
|
return Response.json(code ? { error: message, code } : { error: message }, {
|
|
12987
|
-
status: code ? HTTP_STATUS[code] : 400
|
|
13340
|
+
status: code ? HTTP_STATUS[code] : 400,
|
|
13341
|
+
...code === "busy" ? { headers: { "retry-after": "1" } } : {}
|
|
12988
13342
|
});
|
|
12989
13343
|
}
|
|
12990
13344
|
var HTTP_STATUS, syncRoutes, routes;
|
|
@@ -12995,6 +13349,7 @@ var init_routes = __esm(() => {
|
|
|
12995
13349
|
init_peers_routes();
|
|
12996
13350
|
init_blob_routes();
|
|
12997
13351
|
init_errors();
|
|
13352
|
+
init_page_messages();
|
|
12998
13353
|
init_package();
|
|
12999
13354
|
HTTP_STATUS = {
|
|
13000
13355
|
invalid_input: 400,
|
|
@@ -13005,6 +13360,8 @@ var init_routes = __esm(() => {
|
|
|
13005
13360
|
auth: 401,
|
|
13006
13361
|
network: 502,
|
|
13007
13362
|
rate_limited: 429,
|
|
13363
|
+
busy: 503,
|
|
13364
|
+
clock_skew: 409,
|
|
13008
13365
|
port_in_use: 500
|
|
13009
13366
|
};
|
|
13010
13367
|
syncRoutes = [
|
|
@@ -13016,12 +13373,18 @@ var init_routes = __esm(() => {
|
|
|
13016
13373
|
response: SyncResponseSchema,
|
|
13017
13374
|
async handler(req, { db, node }) {
|
|
13018
13375
|
const body = await req.json();
|
|
13019
|
-
|
|
13376
|
+
const r = ingestDetailed(db, body.changes ?? []);
|
|
13377
|
+
const skewed = Object.values(r.skewed).reduce((a, b) => a + b, 0);
|
|
13020
13378
|
const batch = changesAfterSeq(db, body.since ?? 0, {
|
|
13021
13379
|
limit: body.limit,
|
|
13022
13380
|
excludeDatasets: body.exclude_datasets
|
|
13023
13381
|
});
|
|
13024
|
-
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
|
+
});
|
|
13025
13388
|
}
|
|
13026
13389
|
},
|
|
13027
13390
|
{
|
|
@@ -13229,12 +13592,22 @@ function renewToken(req, url, cfg) {
|
|
|
13229
13592
|
function wantsHtml(req) {
|
|
13230
13593
|
return req.method === "GET" && (req.headers.get("accept") ?? "").includes("text/html");
|
|
13231
13594
|
}
|
|
13232
|
-
function unlockPage() {
|
|
13233
|
-
|
|
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>
|
|
13234
13607
|
<meta charset="utf-8">
|
|
13235
13608
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
13236
13609
|
<meta name="theme-color" content="#ffffff">
|
|
13237
|
-
<title
|
|
13610
|
+
<title>${m("\u9A8C\u8BC1\u8BBF\u95EE\u4EE4\u724C \xB7 metahub")}</title>
|
|
13238
13611
|
<script>
|
|
13239
13612
|
// FOUC-guard theme resolution, same key/logic as src/webui/theme.ts.
|
|
13240
13613
|
try {
|
|
@@ -13248,18 +13621,18 @@ function unlockPage() {
|
|
|
13248
13621
|
<div class="boot" id="boot"><svg viewBox="0 0 24 24">${CUBE_SVG}</svg></div>
|
|
13249
13622
|
<form class="card" id="f" style="display:none" autocomplete="on">
|
|
13250
13623
|
<div class="brand"><svg viewBox="0 0 24 24">${CUBE_SVG}</svg></div>
|
|
13251
|
-
<h1
|
|
13252
|
-
<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>
|
|
13253
13626
|
<div class="field">
|
|
13254
|
-
<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")}"
|
|
13255
13628
|
autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" enterkeyhint="go">
|
|
13256
|
-
<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>
|
|
13257
13630
|
</div>
|
|
13258
13631
|
<p class="err" id="e" style="display:none"></p>
|
|
13259
|
-
<button type="submit" class="go" id="go"
|
|
13632
|
+
<button type="submit" class="go" id="go">${m("\u89E3\u9501\u5E76\u8FDB\u5165")}</button>
|
|
13260
13633
|
</form>
|
|
13261
13634
|
<script>
|
|
13262
|
-
var KEY = "mh_token";
|
|
13635
|
+
var KEY = "mh_token", L = ${L};
|
|
13263
13636
|
var form = document.getElementById("f"), boot = document.getElementById("boot");
|
|
13264
13637
|
var input = document.getElementById("t"), errEl = document.getElementById("e");
|
|
13265
13638
|
var goBtn = document.getElementById("go"), peek = document.getElementById("peek");
|
|
@@ -13284,7 +13657,7 @@ function unlockPage() {
|
|
|
13284
13657
|
}
|
|
13285
13658
|
function fail(msg) {
|
|
13286
13659
|
goBtn.disabled = false;
|
|
13287
|
-
goBtn.
|
|
13660
|
+
goBtn.textContent = L.go;
|
|
13288
13661
|
errEl.textContent = msg;
|
|
13289
13662
|
errEl.style.display = "flex";
|
|
13290
13663
|
form.classList.remove("bad");
|
|
@@ -13297,7 +13670,7 @@ function unlockPage() {
|
|
|
13297
13670
|
var pw = input.type === "password";
|
|
13298
13671
|
input.type = pw ? "text" : "password";
|
|
13299
13672
|
peek.innerHTML = '<svg viewBox="0 0 24 24">' + (pw ? ${JSON.stringify(EYE_OFF_SVG)} : ${JSON.stringify(EYE_SVG)}) + "</svg>";
|
|
13300
|
-
peek.setAttribute("aria-label", pw ?
|
|
13673
|
+
peek.setAttribute("aria-label", pw ? L.hide : L.show);
|
|
13301
13674
|
input.focus();
|
|
13302
13675
|
});
|
|
13303
13676
|
input.addEventListener("input", function () {
|
|
@@ -13307,9 +13680,9 @@ function unlockPage() {
|
|
|
13307
13680
|
form.addEventListener("submit", function (ev) {
|
|
13308
13681
|
ev.preventDefault();
|
|
13309
13682
|
var token = normalize(input.value);
|
|
13310
|
-
if (!token) { fail(
|
|
13683
|
+
if (!token) { fail(L.empty); return; }
|
|
13311
13684
|
goBtn.disabled = true;
|
|
13312
|
-
goBtn.innerHTML = '<span class="spin"></span
|
|
13685
|
+
goBtn.innerHTML = '<span class="spin"></span>' + L.verifying;
|
|
13313
13686
|
var hdr = { headers: { authorization: "Bearer " + token } };
|
|
13314
13687
|
// Prefer the renewal endpoint (managed mode returns the canonical token,
|
|
13315
13688
|
// also upgrading a pasted in-grace token). Fall back to a cheap
|
|
@@ -13320,7 +13693,7 @@ function unlockPage() {
|
|
|
13320
13693
|
.then(function (d) {
|
|
13321
13694
|
if (d && d.token) { save(d.token); location.reload(); return; }
|
|
13322
13695
|
return fetch("/api/version", hdr).then(function (r) {
|
|
13323
|
-
if (r.status === 401) { fail(
|
|
13696
|
+
if (r.status === 401) { fail(L.invalid); return; }
|
|
13324
13697
|
// 200 (static-token mode) or no UI (404) \u2192 trust it and reload.
|
|
13325
13698
|
save(token); location.reload();
|
|
13326
13699
|
});
|
|
@@ -13337,7 +13710,7 @@ function unlockPage() {
|
|
|
13337
13710
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
13338
13711
|
.then(function (d) {
|
|
13339
13712
|
if (d && d.token) { save(d.token); location.reload(); }
|
|
13340
|
-
else showForm(
|
|
13713
|
+
else showForm(L.expired);
|
|
13341
13714
|
})
|
|
13342
13715
|
.catch(function () { showForm(""); });
|
|
13343
13716
|
} else {
|
|
@@ -13465,8 +13838,9 @@ var COOKIE = "mh_token", COOKIE_ATTRS = "path=/; SameSite=Strict; Max-Age=315360
|
|
|
13465
13838
|
@keyframes spin{to{transform:rotate(360deg)}}
|
|
13466
13839
|
@media (prefers-reduced-motion:reduce){*{animation-duration:.01ms!important}
|
|
13467
13840
|
.brand path{stroke-dashoffset:0}}
|
|
13468
|
-
|
|
13841
|
+
`, escapeAttr = (v) => v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
13469
13842
|
var init_auth = __esm(() => {
|
|
13843
|
+
init_page_messages();
|
|
13470
13844
|
init_inject_runtime();
|
|
13471
13845
|
init_token();
|
|
13472
13846
|
init_pairing();
|
|
@@ -13496,7 +13870,9 @@ var init_http_status = __esm(() => {
|
|
|
13496
13870
|
stale: 409,
|
|
13497
13871
|
conflict: 409,
|
|
13498
13872
|
auth: 401,
|
|
13499
|
-
rate_limited: 429
|
|
13873
|
+
rate_limited: 429,
|
|
13874
|
+
busy: 503,
|
|
13875
|
+
clock_skew: 409
|
|
13500
13876
|
};
|
|
13501
13877
|
});
|
|
13502
13878
|
|
|
@@ -14275,7 +14651,7 @@ async function serveSite(req, ctx, auth, opts = {}) {
|
|
|
14275
14651
|
const ambientOnly = mutating && authActive(auth) && explicitToken(req, url) == null;
|
|
14276
14652
|
if (!ambientOnly && hasValidToken(req, url, auth)) {
|
|
14277
14653
|
if (!site)
|
|
14278
|
-
return
|
|
14654
|
+
return siteNotFound(req, name);
|
|
14279
14655
|
if (!opts.forwardApi)
|
|
14280
14656
|
return new Response("not found", { status: 404 });
|
|
14281
14657
|
const fwd = new Request(`${url.origin}/${filePath}${url.search}`, req);
|
|
@@ -14302,17 +14678,18 @@ async function serveSite(req, ctx, auth, opts = {}) {
|
|
|
14302
14678
|
return serveSiteFile(req, ctx.db, site.id, filePath, { spa: site.spa === 1, isPublic: true });
|
|
14303
14679
|
}
|
|
14304
14680
|
if (!hasValidToken(req, url, auth)) {
|
|
14305
|
-
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();
|
|
14306
14682
|
}
|
|
14307
14683
|
if (!site)
|
|
14308
|
-
return
|
|
14684
|
+
return siteNotFound(req, name);
|
|
14309
14685
|
const res = await serveSiteFile(req, ctx.db, site.id, filePath, { spa: site.spa === 1 });
|
|
14310
14686
|
return withShim(res, auth, req, url);
|
|
14311
14687
|
}
|
|
14312
14688
|
async function serveSiteFile(req, db, siteId, path, opts = {}) {
|
|
14689
|
+
const locale = pickLocale(req);
|
|
14313
14690
|
const meta = getFileMetaForServe(db, siteId, path, { spa: opts.spa });
|
|
14314
14691
|
if (!meta)
|
|
14315
|
-
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"));
|
|
14316
14693
|
const cacheControl = siteCacheControl(meta.row.content_type, meta.row.encoding, opts.isPublic ?? false);
|
|
14317
14694
|
const headers = { etag: meta.etag, "cache-control": cacheControl };
|
|
14318
14695
|
if (etagMatches(req.headers.get("if-none-match"), meta.etag)) {
|
|
@@ -14320,7 +14697,7 @@ async function serveSiteFile(req, db, siteId, path, opts = {}) {
|
|
|
14320
14697
|
}
|
|
14321
14698
|
const bytes = await decodeFileRow(db, meta.row);
|
|
14322
14699
|
if (!bytes) {
|
|
14323
|
-
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"));
|
|
14324
14701
|
}
|
|
14325
14702
|
const body = new Uint8Array(bytes.byteLength);
|
|
14326
14703
|
body.set(bytes);
|
|
@@ -14345,8 +14722,12 @@ function etagMatches(header, etag) {
|
|
|
14345
14722
|
}
|
|
14346
14723
|
return false;
|
|
14347
14724
|
}
|
|
14348
|
-
function
|
|
14349
|
-
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>
|
|
14350
14731
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
14351
14732
|
<meta name="robots" content="noindex">
|
|
14352
14733
|
<title>${escapeHtml(title)}</title>
|
|
@@ -14367,8 +14748,8 @@ function notFoundResponse(title, detail) {
|
|
|
14367
14748
|
<p class="code">404</p>
|
|
14368
14749
|
<h1>${escapeHtml(title)}</h1>
|
|
14369
14750
|
<p>${detail}</p>
|
|
14370
|
-
<a class="home" href="./"
|
|
14371
|
-
<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>
|
|
14372
14753
|
</main></body></html>`;
|
|
14373
14754
|
return new Response(html, {
|
|
14374
14755
|
status: 404,
|
|
@@ -14383,6 +14764,7 @@ var init_sites_serve = __esm(() => {
|
|
|
14383
14764
|
init_grants_routes();
|
|
14384
14765
|
init_rate_limit();
|
|
14385
14766
|
init_share_render();
|
|
14767
|
+
init_page_messages();
|
|
14386
14768
|
init_auth();
|
|
14387
14769
|
});
|
|
14388
14770
|
|
|
@@ -14437,6 +14819,51 @@ async function mintGuestSession(scope, opts) {
|
|
|
14437
14819
|
return { sub: opts.sub, exp, cookie };
|
|
14438
14820
|
}
|
|
14439
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
|
+
|
|
14440
14867
|
// src/core/sync/share-serve.ts
|
|
14441
14868
|
function notFound() {
|
|
14442
14869
|
return new Response("not found", { status: 404 });
|
|
@@ -14467,25 +14894,26 @@ async function serveShare(req, ctx, opts = {}) {
|
|
|
14467
14894
|
return notFound();
|
|
14468
14895
|
if (share.transport !== "server")
|
|
14469
14896
|
return notFound();
|
|
14897
|
+
const locale = pickLocale(req);
|
|
14470
14898
|
if (shareExpired(share))
|
|
14471
14899
|
return gone();
|
|
14472
14900
|
if (sub === "unlock" && req.method === "POST")
|
|
14473
14901
|
return handleUnlock(ctx, req, share, url);
|
|
14474
14902
|
const locked = !!share.pw_hash && !await readShareSession(ctx, req, share);
|
|
14475
14903
|
if (locked) {
|
|
14476
|
-
return wantsHtml2(req) ? new Response(passwordPage(share, false), { headers: HTML }) : unauthorizedJson();
|
|
14904
|
+
return wantsHtml2(req) ? new Response(passwordPage(locale, share, false), { headers: HTML }) : unauthorizedJson();
|
|
14477
14905
|
}
|
|
14478
14906
|
if (sub.startsWith("blob/"))
|
|
14479
14907
|
return serveScopedBlob(ctx, share, sub.slice("blob/".length));
|
|
14480
14908
|
if (share.kind === "doc")
|
|
14481
|
-
return serveDoc(ctx, req, share, sub);
|
|
14909
|
+
return serveDoc(ctx, req, share, sub, locale);
|
|
14482
14910
|
if (share.kind === "database")
|
|
14483
|
-
return serveTable(ctx, req, share, sub);
|
|
14911
|
+
return serveTable(ctx, req, share, sub, locale);
|
|
14484
14912
|
if (share.kind === "site")
|
|
14485
|
-
return serveSiteShare(ctx, req, share, sub, opts);
|
|
14913
|
+
return serveSiteShare(ctx, req, share, sub, opts, locale);
|
|
14486
14914
|
return notFound();
|
|
14487
14915
|
}
|
|
14488
|
-
async function serveDoc(ctx, req, share, sub) {
|
|
14916
|
+
async function serveDoc(ctx, req, share, sub, locale) {
|
|
14489
14917
|
const doc = getDocument(ctx.db, share.target_id);
|
|
14490
14918
|
if (!doc)
|
|
14491
14919
|
return notFound();
|
|
@@ -14514,26 +14942,27 @@ async function serveDoc(ctx, req, share, sub) {
|
|
|
14514
14942
|
});
|
|
14515
14943
|
const version = documentVersion(ctx.db, share.target_id);
|
|
14516
14944
|
const editable = share.permission === "edit";
|
|
14517
|
-
const inner = `<article class="doc">${rendered ||
|
|
14518
|
-
const script = editable ? docEditScript(share.slug, version, doc.body ?? "") : "";
|
|
14519
|
-
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 });
|
|
14520
14948
|
}
|
|
14521
|
-
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") });
|
|
14522
14951
|
return `
|
|
14523
|
-
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};
|
|
14524
14953
|
const art=document.querySelector('article.doc');
|
|
14525
14954
|
const btn=document.getElementById('mh-edit');
|
|
14526
14955
|
let ta;
|
|
14527
14956
|
btn.addEventListener('click', async ()=>{
|
|
14528
14957
|
if(!editing){
|
|
14529
|
-
editing=true; btn.textContent=
|
|
14958
|
+
editing=true; btn.textContent=L.save;
|
|
14530
14959
|
ta=document.createElement('textarea'); ta.className='mh-raw'; ta.value=body0;
|
|
14531
14960
|
art.replaceWith(ta); ta.focus();
|
|
14532
14961
|
} else {
|
|
14533
|
-
btn.disabled=true; btn.textContent=
|
|
14962
|
+
btn.disabled=true; btn.textContent=L.saving;
|
|
14534
14963
|
const res=await fetch('/share/${slug}/doc',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({body:ta.value,ifMatch:ver})});
|
|
14535
14964
|
if(res.ok){ location.reload(); }
|
|
14536
|
-
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; }
|
|
14537
14966
|
}
|
|
14538
14967
|
});`;
|
|
14539
14968
|
}
|
|
@@ -14562,7 +14991,7 @@ function cellHtml(prop, value, titles) {
|
|
|
14562
14991
|
function editableType(t) {
|
|
14563
14992
|
return t === "text" || t === "number" || t === "url" || t === "date";
|
|
14564
14993
|
}
|
|
14565
|
-
async function serveTable(ctx, req, share, sub) {
|
|
14994
|
+
async function serveTable(ctx, req, share, sub, locale) {
|
|
14566
14995
|
const dbRow = getDatabase(ctx.db, share.target_id);
|
|
14567
14996
|
if (!dbRow)
|
|
14568
14997
|
return notFound();
|
|
@@ -14613,13 +15042,18 @@ async function serveTable(ctx, req, share, sub) {
|
|
|
14613
15042
|
return `<tr>${tds}</tr>`;
|
|
14614
15043
|
}).join("");
|
|
14615
15044
|
const inner = `<div class="table-wrap"><table class="db"><thead><tr>${head}</tr></thead><tbody>${rows}</tbody></table></div>`;
|
|
14616
|
-
const script = editable ? tableEditScript(share.slug) : "";
|
|
14617
|
-
|
|
14618
|
-
|
|
14619
|
-
|
|
14620
|
-
|
|
14621
|
-
|
|
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) {
|
|
14622
15055
|
return `
|
|
15056
|
+
const L=${JSON.stringify({ failed: pm(locale, "\u4FDD\u5B58\u5931\u8D25") })};
|
|
14623
15057
|
document.querySelectorAll('td[contenteditable]').forEach(td=>{
|
|
14624
15058
|
let orig=td.textContent;
|
|
14625
15059
|
td.addEventListener('blur', async ()=>{
|
|
@@ -14631,11 +15065,11 @@ function tableEditScript(slug) {
|
|
|
14631
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})});
|
|
14632
15066
|
td.classList.remove('saving');
|
|
14633
15067
|
if(res.ok){ orig=val; td.classList.add('saved'); setTimeout(()=>td.classList.remove('saved'),600); }
|
|
14634
|
-
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); }
|
|
14635
15069
|
});
|
|
14636
15070
|
});`;
|
|
14637
15071
|
}
|
|
14638
|
-
async function serveSiteShare(ctx, req, share, sub, opts = {}) {
|
|
15072
|
+
async function serveSiteShare(ctx, req, share, sub, opts = {}, locale = "zh-CN") {
|
|
14639
15073
|
let site;
|
|
14640
15074
|
try {
|
|
14641
15075
|
site = resolveSite(ctx.db, share.target_id);
|
|
@@ -14658,7 +15092,7 @@ async function serveSiteShare(ctx, req, share, sub, opts = {}) {
|
|
|
14658
15092
|
}
|
|
14659
15093
|
if (share.permission === "edit") {
|
|
14660
15094
|
if (sub === "_files")
|
|
14661
|
-
return new Response(siteFilesPage(ctx, share, siteId), { headers: HTML });
|
|
15095
|
+
return new Response(siteFilesPage(ctx, share, siteId, locale), { headers: HTML });
|
|
14662
15096
|
if (sub === "_file" && req.method === "POST") {
|
|
14663
15097
|
const body = await req.json().catch(() => ({}));
|
|
14664
15098
|
if (!body.path || typeof body.content !== "string")
|
|
@@ -14690,34 +15124,35 @@ async function serveSiteShare(ctx, req, share, sub, opts = {}) {
|
|
|
14690
15124
|
}
|
|
14691
15125
|
return serveSiteFile(req, ctx.db, siteId, sub, { spa: site.spa === 1 });
|
|
14692
15126
|
}
|
|
14693
|
-
function siteFilesPage(ctx, share, siteId) {
|
|
15127
|
+
function siteFilesPage(ctx, share, siteId, locale) {
|
|
14694
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") });
|
|
14695
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("");
|
|
14696
15131
|
const inner = `
|
|
14697
|
-
<p class="hint"
|
|
14698
|
-
<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>
|
|
14699
15134
|
<div id="editor" hidden>
|
|
14700
|
-
<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>
|
|
14701
15136
|
<textarea class="mh-raw" id="fbody"></textarea>
|
|
14702
|
-
<div class="row"><button id="save"
|
|
15137
|
+
<div class="row"><button id="save">${escapeHtml(pm(locale, "\u4FDD\u5B58"))}</button></div>
|
|
14703
15138
|
</div>`;
|
|
14704
15139
|
const script = `
|
|
14705
|
-
const slug=${JSON.stringify(share.slug)};
|
|
15140
|
+
const slug=${JSON.stringify(share.slug)}; const L=${L};
|
|
14706
15141
|
const ed=document.getElementById('editor'), fpath=document.getElementById('fpath'), fbody=document.getElementById('fbody'), open=document.getElementById('open');
|
|
14707
15142
|
let cur=null;
|
|
14708
15143
|
document.querySelectorAll('button.file').forEach(b=>b.addEventListener('click', async ()=>{
|
|
14709
15144
|
cur=b.dataset.path; fpath.textContent=cur; open.href='/share/'+slug+'/'+cur;
|
|
14710
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; }
|
|
14711
|
-
else { fbody.value=
|
|
15146
|
+
else { fbody.value=L.binary; fbody.disabled=true; }
|
|
14712
15147
|
ed.hidden=false;
|
|
14713
15148
|
}));
|
|
14714
15149
|
document.getElementById('save').addEventListener('click', async ()=>{
|
|
14715
15150
|
if(!cur||fbody.disabled) return;
|
|
14716
15151
|
const res=await fetch('/share/'+slug+'/_file',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({path:cur,content:fbody.value})});
|
|
14717
|
-
if(res.ok){ const s=document.getElementById('save'); s.textContent=
|
|
14718
|
-
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); }
|
|
14719
15154
|
});`;
|
|
14720
|
-
return pageShell(
|
|
15155
|
+
return pageShell(pm(locale, "{name} \xB7 \u6587\u4EF6", { name: resolveSite(ctx.db, share.target_id).name }), inner, { lang: locale, script });
|
|
14721
15156
|
}
|
|
14722
15157
|
function referencedHashes2(ctx, share) {
|
|
14723
15158
|
const out = new Set;
|
|
@@ -14805,7 +15240,7 @@ async function handleUnlock(ctx, req, share, url) {
|
|
|
14805
15240
|
pw = form ? String(form.get("password") ?? "") : "";
|
|
14806
15241
|
}
|
|
14807
15242
|
if (!await verifySharePassword(share, pw)) {
|
|
14808
|
-
return new Response(passwordPage(share, true), { headers: HTML, status: 401 });
|
|
15243
|
+
return new Response(passwordPage(pickLocale(req), share, true), { headers: HTML, status: 401 });
|
|
14809
15244
|
}
|
|
14810
15245
|
const { cookie } = await mintShareSession(ctx, share, url);
|
|
14811
15246
|
return new Response(null, {
|
|
@@ -14816,21 +15251,23 @@ async function handleUnlock(ctx, req, share, url) {
|
|
|
14816
15251
|
function ttlRemaining(share) {
|
|
14817
15252
|
return share.expires_at == null ? SHARE_SESSION_TTL_MS : Math.max(0, share.expires_at - Date.now());
|
|
14818
15253
|
}
|
|
14819
|
-
function passwordPage(share, error) {
|
|
15254
|
+
function passwordPage(locale, share, error) {
|
|
14820
15255
|
const inner = `
|
|
14821
15256
|
<form method="post" action="/share/${share.slug}/unlock" class="pw">
|
|
14822
|
-
<h1
|
|
14823
|
-
${error ?
|
|
14824
|
-
<input type="password" name="password" placeholder="\u53E3\u4EE4" autofocus autocomplete="current-password">
|
|
14825
|
-
<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>
|
|
14826
15261
|
</form>`;
|
|
14827
|
-
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 });
|
|
14828
15263
|
}
|
|
14829
|
-
function pageShell(title, inner, opts
|
|
14830
|
-
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>` : "";
|
|
14831
15268
|
const hint = opts.hint ? `<p class="hint">${escapeHtml(opts.hint)}</p>` : "";
|
|
14832
|
-
const script = opts.script ? `<script>(function(){${opts.script}})();</script>` : "";
|
|
14833
|
-
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>
|
|
14834
15271
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
14835
15272
|
<meta name="robots" content="noindex">
|
|
14836
15273
|
<title>${escapeHtml(title)}</title>
|
|
@@ -14842,7 +15279,8 @@ function pageShell(title, inner, opts = {}) {
|
|
|
14842
15279
|
.wrap{max-width:820px;margin:0 auto;padding:32px 20px 80px}
|
|
14843
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}
|
|
14844
15281
|
header.mh h1.title{font-size:22px;margin:0}
|
|
14845
|
-
.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}
|
|
14846
15284
|
.hint{color:var(--muted);font-size:13px;margin:0 0 14px}
|
|
14847
15285
|
article.doc h1,article.doc h2,article.doc h3{line-height:1.3;margin:1.4em 0 .5em}
|
|
14848
15286
|
article.doc h1{font-size:1.7em} article.doc h2{font-size:1.4em} article.doc h3{font-size:1.2em}
|
|
@@ -14873,10 +15311,10 @@ function pageShell(title, inner, opts = {}) {
|
|
|
14873
15311
|
footer.mh{margin-top:48px;border-top:1px solid var(--line);padding-top:14px;color:var(--muted);font-size:12px;text-align:center}
|
|
14874
15312
|
</style></head><body>
|
|
14875
15313
|
${opts.bare ? `<div class="wrap">${inner}</div>` : `<div class="wrap">
|
|
14876
|
-
<header class="mh"><h1 class="title">${escapeHtml(title)}</h1>${
|
|
15314
|
+
<header class="mh"><h1 class="title">${escapeHtml(title)}</h1>${tools}</header>
|
|
14877
15315
|
${hint}
|
|
14878
15316
|
${inner}
|
|
14879
|
-
<footer class="mh"
|
|
15317
|
+
<footer class="mh">${escapeHtml(pm(opts.lang, "\u901A\u8FC7 metahub \u5206\u4EAB"))}</footer>
|
|
14880
15318
|
</div>`}
|
|
14881
15319
|
${script}
|
|
14882
15320
|
</body></html>`;
|
|
@@ -14899,6 +15337,8 @@ var init_share_serve = __esm(() => {
|
|
|
14899
15337
|
init_grants_routes();
|
|
14900
15338
|
init_rate_limit();
|
|
14901
15339
|
init_share_render();
|
|
15340
|
+
init_share_copy();
|
|
15341
|
+
init_page_messages();
|
|
14902
15342
|
HTML = { "content-type": "text/html; charset=utf-8" };
|
|
14903
15343
|
HASH_RE2 = /^[0-9a-f]{16,64}$/;
|
|
14904
15344
|
SHARE_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
|
|
@@ -15275,11 +15715,12 @@ var DROP_ENVELOPE_MAX_BYTES, DROP_HLC_SKEW_MS, GUEST_NODE_RE;
|
|
|
15275
15715
|
var init_drop_protocol = __esm(() => {
|
|
15276
15716
|
init_errors();
|
|
15277
15717
|
init_ids();
|
|
15718
|
+
init_hlc();
|
|
15278
15719
|
init_grants_core();
|
|
15279
15720
|
init_e2ee();
|
|
15280
15721
|
init_seal();
|
|
15281
15722
|
DROP_ENVELOPE_MAX_BYTES = 64 * 1024;
|
|
15282
|
-
DROP_HLC_SKEW_MS =
|
|
15723
|
+
DROP_HLC_SKEW_MS = HLC_MAX_SKEW_MS;
|
|
15283
15724
|
GUEST_NODE_RE = /^g[0-9a-z]{8}$/;
|
|
15284
15725
|
});
|
|
15285
15726
|
|
|
@@ -15333,12 +15774,12 @@ function applyDropV2(db, set, payload, now) {
|
|
|
15333
15774
|
};
|
|
15334
15775
|
const before = db.query("SELECT MAX(seq) AS s FROM crdt_changes").get().s ?? 0;
|
|
15335
15776
|
const ids = [];
|
|
15336
|
-
db
|
|
15777
|
+
writeTx(db, () => {
|
|
15337
15778
|
for (const intent of payload.intents) {
|
|
15338
15779
|
applyGuestIntent(db, policy, { guestNode: payload.guest_node }, intent, { clock: "submitted", now });
|
|
15339
15780
|
ids.push(intent.intentId);
|
|
15340
15781
|
}
|
|
15341
|
-
})
|
|
15782
|
+
});
|
|
15342
15783
|
const ingested = db.query("SELECT COUNT(*) AS n FROM crdt_changes WHERE seq > ?").get(before).n;
|
|
15343
15784
|
return { ackSeq: intentsMaxSeq(db, payload.guest_node, ids), ingested };
|
|
15344
15785
|
}
|
|
@@ -15532,6 +15973,7 @@ function appKind() {
|
|
|
15532
15973
|
function openMetahub() {
|
|
15533
15974
|
ensureDirs();
|
|
15534
15975
|
const db = new Database(dbPath(), { create: true });
|
|
15976
|
+
db.exec("PRAGMA busy_timeout = 5000;");
|
|
15535
15977
|
db.exec("PRAGMA journal_mode = WAL;");
|
|
15536
15978
|
initSchema(db);
|
|
15537
15979
|
const app = appKind();
|
|
@@ -15556,6 +15998,7 @@ function safeHostname() {
|
|
|
15556
15998
|
// src/core/index.ts
|
|
15557
15999
|
init_ids();
|
|
15558
16000
|
init_resolve();
|
|
16001
|
+
init_hlc();
|
|
15559
16002
|
init_crdt();
|
|
15560
16003
|
init_node();
|
|
15561
16004
|
init_databases();
|
|
@@ -15682,18 +16125,18 @@ function ensureIndex(db) {
|
|
|
15682
16125
|
return false;
|
|
15683
16126
|
const version = readMeta(db, "search_index_version");
|
|
15684
16127
|
const seq = readMeta(db, "search_seq");
|
|
15685
|
-
db
|
|
16128
|
+
writeTx(db, () => {
|
|
15686
16129
|
if (version !== SEARCH_INDEX_VERSION || seq === null)
|
|
15687
16130
|
fullRebuild(db);
|
|
15688
16131
|
else
|
|
15689
16132
|
incrementalUpdate(db);
|
|
15690
|
-
})
|
|
16133
|
+
});
|
|
15691
16134
|
return true;
|
|
15692
16135
|
}
|
|
15693
16136
|
function rebuildSearchIndex(db) {
|
|
15694
16137
|
if (!ftsAvailable(db))
|
|
15695
16138
|
return false;
|
|
15696
|
-
db
|
|
16139
|
+
writeTx(db, () => fullRebuild(db));
|
|
15697
16140
|
return true;
|
|
15698
16141
|
}
|
|
15699
16142
|
function ftsSearch(db, query, limit) {
|
|
@@ -15853,6 +16296,7 @@ function startServer(opts = {}) {
|
|
|
15853
16296
|
allowRemoteSiteHosting: opts.allowRemoteSiteHosting ?? true
|
|
15854
16297
|
};
|
|
15855
16298
|
const allRoutes = opts.ui ? [...routes, ...opts.ui.routes] : routes;
|
|
16299
|
+
opts.ui?.onStart?.(db);
|
|
15856
16300
|
const forwardApi = async (req) => {
|
|
15857
16301
|
const path = new URL(req.url).pathname;
|
|
15858
16302
|
const route = allRoutes.find((r) => r.method === req.method && r.path === path);
|
|
@@ -15898,7 +16342,7 @@ function startServer(opts = {}) {
|
|
|
15898
16342
|
} else {
|
|
15899
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/");
|
|
15900
16344
|
if (!exempt && !hasValidToken(req, url, auth)) {
|
|
15901
|
-
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();
|
|
15902
16346
|
}
|
|
15903
16347
|
}
|
|
15904
16348
|
if (url.pathname === RENEW_PATH) {
|
|
@@ -16065,6 +16509,8 @@ var styles_default = ` :root {
|
|
|
16065
16509
|
--fs-body:14px; --fs-ui:13.5px; --fs-sm:12.5px; --fs-xs:11.5px; --fs-micro:11px;
|
|
16066
16510
|
--ui:"Hanken Grotesk",-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
|
|
16067
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);
|
|
16068
16514
|
--note-lh:1.6;
|
|
16069
16515
|
--code-bg:#f8f8f6;
|
|
16070
16516
|
--hl-cmt:#9a9aa2; --hl-key:#a626a4; --hl-str:#28823f; --hl-num:#b76a00;
|
|
@@ -16083,6 +16529,7 @@ var styles_default = ` :root {
|
|
|
16083
16529
|
--hover:rgba(255,255,255,.05); --hover-2:rgba(255,255,255,.09);
|
|
16084
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);
|
|
16085
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);
|
|
16086
16533
|
--hl-cmt:#7d7d86; --hl-key:#d291e4; --hl-str:#7ed492; --hl-num:#e0a566;
|
|
16087
16534
|
--hl-fn:#86a8ff; --hl-type:#5fc6e0; --hl-var:#f0936b;
|
|
16088
16535
|
}
|
|
@@ -16161,12 +16608,11 @@ var styles_default = ` :root {
|
|
|
16161
16608
|
background:linear-gradient(135deg,var(--accent),color-mix(in srgb,var(--accent) 60%,#fff));
|
|
16162
16609
|
display:grid; place-items:center; color:#fff; box-shadow:var(--shadow-sm); }
|
|
16163
16610
|
.brand .mark svg { width:15px; height:15px; stroke:#fff; }
|
|
16164
|
-
.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;
|
|
16165
16612
|
background:var(--bg); border:1px solid var(--line); border-radius:var(--radius-sm); padding:6px 9px; color:var(--muted);
|
|
16166
|
-
box-shadow:var(--shadow-sm); transition:border-color .15s; }
|
|
16167
|
-
.sb-search:focus-
|
|
16168
|
-
.sb-search
|
|
16169
|
-
.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); }
|
|
16170
16616
|
.sb-scroll { flex:1; overflow-y:auto; padding:4px 8px 24px; }
|
|
16171
16617
|
/* Notion-style list switcher: the active tab is an icon+label pill, inactive
|
|
16172
16618
|
tabs are icon-only. One absolutely-positioned indicator slides & stretches
|
|
@@ -16380,6 +16826,9 @@ var styles_default = ` :root {
|
|
|
16380
16826
|
.set-row-tag { display:inline-block; margin-left:8px; font-size:11px; font-weight:500;
|
|
16381
16827
|
color:var(--muted); border:1px solid var(--line); border-radius:999px; padding:1px 7px; vertical-align:1px; }
|
|
16382
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); }
|
|
16383
16832
|
|
|
16384
16833
|
/* Page chrome (primitives.tsx PageHeader / DangerZone) \u2014 every page opens
|
|
16385
16834
|
with its own PageHeader (the shell renders no title). */
|
|
@@ -16388,7 +16837,7 @@ var styles_default = ` :root {
|
|
|
16388
16837
|
.page-header-text { flex:1 1 auto; min-width:0; }
|
|
16389
16838
|
.page-header-title { font-size:26px; font-weight:700; letter-spacing:-.02em; color:var(--fg); margin:0; }
|
|
16390
16839
|
.page-header-sub { color:var(--muted); font-size:var(--fs-sm); margin-top:3px; }
|
|
16391
|
-
.page-header-action { flex:none; padding-top:4px; }
|
|
16840
|
+
.page-header-action { flex:none; display:flex; align-items:center; gap:8px; padding-top:4px; }
|
|
16392
16841
|
.page-header-banner { margin-top:8px; }
|
|
16393
16842
|
.danger-zone { margin-top:34px; }
|
|
16394
16843
|
.danger-zone-label { font-size:12px; font-weight:600; text-transform:uppercase; letter-spacing:.07em;
|
|
@@ -16437,11 +16886,6 @@ var styles_default = ` :root {
|
|
|
16437
16886
|
.audit-end::before, .audit-end::after { content:""; flex:1; height:1px; background:var(--line); }
|
|
16438
16887
|
@media (hover:none) { .audit-revert { opacity:1; } }
|
|
16439
16888
|
.set-callout.warn { border-color:color-mix(in srgb,var(--warning,#9a6700) 32%,var(--line)); }
|
|
16440
|
-
/* two-fact meta line under a block title (who publishes \xB7 where creds live) \u2014
|
|
16441
|
-
answers the questions hiding under "is this bucket for server or browser?" */
|
|
16442
|
-
.set-meta { display:flex; flex-wrap:wrap; gap:8px 16px; margin:0 0 12px; }
|
|
16443
|
-
.set-meta-item { display:inline-flex; align-items:center; gap:6px; font-size:var(--fs-sm); color:var(--fg-soft); }
|
|
16444
|
-
.set-meta-item svg { width:14px; height:14px; color:var(--accent); }
|
|
16445
16889
|
/* a quiet helper line under a field / chip group */
|
|
16446
16890
|
.set-hint { font-size:var(--fs-xs); color:var(--muted); line-height:1.5; margin:3px 0 4px; }
|
|
16447
16891
|
/* progressive disclosure ("\u5DE5\u4F5C\u539F\u7406" / "\u8FDB\u9636") \u2014 native details, themed */
|
|
@@ -16526,39 +16970,18 @@ var styles_default = ` :root {
|
|
|
16526
16970
|
.peer-sub { font-size:12px; color:var(--muted); margin-top:2px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
16527
16971
|
.peer-menu { flex:none; padding:6px; }
|
|
16528
16972
|
|
|
16529
|
-
/*
|
|
16530
|
-
|
|
16531
|
-
.sync-health { margin:0; }
|
|
16532
|
-
.sync-health-line { display:inline-flex; align-items:center; gap:8px; max-width:100%; padding:2px 0;
|
|
16533
|
-
background:none; border:0; cursor:pointer; text-align:left; font:inherit; color:var(--fg-soft); }
|
|
16534
|
-
.sync-health-line:hover { color:var(--fg); }
|
|
16535
|
-
.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; }
|
|
16536
16975
|
.sync-health-dot { flex:none; width:8px; height:8px; border-radius:50%; }
|
|
16537
16976
|
.sync-health-dot.dot-ok { background:var(--success); }
|
|
16538
16977
|
.sync-health-dot.dot-warn { background:var(--warning,#9a6700); }
|
|
16539
16978
|
.sync-health-dot.dot-error { background:var(--danger); }
|
|
16540
|
-
|
|
16541
|
-
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
16542
|
-
.sync-health-more { flex:none; font-size:var(--fs-xs); color:var(--muted); }
|
|
16543
|
-
/* shared expander chevron (SyncHealthLine \u8BE6\u60C5, device roster rows) */
|
|
16979
|
+
/* shared expander chevron (bucket + device roster rows) */
|
|
16544
16980
|
.row-chev { flex:none; color:var(--muted); display:flex; transition:transform .15s; }
|
|
16545
16981
|
.row-chev .ico { width:14px; height:14px; }
|
|
16546
16982
|
.row-chev.open { transform:rotate(180deg); }
|
|
16547
16983
|
/* confirm dialogs: keep \\n line breaks (consequence lists) */
|
|
16548
16984
|
.cfm-msg { white-space: pre-line; }
|
|
16549
|
-
/* concurrent problems under the headline (only when >1, else redundant) */
|
|
16550
|
-
.sync-health-issues { padding:2px 0 4px; }
|
|
16551
|
-
.sync-health-issue { font-size:12px; color:var(--muted); padding:2px 0 2px 18px; }
|
|
16552
|
-
/* \u67E5\u770B\u5404\u5904: a bordered place list (kind \xB7 name \xB7 freshness \xB7 inline retry) */
|
|
16553
|
-
.sync-health-places { margin-top:6px; border:1px solid var(--line); border-radius:var(--radius); overflow:hidden; }
|
|
16554
|
-
.sync-health-place { display:flex; align-items:center; gap:10px; padding:9px 12px; min-width:0; font-size:var(--fs-sm); }
|
|
16555
|
-
.sync-health-place + .sync-health-place { border-top:1px solid var(--line); }
|
|
16556
|
-
.sync-health-place-kind { flex:none; width:64px; font-size:var(--fs-xs); color:var(--muted); }
|
|
16557
|
-
.sync-health-place-label { flex:none; font-weight:550; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:40%; }
|
|
16558
|
-
.sync-health-place-cap { flex:1; min-width:0; color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
16559
|
-
.sync-health-place .btn { margin-left:auto; }
|
|
16560
|
-
.sync-health-role { flex:none; font-size:11px; color:var(--accent); background:var(--accent-soft);
|
|
16561
|
-
border-radius:999px; padding:2px 7px; }
|
|
16562
16985
|
|
|
16563
16986
|
/* device roster (settings/devices-page.tsx): expanded detail = key/value
|
|
16564
16987
|
table + one action row + one plain sentence \u2014 no gray callout box. */
|
|
@@ -16580,7 +17003,6 @@ var styles_default = ` :root {
|
|
|
16580
17003
|
display:grid; grid-template-columns:1fr auto; gap:4px 12px; align-items:center; background:var(--surface-1); }
|
|
16581
17004
|
.cloud-cta-title { font-weight:650; font-size:var(--fs-body); }
|
|
16582
17005
|
.cloud-cta-sub { font-size:12px; color:var(--muted); margin-top:2px; line-height:1.6; }
|
|
16583
|
-
.cloud-cta-alt { grid-column:1 / -1; font-size:12px; }
|
|
16584
17006
|
|
|
16585
17007
|
/* printable recovery card */
|
|
16586
17008
|
.recovery-card { border:2px solid var(--fg); border-radius:12px; padding:16px 18px; margin-top:8px; background:var(--surface); }
|
|
@@ -16627,6 +17049,8 @@ var styles_default = ` :root {
|
|
|
16627
17049
|
.crumb-link { flex:none; padding:2px 5px; margin:-2px -3px; border-radius:var(--radius-sm); font-weight:500; }
|
|
16628
17050
|
.crumb-link:hover { background:var(--hover); }
|
|
16629
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); }
|
|
16630
17054
|
.iconbtn { width:30px; height:30px; display:grid; place-items:center; border-radius:var(--radius-sm); color:var(--fg-soft); transition:background .12s; }
|
|
16631
17055
|
.iconbtn:hover { background:var(--hover-2); color:var(--fg); }
|
|
16632
17056
|
/* Dead end of the history (topbar back/forward). pointer-events:none also
|
|
@@ -16720,7 +17144,7 @@ var styles_default = ` :root {
|
|
|
16720
17144
|
/* document editor */
|
|
16721
17145
|
.doc { max-width:740px; margin:0 auto; padding:60px 60px 36vh; }
|
|
16722
17146
|
.doc-title { font-size:38px; font-weight:700; letter-spacing:-.02em; outline:none; line-height:1.15; margin-bottom:4px; }
|
|
16723
|
-
.doc-title:empty::before { content:
|
|
17147
|
+
.doc-title:empty::before { content:attr(data-placeholder); color:var(--muted); }
|
|
16724
17148
|
/* Backstop for the plain-text titles (see plain-edit.ts): should markup ever
|
|
16725
17149
|
slip into one of these hosts, its inline font-size can't shrink the title. */
|
|
16726
17150
|
.doc-title *, .db-title * { font:inherit; color:inherit; background:none; }
|
|
@@ -17210,11 +17634,16 @@ var styles_default = ` :root {
|
|
|
17210
17634
|
.db-icon { width:38px; height:38px; border-radius:9px; background:var(--surface-2); display:grid; place-items:center; font-size:20px; }
|
|
17211
17635
|
.db-title { font-size:27px; font-weight:700; letter-spacing:-.02em; outline:none; }
|
|
17212
17636
|
.db-desc { color:var(--muted); font-size:13px; margin-top:2px; }
|
|
17213
|
-
.views { display:flex; align-items:center; gap:2px; border-bottom:1px solid var(--line); margin:16px 0 0; }
|
|
17214
|
-
.
|
|
17215
|
-
|
|
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; }
|
|
17216
17641
|
.view-tab.active { color:var(--fg); border-bottom-color:var(--accent); }
|
|
17217
|
-
.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); }
|
|
17218
17647
|
/* \u6D6E\u7A97\u770B\u677F (right end of the toolbar / board bar, desktop only): a quiet
|
|
17219
17648
|
Notion-style secondary pill \u2014 visible as a feature entry, never louder
|
|
17220
17649
|
than \u65B0\u5EFA. Hover lifts the pill and nudges the icon's top kanban card up
|
|
@@ -17230,12 +17659,56 @@ var styles_default = ` :root {
|
|
|
17230
17659
|
.popout-btn:active { transform:translateY(0); box-shadow:var(--shadow-sm); }
|
|
17231
17660
|
.popout-btn .i-card { transition:transform .18s ease; }
|
|
17232
17661
|
.popout-btn:hover .i-card { transform:translateY(-2px); }
|
|
17233
|
-
.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; }
|
|
17234
17663
|
.toolbar .spacer { flex:1; }
|
|
17235
|
-
.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; }
|
|
17236
17665
|
.tbtn:hover { background:var(--hover-2); color:var(--fg); }
|
|
17237
17666
|
.tbtn.on { color:var(--accent); }
|
|
17238
|
-
.
|
|
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); }
|
|
17239
17712
|
.tablescroll { overflow-x:auto; }
|
|
17240
17713
|
/* fixed layout + max-content: each <col> width is authoritative, so the table
|
|
17241
17714
|
is exactly the sum of its columns and resizing one column never redistributes
|
|
@@ -17244,12 +17717,26 @@ var styles_default = ` :root {
|
|
|
17244
17717
|
table.grid { border-collapse:collapse; table-layout:fixed; width:max-content; min-width:100%; }
|
|
17245
17718
|
table.grid th, table.grid td { border-right:1px solid var(--line); border-bottom:1px solid var(--line); padding:0; text-align:left; }
|
|
17246
17719
|
table.grid th:last-child, table.grid td:last-child { border-right:0; }
|
|
17247
|
-
table.grid tbody tr:last-child td { border-bottom:
|
|
17248
|
-
thead th { background:var(--
|
|
17249
|
-
.colhead { display:flex; align-items:center; gap:7px; padding:
|
|
17250
|
-
.colhead:hover { background:var(--hover); }
|
|
17251
|
-
.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; }
|
|
17252
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; }
|
|
17253
17740
|
.col-resizer { position:absolute; top:0; right:-4px; width:7px; height:100%; cursor:col-resize; z-index:2; touch-action:none; }
|
|
17254
17741
|
.addcol { min-width:46px; } .addcol .colhead { justify-content:flex-start; color:var(--muted); }
|
|
17255
17742
|
/* trailing filler column: empty canvas to the right of the data, not a real cell */
|
|
@@ -17257,19 +17744,24 @@ var styles_default = ` :root {
|
|
|
17257
17744
|
body.col-resizing { user-select:none; cursor:col-resize; }
|
|
17258
17745
|
table.grid .selcell { width:38px; min-width:38px; max-width:38px; text-align:center; }
|
|
17259
17746
|
/* relative: anchors the .celledit overlay editor to the cell */
|
|
17260
|
-
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; }
|
|
17261
17749
|
.cell { padding:8px 11px; min-height:37px; cursor:text; word-break:break-word; display:flex; flex-wrap:wrap; gap:3px; align-items:center; }
|
|
17262
17750
|
/* hover/active tint goes on the <td> (always full row height) not the inner
|
|
17263
17751
|
.cell (only as tall as its content), so a taller sibling row never leaves a
|
|
17264
17752
|
gap of un-tinted space at the bottom of the hovered cell. */
|
|
17265
17753
|
td.cell-td { transition:background .1s; }
|
|
17266
|
-
|
|
17754
|
+
.cell.center { justify-content:center; }
|
|
17267
17755
|
tbody tr { transition:background .08s; }
|
|
17268
17756
|
tbody tr:hover { background:var(--hover); }
|
|
17269
17757
|
tbody tr.sel { background:var(--accent-soft); }
|
|
17270
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. */
|
|
17271
17759
|
td.cell-td.cellsel, td.cell-td.cellsel:hover { background:var(--accent-soft); }
|
|
17272
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; }
|
|
17273
17765
|
body.cell-selecting .cell { cursor:cell; }
|
|
17274
17766
|
/* column reorder still marks the target <th>; rows use the .rowdrop line below */
|
|
17275
17767
|
table.grid th.drop-before { box-shadow:inset 2px 0 0 0 var(--accent); }
|
|
@@ -17325,23 +17817,57 @@ var styles_default = ` :root {
|
|
|
17325
17817
|
/* data-resolved (not prefers-color-scheme): tracks a manual dark choice too. */
|
|
17326
17818
|
:root[data-resolved="dark"] .chip { background:color-mix(in srgb,var(--c) 22%,transparent); color:color-mix(in srgb,var(--c) 75%,#fff); }
|
|
17327
17819
|
/* relation chip: an <a> that jumps to the target record */
|
|
17328
|
-
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; }
|
|
17329
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; }
|
|
17330
17822
|
/* doc chip whose target is deleted/unsynced: muted + dashed, same cue as the
|
|
17331
17823
|
editor's .cm-doclink-missing pill */
|
|
17332
17824
|
a.chip.rel.doc-missing { background:transparent; color:var(--muted); outline:1px dashed var(--line-strong); outline-offset:-1px; }
|
|
17333
|
-
.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; }
|
|
17334
17826
|
.addrow:hover { background:var(--hover); color:var(--fg-soft); }
|
|
17827
|
+
.addrow svg { width:14px; height:14px; }
|
|
17335
17828
|
.gridfoot { display:flex; gap:18px; color:var(--muted); font-size:12px; padding:8px 2px; }
|
|
17336
|
-
|
|
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; }
|
|
17337
17832
|
/* Overlay cell editor: floats over the td; the display content underneath
|
|
17338
17833
|
stays in flow so the row height never changes while editing. Padding on the
|
|
17339
|
-
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. */
|
|
17340
17836
|
.celledit { position:absolute; top:0; left:0; width:100%; min-height:100%; z-index:10;
|
|
17341
|
-
display:flex; align-items:stretch; background:var(--bg); border-radius:
|
|
17342
|
-
box-shadow:0 0 0 2px var(--accent), var(--shadow-md); }
|
|
17343
|
-
.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;
|
|
17344
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; }
|
|
17345
17871
|
|
|
17346
17872
|
/* ---- sites (\u7AD9\u70B9\u7BA1\u7406) ---- */
|
|
17347
17873
|
.sites-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(264px, 1fr)); gap:14px; margin-top:4px; }
|
|
@@ -17690,6 +18216,11 @@ var styles_default = ` :root {
|
|
|
17690
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; }
|
|
17691
18217
|
.proprow .v:hover { background:var(--hover); }
|
|
17692
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; }
|
|
17693
18224
|
/* linked-document tabs + embedded editor below the peek's property list */
|
|
17694
18225
|
.peekdocs-tabs { display:flex; align-items:center; gap:4px; flex-wrap:wrap; margin-bottom:6px; }
|
|
17695
18226
|
.peekdocs-tab { border:0; background:none; font:inherit; font-size:var(--fs-sm); font-weight:500; color:var(--muted);
|
|
@@ -17720,7 +18251,7 @@ var styles_default = ` :root {
|
|
|
17720
18251
|
.hist-kind.repair { background:var(--danger-soft); color:var(--danger); }
|
|
17721
18252
|
.hist-now { font-size:var(--fs-xs); padding:1px 6px; border-radius:99px; background:var(--accent); color:var(--accent-fg); }
|
|
17722
18253
|
.hist-preview { flex:1; overflow:auto; padding:20px 24px; }
|
|
17723
|
-
.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; }
|
|
17724
18255
|
.hist-foot { display:flex; justify-content:flex-end; padding:12px 16px; border-top:1px solid var(--line); }
|
|
17725
18256
|
.pad { padding:12px; }
|
|
17726
18257
|
.hist-rec-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:10px; }
|
|
@@ -17827,50 +18358,68 @@ var styles_default = ` :root {
|
|
|
17827
18358
|
button.fname.link { border:0; background:none; padding:0; text-align:left; cursor:pointer; font-size:inherit; }
|
|
17828
18359
|
button.fname.link:hover { color:var(--accent); text-decoration:underline; text-decoration-style:dashed; }
|
|
17829
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
|
+
|
|
17830
18380
|
/* menu / popover */
|
|
17831
18381
|
.menu-layer { position:fixed; inset:0; z-index:115; }
|
|
17832
18382
|
.pop { position:fixed; background:var(--bg); border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow-lg);
|
|
17833
|
-
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; }
|
|
17834
18384
|
@keyframes pop { from { opacity:0; transform:translateY(-4px); } }
|
|
17835
18385
|
/* Action-sheet variant: MenuHost (ui.tsx) adds .sheet on phones, where an
|
|
17836
18386
|
anchored popover clips at the edges and sits under the finger. */
|
|
17837
18387
|
.pop.sheet { left:8px; right:8px; top:auto; bottom:calc(8px + env(safe-area-inset-bottom));
|
|
17838
|
-
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);
|
|
17839
18389
|
animation:sheet-in .18s ease; }
|
|
17840
18390
|
@keyframes sheet-in { from { opacity:0; transform:translateY(12px); } }
|
|
17841
|
-
.pop .lbl { font-size:var(--fs-micro); font-weight:600; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; padding:6px 8px 3px; }
|
|
17842
|
-
.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); }
|
|
17843
|
-
.pop .item:hover, .pop .item.sel { background:var(--hover-2); }
|
|
17844
|
-
.pop .item
|
|
17845
|
-
.pop .item .lico
|
|
17846
|
-
.pop .item .
|
|
17847
|
-
|
|
17848
|
-
.pop .item .
|
|
17849
|
-
|
|
17850
|
-
|
|
17851
|
-
.pop .item
|
|
17852
|
-
.pop .
|
|
17853
|
-
.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; }
|
|
17854
18403
|
.pop .typegrid { display:grid; grid-template-columns:repeat(4,1fr); gap:4px; padding:2px 2px 4px; }
|
|
17855
18404
|
.pop .typegrid .item { flex-direction:column; gap:5px; padding:9px 4px; text-align:center; }
|
|
17856
18405
|
.pop .typegrid .item .lico { margin:0 auto; }
|
|
17857
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; }
|
|
17858
18407
|
.pop input.field:focus { border-color:var(--accent); }
|
|
17859
18408
|
/* select-cell picker: search/create input at the top of the popover */
|
|
17860
|
-
.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;
|
|
17861
18410
|
border:1px solid var(--line-strong); border-radius:var(--radius-sm); background:var(--surface); color:var(--muted);
|
|
17862
18411
|
transition:border-color .12s ease, box-shadow .12s ease, color .12s ease; }
|
|
17863
|
-
.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);
|
|
17864
18413
|
box-shadow:0 0 0 2.5px color-mix(in srgb, var(--accent) 16%, transparent); }
|
|
17865
|
-
.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;
|
|
17866
18415
|
font:inherit; font-size:var(--fs-ui); color:var(--fg); }
|
|
17867
|
-
.pop .selsearch input::placeholder { color:var(--muted); }
|
|
17868
|
-
.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;
|
|
17869
18418
|
border-radius:50%; color:var(--muted); flex:none; }
|
|
17870
|
-
.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); }
|
|
17871
18420
|
/* relation record picker: fixed-height list \u2014 the popover is positioned once
|
|
17872
18421
|
on open, so async-loaded rows must scroll instead of growing off-screen */
|
|
17873
|
-
.pop .rellist { max-height:264px; overflow-y:auto; }
|
|
18422
|
+
:is(.pop,.palette) .rellist { max-height:264px; overflow-y:auto; }
|
|
17874
18423
|
.optrow { display:flex; align-items:center; gap:6px; padding:3px 4px; }
|
|
17875
18424
|
.optrow .chip { flex:1; }
|
|
17876
18425
|
.optrow .grip { width:18px; height:22px; display:grid; place-items:center; color:var(--muted); cursor:grab; touch-action:none; flex:none; }
|
|
@@ -17915,12 +18464,46 @@ var styles_default = ` :root {
|
|
|
17915
18464
|
/* opt-in (openModal aboveMenus): stack one dialog over the menu layer (115)
|
|
17916
18465
|
so a confirm can appear without dismissing an open popover */
|
|
17917
18466
|
.modal-scrim.above-menus { z-index:130; }
|
|
17918
|
-
.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; }
|
|
17919
18501
|
.modal-head { padding:20px 22px 0; }
|
|
17920
18502
|
.modal-head h3 { margin:0; font-size:18px; font-weight:700; letter-spacing:-.01em; }
|
|
17921
18503
|
.modal-head p { margin:6px 0 0; color:var(--muted); font-size:var(--fs-ui); }
|
|
17922
18504
|
.modal-body { padding:18px 22px; overflow-y:auto; }
|
|
17923
|
-
.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); }
|
|
17924
18507
|
.field-label { font-size:12px; font-weight:600; color:var(--fg-soft); margin:12px 0 5px; }
|
|
17925
18508
|
.field-label:first-child { margin-top:0; }
|
|
17926
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); }
|
|
@@ -17943,6 +18526,12 @@ var styles_default = ` :root {
|
|
|
17943
18526
|
.toast { background:var(--fg); color:var(--bg); padding:10px 14px; border-radius:9px; font-size:13px; box-shadow:var(--shadow-lg);
|
|
17944
18527
|
display:flex; align-items:center; gap:8px; animation:slidein .2s ease; }
|
|
17945
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); }
|
|
17946
18535
|
|
|
17947
18536
|
/* misc */
|
|
17948
18537
|
.empty { display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%;
|
|
@@ -18010,9 +18599,74 @@ var styles_default = ` :root {
|
|
|
18010
18599
|
.kg-glow { opacity:.6 !important; }
|
|
18011
18600
|
.kg-particle { opacity:0 !important; }
|
|
18012
18601
|
}
|
|
18013
|
-
.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; }
|
|
18014
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; }
|
|
18015
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; }
|
|
18016
18670
|
.search-hit:hover { background:var(--surface-2); }
|
|
18017
18671
|
.search-hit mark { background:transparent; color:var(--accent); font-weight:600; }
|
|
18018
18672
|
.view-placeholder { padding:40px; text-align:center; border:1px dashed var(--line-strong); border-radius:var(--radius); color:var(--muted); margin-top:8px; }
|
|
@@ -18235,17 +18889,15 @@ var styles_default = ` :root {
|
|
|
18235
18889
|
.navitem { padding:9px 8px; font-size:16px; }
|
|
18236
18890
|
.navitem .emoji { font-size:18px; }
|
|
18237
18891
|
.navitem .tw { width:26px; height:26px; }
|
|
18238
|
-
.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 */
|
|
18239
18893
|
/* Search box is hidden until the header search button reveals it; it then
|
|
18240
18894
|
slides+fades down in. Desktop base (.sb-search{display:flex}) is untouched,
|
|
18241
18895
|
so search stays always-on there. reduced-motion is handled globally. */
|
|
18242
18896
|
.sb-search { display:none; }
|
|
18243
|
-
.sb-search.open { display:flex; animation:search-reveal .22s cubic-bezier(.32,.72,0,1); }
|
|
18244
|
-
@keyframes search-reveal { from { opacity:0; transform:translateY(-8px); } }
|
|
18245
18897
|
.btn { font-size:15px; }
|
|
18246
18898
|
.crumb { font-size:16px; }
|
|
18247
|
-
.pop .item { font-size:16px; }
|
|
18248
|
-
.pop .item .t { font-size:16px; }
|
|
18899
|
+
:is(.pop,.palette) .item { font-size:16px; }
|
|
18900
|
+
:is(.pop,.palette) .item .t { font-size:16px; }
|
|
18249
18901
|
|
|
18250
18902
|
/* Document: bigger body text, tighter side padding for narrow screens. */
|
|
18251
18903
|
.doc { padding:24px 18px calc(36vh + env(safe-area-inset-bottom)); }
|
|
@@ -18296,11 +18948,24 @@ var styles_default = ` :root {
|
|
|
18296
18948
|
border-radius:var(--radius-xl) var(--radius-xl) 0 0;
|
|
18297
18949
|
padding-bottom:env(safe-area-inset-bottom);
|
|
18298
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; }
|
|
18299
18961
|
.toasts { left:12px; right:12px; bottom:calc(12px + env(safe-area-inset-bottom)); }
|
|
18300
18962
|
.toast { justify-content:center; }
|
|
18301
18963
|
.selbar { bottom:calc(16px + env(safe-area-inset-bottom)); max-width:calc(100vw - 24px); }
|
|
18302
18964
|
}
|
|
18303
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%); } }
|
|
18304
18969
|
/* In-flow stand-in for the old two-pane slide: only the entering view
|
|
18305
18970
|
animates (the other is display:none, document-scroll layout above). */
|
|
18306
18971
|
@keyframes page-in { from { transform:translateX(28px); opacity:0; } }
|
|
@@ -18317,7 +18982,7 @@ var styles_default = ` :root {
|
|
|
18317
18982
|
.sb-tabs .add { width:40px; height:40px; display:grid; place-items:center; padding:0; }
|
|
18318
18983
|
.iconbtn { width:40px; height:40px; }
|
|
18319
18984
|
.btn { padding:9px 14px; min-height:40px; }
|
|
18320
|
-
.pop .item { padding:11px 10px; }
|
|
18985
|
+
:is(.pop,.palette) .item { padding:11px 10px; }
|
|
18321
18986
|
/* Column/sidebar resize handles: 7px is a mouse affordance, not a finger's. */
|
|
18322
18987
|
.col-resizer, .doc-col-resizer, .sb-resizer { width:16px; right:-8px; }
|
|
18323
18988
|
/* cell-rectangle corner handle: big enough to grab with a finger (it is the
|
|
@@ -18391,6 +19056,8 @@ var styles_default = ` :root {
|
|
|
18391
19056
|
.qn-body { flex:1; overflow:auto; scrollbar-gutter:stable; scrollbar-width:thin; -webkit-app-region:no-drag; }
|
|
18392
19057
|
.qn-body .doc { max-width:none; margin:0; padding:14px 1px 30vh 12px; }
|
|
18393
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; }
|
|
18394
19061
|
.qn-body .doc-meta { display:none; }
|
|
18395
19062
|
/* The CM6 hover gutter never fits the compact window: quick notes offer no
|
|
18396
19063
|
block insert/drag handles (Enter / slash commands cover insertion). */
|
|
@@ -18411,6 +19078,7 @@ var styles_default = ` :root {
|
|
|
18411
19078
|
share .pop/.item/.lico with the main app; scope under the quick window
|
|
18412
19079
|
body classes so the main app keeps its roomier density). */
|
|
18413
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; }
|
|
18414
19082
|
body.quicknote .pop .lbl, body.quickboard .pop .lbl { padding:4px 8px 2px; font-size:10px; }
|
|
18415
19083
|
body.quicknote .pop .item, body.quickboard .pop .item { padding:5px 9px; gap:8px; font-size:var(--fs-sm); }
|
|
18416
19084
|
body.quicknote .pop .item .t, body.quickboard .pop .item .t { font-size:var(--fs-sm); }
|
|
@@ -18645,17 +19313,7 @@ var styles_default = ` :root {
|
|
|
18645
19313
|
.switch.on .switch-knob { transform:translateX(16px); }
|
|
18646
19314
|
.ico.spin { animation:save-spin .8s linear infinite; }
|
|
18647
19315
|
|
|
18648
|
-
/* clear-policy segmented control (redundancy any/all) */
|
|
18649
19316
|
.seg { display:flex; gap:8px; }
|
|
18650
|
-
.seg-opt { flex:1 1 0; display:flex; flex-direction:column; gap:2px; align-items:flex-start;
|
|
18651
|
-
text-align:left; padding:9px 12px; border:1px solid var(--line-strong); border-radius:var(--radius);
|
|
18652
|
-
background:var(--surface); cursor:pointer; transition:border-color .14s, background .14s, box-shadow .14s; }
|
|
18653
|
-
.seg-opt:hover { border-color:var(--muted); }
|
|
18654
|
-
.seg-opt.on { border-color:var(--accent); background:var(--accent-soft); box-shadow:0 0 0 1px var(--accent) inset; }
|
|
18655
|
-
.seg-opt:disabled { opacity:.6; cursor:default; }
|
|
18656
|
-
.seg-opt-t { font-size:var(--fs-sm); font-weight:600; color:var(--fg); }
|
|
18657
|
-
.seg-opt-s { font-size:var(--fs-xs); color:var(--muted); }
|
|
18658
|
-
.seg-opt.on .seg-opt-s { color:var(--accent); }
|
|
18659
19317
|
|
|
18660
19318
|
/* cache-usage ring hero (settings/cache-ring.tsx CacheRingHero): legend
|
|
18661
19319
|
column left, 152px three-segment ring right. Arcs animate 0 \u2192 share on
|
|
@@ -18671,9 +19329,6 @@ var styles_default = ` :root {
|
|
|
18671
19329
|
.blob-dot.keep { background:var(--line-strong); }
|
|
18672
19330
|
.blob-dot.pin { background:var(--muted); opacity:.6; }
|
|
18673
19331
|
.blob-total { margin:13px 0 14px; font-size:var(--fs-xs); color:var(--muted); }
|
|
18674
|
-
/* headline number over the legend (\u6570\u636E\u4E0E\u5907\u4EFD \xB7 \u9644\u4EF6) */
|
|
18675
|
-
.blob-stat { font-size:20px; font-weight:700; letter-spacing:-.02em; font-variant-numeric:tabular-nums; margin-bottom:10px; }
|
|
18676
|
-
.blob-stat small { font-size:var(--fs-sm); font-weight:500; color:var(--muted); margin-left:8px; letter-spacing:0; }
|
|
18677
19332
|
/* a status sentence in the action row (never a disabled button) */
|
|
18678
19333
|
.blob-actions .blob-status { display:inline-flex; align-items:center; gap:6px; font-size:var(--fs-sm); color:var(--muted); }
|
|
18679
19334
|
.blob-actions .blob-status .ico { width:14px; height:14px; color:var(--success); }
|
|
@@ -18821,7 +19476,7 @@ var FMT_PROVIDERS = [
|
|
|
18821
19476
|
init_package();
|
|
18822
19477
|
var RUNNING_FROM_SOURCE = import.meta.url.includes("/src/webui/");
|
|
18823
19478
|
var HTML2 = `<!doctype html>
|
|
18824
|
-
<html lang="zh">
|
|
19479
|
+
<html lang="zh-CN">
|
|
18825
19480
|
<head>
|
|
18826
19481
|
<meta charset="utf-8" />
|
|
18827
19482
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
@@ -18845,6 +19500,9 @@ var HTML2 = `<!doctype html>
|
|
|
18845
19500
|
alone, so the dark palette exists exactly once \u2014 see styles.css. Dark also
|
|
18846
19501
|
pre-tints the status bar (hex mirrors --bg) so the first frame isn't white. -->
|
|
18847
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>
|
|
18848
19506
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
18849
19507
|
<link rel="apple-touch-icon" href="/icons/icon-180.png">
|
|
18850
19508
|
<link rel="stylesheet" href="/webui.css">
|
|
@@ -19095,6 +19753,358 @@ async function serveWebui(req) {
|
|
|
19095
19753
|
}
|
|
19096
19754
|
// src/webui/server/routes.ts
|
|
19097
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
|
|
19098
20108
|
init_errors();
|
|
19099
20109
|
init_routes();
|
|
19100
20110
|
init_databases();
|
|
@@ -19107,6 +20117,7 @@ init_crdt();
|
|
|
19107
20117
|
init_documents();
|
|
19108
20118
|
init_properties();
|
|
19109
20119
|
init_errors();
|
|
20120
|
+
init_history_cache();
|
|
19110
20121
|
var REVISION_GAP_MS = 1500;
|
|
19111
20122
|
function hlcMillis(hlc) {
|
|
19112
20123
|
return Number(hlc.slice(0, 15));
|
|
@@ -19164,16 +20175,23 @@ function rowChanges(db, dataset, rowId) {
|
|
|
19164
20175
|
function everBlockIds(db, docId) {
|
|
19165
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);
|
|
19166
20177
|
}
|
|
19167
|
-
function
|
|
20178
|
+
function docHistory(db, docId) {
|
|
20179
|
+
const cached = getDocHistory(db, docId);
|
|
20180
|
+
if (cached)
|
|
20181
|
+
return cached;
|
|
19168
20182
|
const blocks = everBlockIds(db, docId);
|
|
19169
20183
|
const placeholders = blocks.map(() => "?").join(",");
|
|
19170
20184
|
const blockClause = blocks.length ? ` OR (dataset = 'doc_blocks' AND row_id IN (${placeholders}))` : "";
|
|
19171
|
-
|
|
20185
|
+
const changes = db.query(`SELECT ${CHANGE_SELECT} FROM crdt_changes
|
|
19172
20186
|
WHERE (dataset = 'documents' AND row_id = ?)${blockClause}
|
|
19173
20187
|
ORDER BY hlc`).all(docId, ...blocks);
|
|
20188
|
+
return setDocHistory(db, docId, changes, blocks);
|
|
19174
20189
|
}
|
|
19175
20190
|
function listDocumentRevisions(db, id) {
|
|
19176
|
-
const
|
|
20191
|
+
const entry = docHistory(db, id);
|
|
20192
|
+
if (entry.revisions)
|
|
20193
|
+
return entry.revisions.slice();
|
|
20194
|
+
const changes = entry.changes;
|
|
19177
20195
|
if (!changes.length)
|
|
19178
20196
|
throw new MhError("not_found", `no such document: ${id}`);
|
|
19179
20197
|
const reg = new Map;
|
|
@@ -19228,12 +20246,17 @@ function listDocumentRevisions(db, id) {
|
|
|
19228
20246
|
blocks_deleted: deletedBlocks.size
|
|
19229
20247
|
});
|
|
19230
20248
|
}
|
|
19231
|
-
|
|
20249
|
+
entry.revisions = out.reverse();
|
|
20250
|
+
return entry.revisions.slice();
|
|
19232
20251
|
}
|
|
19233
20252
|
function documentAtVersion(db, id, at) {
|
|
19234
20253
|
if (!at)
|
|
19235
20254
|
throw new MhError("invalid_input", "missing version cutoff");
|
|
19236
|
-
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);
|
|
19237
20260
|
if (!changes.length)
|
|
19238
20261
|
throw new MhError("not_found", `no version of document ${id} at or before ${at}`);
|
|
19239
20262
|
const docRegs = registersAt(changes.filter((c) => c.dataset === "documents"), at);
|
|
@@ -19279,13 +20302,15 @@ function documentAtVersion(db, id, at) {
|
|
|
19279
20302
|
return a.id < b.id ? -1 : 1;
|
|
19280
20303
|
});
|
|
19281
20304
|
const body = byBlock.size ? serializeDocBlocks(blocks) : parse("body") ?? "";
|
|
19282
|
-
|
|
20305
|
+
const state = {
|
|
19283
20306
|
id,
|
|
19284
20307
|
title: parse("title") ?? "",
|
|
19285
20308
|
body,
|
|
19286
20309
|
deleted: docRegs.get("__deleted") ? flagSet(docRegs.get("__deleted").value) : false,
|
|
19287
20310
|
version: changes[changes.length - 1].hlc
|
|
19288
20311
|
};
|
|
20312
|
+
rememberDocState(entry, at, state);
|
|
20313
|
+
return { ...state };
|
|
19289
20314
|
}
|
|
19290
20315
|
var revertDocument = grouped(function revertDocument(db, id, to, opts = {}) {
|
|
19291
20316
|
let cur = getDocument(db, id);
|
|
@@ -19917,6 +20942,7 @@ var revertChangeGroup = grouped(function revertChangeGroup(db, txn) {
|
|
|
19917
20942
|
}, "revert");
|
|
19918
20943
|
|
|
19919
20944
|
// src/webui/server/routes.ts
|
|
20945
|
+
init_resolve();
|
|
19920
20946
|
init_node();
|
|
19921
20947
|
init_blobs();
|
|
19922
20948
|
init_config();
|
|
@@ -20081,7 +21107,7 @@ async function createBucketShare(db, opts) {
|
|
|
20081
21107
|
contentType: "application/octet-stream"
|
|
20082
21108
|
});
|
|
20083
21109
|
await mergeViewerCors(opts.config, opts.viewerOrigin);
|
|
20084
|
-
return { manifestUrl, presignExp, keyB64: opts.password ? undefined : toB64(shareKey), saltB64, title };
|
|
21110
|
+
return { manifestUrl, presignExp, keyB64: opts.password ? undefined : toB64(shareKey), saltB64, title, meta };
|
|
20085
21111
|
}
|
|
20086
21112
|
async function renewBucketShare(db, config, slug, expiresSec = MAX_PRESIGN_SECONDS) {
|
|
20087
21113
|
const meta = await readMeta2(config, slug);
|
|
@@ -20148,7 +21174,7 @@ async function represignBucketShare(config, slug, expiresSec = MAX_PRESIGN_SECON
|
|
|
20148
21174
|
async function listBucketShares(config) {
|
|
20149
21175
|
const client = storageClientFor(config);
|
|
20150
21176
|
const root = sharesRoot(config.prefix);
|
|
20151
|
-
const entries = await client.list(`${root}/`, undefined, "/")
|
|
21177
|
+
const entries = await client.list(`${root}/`, undefined, "/");
|
|
20152
21178
|
const slugs = entries.map((e) => e.key.slice(`${root}/`.length).replace(/\/$/, "")).filter((s) => s && !s.includes("/"));
|
|
20153
21179
|
const out = [];
|
|
20154
21180
|
for (const slug of slugs) {
|
|
@@ -20177,10 +21203,156 @@ function clampExpiry(sec) {
|
|
|
20177
21203
|
|
|
20178
21204
|
// src/core/sync/share-actions.ts
|
|
20179
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
|
|
20180
21353
|
init_site_channel_store();
|
|
20181
21354
|
var DEFAULT_SHARE_VIEWER = "https://share.mh.tensorix.org";
|
|
20182
21355
|
var DEFAULT_S3_EXPIRY_SEC = MAX_PRESIGN_SECONDS;
|
|
20183
|
-
var PEER_LIST_TIMEOUT_MS = 4000;
|
|
20184
21356
|
var PEER_SHARE_TIMEOUT_MS = 1e4;
|
|
20185
21357
|
var CreatedShareWireSchema = object({
|
|
20186
21358
|
slug: string2().min(1).max(128),
|
|
@@ -20277,13 +21449,6 @@ function bucketLink(base, manifestUrl, keyB64, saltB64) {
|
|
|
20277
21449
|
const frag = `m=${encodeURIComponent(manifestUrl)}&` + (keyB64 ? `k=${keyB64}` : `s=${saltB64}`);
|
|
20278
21450
|
return `${base}/#${frag}`;
|
|
20279
21451
|
}
|
|
20280
|
-
function absoluteShareUrl(url, base) {
|
|
20281
|
-
try {
|
|
20282
|
-
return new URL(url, `${base.replace(/\/+$/, "")}/`).toString();
|
|
20283
|
-
} catch {
|
|
20284
|
-
return url;
|
|
20285
|
-
}
|
|
20286
|
-
}
|
|
20287
21452
|
function assertShareCombo(req) {
|
|
20288
21453
|
const fail = (msg) => {
|
|
20289
21454
|
throw new MhError("invalid_input", msg);
|
|
@@ -20321,6 +21486,7 @@ async function createShareAction(db, req) {
|
|
|
20321
21486
|
expiresSec: req.expiresMs != null ? Math.floor(req.expiresMs / 1000) : DEFAULT_S3_EXPIRY_SEC,
|
|
20322
21487
|
viewerOrigin: viewerOriginOf(base)
|
|
20323
21488
|
});
|
|
21489
|
+
primeRemoteShare(db, bucketUrl, bucketShareItem({ url: bucketUrl, label }, out.meta));
|
|
20324
21490
|
return {
|
|
20325
21491
|
slug,
|
|
20326
21492
|
kind: req.kind,
|
|
@@ -20510,28 +21676,21 @@ function listServerSharesLocal(db, targetId) {
|
|
|
20510
21676
|
}
|
|
20511
21677
|
return out;
|
|
20512
21678
|
}
|
|
20513
|
-
async function listSharesLocal(db, targetId) {
|
|
21679
|
+
async function listSharesLocal(db, targetId, opts = {}) {
|
|
20514
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
|
+
}
|
|
20515
21687
|
for (const p of listPeers(db).filter((x) => x.kind === "s3" && x.config)) {
|
|
20516
21688
|
const config = JSON.parse(p.config);
|
|
20517
21689
|
const metas = await listBucketShares(config).catch(() => []);
|
|
20518
21690
|
for (const m of metas) {
|
|
20519
21691
|
if (targetId && m.target_id !== targetId)
|
|
20520
21692
|
continue;
|
|
20521
|
-
out.push(
|
|
20522
|
-
slug: m.slug,
|
|
20523
|
-
kind: m.kind,
|
|
20524
|
-
target_id: m.target_id,
|
|
20525
|
-
title: m.title || m.target_id,
|
|
20526
|
-
permission: m.permission,
|
|
20527
|
-
transport: "s3",
|
|
20528
|
-
source: `\u6876 ${p.label ?? p.url}`,
|
|
20529
|
-
sourceKind: "bucket",
|
|
20530
|
-
hosting: "s3",
|
|
20531
|
-
expiresAt: m.presign_exp,
|
|
20532
|
-
hasPassword: m.has_password,
|
|
20533
|
-
contentUpdatedAt: m.content_updated_at ?? m.created_at
|
|
20534
|
-
});
|
|
21693
|
+
out.push(bucketShareItem(p, m));
|
|
20535
21694
|
}
|
|
20536
21695
|
}
|
|
20537
21696
|
return out;
|
|
@@ -20561,10 +21720,16 @@ async function revokeShareByRequestId(db, requestId) {
|
|
|
20561
21720
|
const share = getShareByRequestId(db, requestId);
|
|
20562
21721
|
return share ? revokeShareAction(db, share.slug) : { ok: false, status: "not_found" };
|
|
20563
21722
|
}
|
|
20564
|
-
async function listSharesAggregated(db, targetId) {
|
|
21723
|
+
async function listSharesAggregated(db, targetId, opts = {}) {
|
|
20565
21724
|
const bySlug = new Map;
|
|
20566
|
-
for (const item of await listSharesLocal(db, targetId))
|
|
21725
|
+
for (const item of await listSharesLocal(db, targetId, opts))
|
|
20567
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
|
+
}
|
|
20568
21733
|
await Promise.all(listShareServers(db).map(async ({ url, label }) => {
|
|
20569
21734
|
const peer = getPeer2(db, url);
|
|
20570
21735
|
if (!peer?.token)
|
|
@@ -20582,19 +21747,6 @@ async function listSharesAggregated(db, targetId) {
|
|
|
20582
21747
|
}));
|
|
20583
21748
|
return [...bySlug.values()];
|
|
20584
21749
|
}
|
|
20585
|
-
async function fetchPeerShares(url, token, targetId) {
|
|
20586
|
-
const u = `${url.replace(/\/+$/, "")}/api/shares${targetId ? `?target=${encodeURIComponent(targetId)}` : ""}`;
|
|
20587
|
-
const res = await Promise.race([
|
|
20588
|
-
fetch(u, { headers: { authorization: `Bearer ${token}` } }),
|
|
20589
|
-
new Promise((_r, rej) => setTimeout(() => rej(new Error("timeout")), PEER_LIST_TIMEOUT_MS))
|
|
20590
|
-
]);
|
|
20591
|
-
if (!res.ok)
|
|
20592
|
-
return [];
|
|
20593
|
-
return (await res.json()).map((item) => ({
|
|
20594
|
-
...item,
|
|
20595
|
-
...item.url ? { url: absoluteShareUrl(item.url, url) } : {}
|
|
20596
|
-
}));
|
|
20597
|
-
}
|
|
20598
21750
|
async function revokeShareAction(db, slug) {
|
|
20599
21751
|
if (getShare(db, slug)) {
|
|
20600
21752
|
const channels = listSiteChannelRows(db).filter((channel) => channel.audience === "link" && channel.target_ref === slug && channel.desired_state === "active");
|
|
@@ -20623,37 +21775,50 @@ async function revokeShareAction(db, slug) {
|
|
|
20623
21775
|
status: "revoked"
|
|
20624
21776
|
};
|
|
20625
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 };
|
|
20626
21789
|
for (const p of listPeers(db).filter((x) => x.kind === "s3" && x.config)) {
|
|
20627
|
-
const
|
|
20628
|
-
const
|
|
20629
|
-
if (
|
|
20630
|
-
|
|
20631
|
-
return { ok: true, status: "revoked" };
|
|
20632
|
-
}
|
|
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 };
|
|
20633
21794
|
}
|
|
20634
|
-
return
|
|
21795
|
+
return null;
|
|
20635
21796
|
}
|
|
20636
21797
|
async function renewShareAction(db, slug, viewerBaseOverride, opts) {
|
|
20637
|
-
|
|
20638
|
-
|
|
20639
|
-
|
|
20640
|
-
|
|
20641
|
-
|
|
20642
|
-
|
|
20643
|
-
|
|
20644
|
-
|
|
20645
|
-
|
|
20646
|
-
|
|
20647
|
-
|
|
20648
|
-
permission: "view",
|
|
20649
|
-
transport: "s3",
|
|
20650
|
-
hosting: "s3",
|
|
20651
|
-
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,
|
|
20652
21809
|
expiresAt: out.presignExp,
|
|
20653
|
-
|
|
20654
|
-
};
|
|
20655
|
-
|
|
20656
|
-
|
|
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
|
+
};
|
|
20657
21822
|
}
|
|
20658
21823
|
function freshSlug(db) {
|
|
20659
21824
|
let slug = randomSuffix(12);
|
|
@@ -20752,7 +21917,7 @@ var shareRoutes = [
|
|
|
20752
21917
|
handler: async (req, { db }) => {
|
|
20753
21918
|
try {
|
|
20754
21919
|
const target = new URL(req.url).searchParams.get("target") ?? undefined;
|
|
20755
|
-
return Response.json(await listSharesLocal(db, target));
|
|
21920
|
+
return Response.json(await listSharesLocal(db, target, { mode: "cached" }));
|
|
20756
21921
|
} catch (e) {
|
|
20757
21922
|
return errorResponse(e);
|
|
20758
21923
|
}
|
|
@@ -20766,7 +21931,7 @@ var shareRoutes = [
|
|
|
20766
21931
|
handler: async (req, { db }) => {
|
|
20767
21932
|
try {
|
|
20768
21933
|
const target = new URL(req.url).searchParams.get("target") ?? undefined;
|
|
20769
|
-
return Response.json(await listSharesAggregated(db, target));
|
|
21934
|
+
return Response.json(await listSharesAggregated(db, target, { mode: "cached" }));
|
|
20770
21935
|
} catch (e) {
|
|
20771
21936
|
return errorResponse(e);
|
|
20772
21937
|
}
|
|
@@ -21057,7 +22222,7 @@ function jsonHandler(fn) {
|
|
|
21057
22222
|
try {
|
|
21058
22223
|
return Response.json(await fn(req, ctx) ?? null);
|
|
21059
22224
|
} catch (e) {
|
|
21060
|
-
return errorResponse(e);
|
|
22225
|
+
return errorResponse(e, req);
|
|
21061
22226
|
}
|
|
21062
22227
|
};
|
|
21063
22228
|
}
|
|
@@ -21227,20 +22392,24 @@ async function verifyBase(input, expectedNode, allowRemote = true) {
|
|
|
21227
22392
|
headers: { accept: "application/json" }
|
|
21228
22393
|
});
|
|
21229
22394
|
if (res.status < 200 || res.status >= 300)
|
|
21230
|
-
throw
|
|
22395
|
+
throw mhError("network", "\u7AD9\u70B9\u5165\u53E3\u5065\u5EB7\u68C0\u67E5\u5931\u8D25\uFF08HTTP {status}\uFF09", { status: res.status });
|
|
21231
22396
|
const body = await limitedJson(res, HEALTH_RESPONSE_MAX);
|
|
21232
22397
|
if (!body?.ok || !body.node)
|
|
21233
22398
|
throw new MhError("network", "\u7AD9\u70B9\u5165\u53E3\u5065\u5EB7\u54CD\u5E94\u65E0\u6548");
|
|
21234
22399
|
if (body.node !== expectedNode)
|
|
21235
|
-
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
|
+
});
|
|
21236
22404
|
if (!Array.isArray(body.capabilities) || !body.capabilities.includes("site_channels"))
|
|
21237
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");
|
|
21238
22406
|
return { ...base, node: body.node };
|
|
21239
22407
|
} catch (e) {
|
|
21240
22408
|
if (e instanceof MhError)
|
|
21241
22409
|
throw e;
|
|
21242
|
-
|
|
21243
|
-
|
|
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 });
|
|
21244
22413
|
} finally {
|
|
21245
22414
|
clearTimeout(timer);
|
|
21246
22415
|
}
|
|
@@ -21561,6 +22730,7 @@ var siteHostingRoutes = [
|
|
|
21561
22730
|
// src/webui/server/edge-routes.ts
|
|
21562
22731
|
init_zod();
|
|
21563
22732
|
init_errors();
|
|
22733
|
+
init_page_messages();
|
|
21564
22734
|
|
|
21565
22735
|
// src/core/sync/edge-service.ts
|
|
21566
22736
|
init_errors();
|
|
@@ -21727,6 +22897,7 @@ init_errors();
|
|
|
21727
22897
|
init_http_status();
|
|
21728
22898
|
init_ids();
|
|
21729
22899
|
init_access_policy();
|
|
22900
|
+
init_page_messages();
|
|
21730
22901
|
init_grants_routes();
|
|
21731
22902
|
init_rate_limit();
|
|
21732
22903
|
init_sites_core();
|
|
@@ -22208,6 +23379,7 @@ function disconnectEdge(db) {
|
|
|
22208
23379
|
|
|
22209
23380
|
// src/core/sync/cf-oauth.ts
|
|
22210
23381
|
init_errors();
|
|
23382
|
+
init_page_messages();
|
|
22211
23383
|
init_e2ee();
|
|
22212
23384
|
var CF_OAUTH_CLIENT_ID = process.env.METAHUB_CF_OAUTH_CLIENT_ID || "0062224f5b80ba3a2074780e73a9d305";
|
|
22213
23385
|
var AUTH_ENDPOINT = "https://dash.cloudflare.com/oauth2/auth";
|
|
@@ -22257,7 +23429,7 @@ function buildTokenRequestBody(p) {
|
|
|
22257
23429
|
function parseTokenResponse(data) {
|
|
22258
23430
|
const d = data ?? {};
|
|
22259
23431
|
if (!d.access_token)
|
|
22260
|
-
throw
|
|
23432
|
+
throw mhError("auth", "Cloudflare \u6388\u6743\u5931\u8D25\uFF1A{detail}", { detail: d.error_description || d.error || "\u672A\u8FD4\u56DE access token" });
|
|
22261
23433
|
return {
|
|
22262
23434
|
accessToken: d.access_token,
|
|
22263
23435
|
refreshToken: d.refresh_token ?? null,
|
|
@@ -22277,11 +23449,11 @@ async function exchangeCode(p) {
|
|
|
22277
23449
|
body: buildTokenRequestBody(p)
|
|
22278
23450
|
});
|
|
22279
23451
|
} catch (e) {
|
|
22280
|
-
throw
|
|
23452
|
+
throw mhError("network", "\u65E0\u6CD5\u8FDE\u63A5 Cloudflare \u6388\u6743\u670D\u52A1\uFF1A{msg}", { msg: e.message });
|
|
22281
23453
|
}
|
|
22282
23454
|
const data = await res.json().catch(() => null);
|
|
22283
23455
|
if (!res.ok && !data?.access_token)
|
|
22284
|
-
throw
|
|
23456
|
+
throw mhError("auth", "Cloudflare \u4EE4\u724C\u4EA4\u6362\u5931\u8D25\uFF08HTTP {status}\uFF09", { status: res.status });
|
|
22285
23457
|
return parseTokenResponse(data);
|
|
22286
23458
|
}
|
|
22287
23459
|
async function discoverAccounts(accessToken) {
|
|
@@ -22291,11 +23463,11 @@ async function discoverAccounts(accessToken) {
|
|
|
22291
23463
|
headers: { authorization: `Bearer ${accessToken}`, accept: "application/json" }
|
|
22292
23464
|
});
|
|
22293
23465
|
} catch (e) {
|
|
22294
|
-
throw
|
|
23466
|
+
throw mhError("network", "\u65E0\u6CD5\u8BFB\u53D6 Cloudflare \u8D26\u53F7\u5217\u8868\uFF1A{msg}", { msg: e.message });
|
|
22295
23467
|
}
|
|
22296
23468
|
const data = await res.json().catch(() => null);
|
|
22297
23469
|
if (!res.ok)
|
|
22298
|
-
throw
|
|
23470
|
+
throw mhError("auth", "\u8BFB\u53D6 Cloudflare \u8D26\u53F7\u5931\u8D25\uFF08HTTP {status}\uFF09", { status: res.status });
|
|
22299
23471
|
return parseAccounts(data);
|
|
22300
23472
|
}
|
|
22301
23473
|
async function startCfLogin(opts = {}) {
|
|
@@ -22314,7 +23486,9 @@ async function startCfLogin(opts = {}) {
|
|
|
22314
23486
|
if (url.pathname !== CALLBACK_PATH)
|
|
22315
23487
|
return new Response("not found", { status: 404 });
|
|
22316
23488
|
settle(url.searchParams);
|
|
22317
|
-
return new Response(
|
|
23489
|
+
return new Response(callbackHtml(localeFromAcceptLanguage(req.headers.get("accept-language"))), {
|
|
23490
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
23491
|
+
});
|
|
22318
23492
|
}
|
|
22319
23493
|
});
|
|
22320
23494
|
const redirectUri = `http://127.0.0.1:${server.port}${CALLBACK_PATH}`;
|
|
@@ -22345,7 +23519,7 @@ async function startCfLogin(opts = {}) {
|
|
|
22345
23519
|
try {
|
|
22346
23520
|
const q = await Promise.race([caught, timeout]);
|
|
22347
23521
|
if (q.get("error"))
|
|
22348
|
-
throw
|
|
23522
|
+
throw mhError("auth", "Cloudflare \u6388\u6743\u88AB\u62D2\u7EDD\uFF1A{detail}", { detail: q.get("error_description") || q.get("error") || "" });
|
|
22349
23523
|
if (q.get("state") !== state)
|
|
22350
23524
|
throw new MhError("auth", "OAuth state \u4E0D\u5339\u914D\uFF0C\u7591\u4F3C\u4F2A\u9020\u56DE\u8C03\uFF0C\u5DF2\u4E2D\u6B62");
|
|
22351
23525
|
const code = q.get("code");
|
|
@@ -22366,9 +23540,9 @@ async function startCfLogin(opts = {}) {
|
|
|
22366
23540
|
}
|
|
22367
23541
|
};
|
|
22368
23542
|
}
|
|
22369
|
-
var
|
|
23543
|
+
var callbackHtml = (locale) => `<!doctype html><html lang="${locale}"><meta charset="utf-8"><title>MetaHub</title>
|
|
22370
23544
|
<body style="font:15px system-ui;display:grid;place-items:center;height:100vh;margin:0">
|
|
22371
|
-
<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>`;
|
|
22372
23546
|
|
|
22373
23547
|
// src/webui/server/edge-routes.ts
|
|
22374
23548
|
init_ids();
|
|
@@ -22439,7 +23613,7 @@ function resolveCfCreds(body) {
|
|
|
22439
23613
|
if (!flow)
|
|
22440
23614
|
throw new MhError("not_found", "OAuth \u6D41\u7A0B\u4E0D\u5B58\u5728\u6216\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55");
|
|
22441
23615
|
if (flow.state === "error")
|
|
22442
|
-
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);
|
|
22443
23617
|
if (flow.state !== "ready" || !flow.token)
|
|
22444
23618
|
throw new MhError("conflict", "Cloudflare \u6388\u6743\u5C1A\u672A\u5B8C\u6210\uFF0C\u8BF7\u7A0D\u5019");
|
|
22445
23619
|
apiToken = flow.token;
|
|
@@ -22483,6 +23657,7 @@ var edgeRoutes = [
|
|
|
22483
23657
|
}).catch((e) => {
|
|
22484
23658
|
flow.state = "error";
|
|
22485
23659
|
flow.error = e.message;
|
|
23660
|
+
flow.errorI18n = e instanceof MhError ? e.i18n : undefined;
|
|
22486
23661
|
});
|
|
22487
23662
|
return { flowId, authUrl: handle.authUrl };
|
|
22488
23663
|
})
|
|
@@ -22501,7 +23676,8 @@ var edgeRoutes = [
|
|
|
22501
23676
|
const flow = flows.get(flowId);
|
|
22502
23677
|
if (!flow)
|
|
22503
23678
|
throw new MhError("not_found", "OAuth \u6D41\u7A0B\u4E0D\u5B58\u5728\u6216\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u8BD5");
|
|
22504
|
-
|
|
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 };
|
|
22505
23681
|
})
|
|
22506
23682
|
},
|
|
22507
23683
|
{
|
|
@@ -22603,6 +23779,7 @@ var DatabaseSchema = object({
|
|
|
22603
23779
|
id: string2(),
|
|
22604
23780
|
name: string2(),
|
|
22605
23781
|
icon: string2().nullable(),
|
|
23782
|
+
meta: record(string2(), any()).nullable(),
|
|
22606
23783
|
created_hlc: string2()
|
|
22607
23784
|
});
|
|
22608
23785
|
var PropertySchema = object({
|
|
@@ -22646,11 +23823,18 @@ var SearchHitSchema = object({
|
|
|
22646
23823
|
title: string2().optional(),
|
|
22647
23824
|
snippet: string2()
|
|
22648
23825
|
});
|
|
23826
|
+
var LookupHitSchema = object({
|
|
23827
|
+
kind: _enum(["db", "doc", "rec"]),
|
|
23828
|
+
id: string2(),
|
|
23829
|
+
label: string2(),
|
|
23830
|
+
database_id: string2().nullable()
|
|
23831
|
+
});
|
|
22649
23832
|
var OkSchema2 = object({ ok: boolean2() });
|
|
22650
23833
|
var CreateDatabaseReq = object({ name: string2(), icon: string2().optional() });
|
|
22651
23834
|
var UpdateDatabaseReq = object({
|
|
22652
23835
|
name: string2().optional(),
|
|
22653
|
-
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")
|
|
22654
23838
|
});
|
|
22655
23839
|
var DuplicateDatabaseReq = object({
|
|
22656
23840
|
name: string2().optional().describe("Name for the copy (defaults to the source name)"),
|
|
@@ -22909,7 +24093,7 @@ function handle3(fn) {
|
|
|
22909
24093
|
return out;
|
|
22910
24094
|
return Response.json(out ?? null);
|
|
22911
24095
|
} catch (e) {
|
|
22912
|
-
return errorResponse(e);
|
|
24096
|
+
return errorResponse(e, req);
|
|
22913
24097
|
}
|
|
22914
24098
|
};
|
|
22915
24099
|
}
|
|
@@ -22972,7 +24156,7 @@ var webuiRoutes = [
|
|
|
22972
24156
|
{
|
|
22973
24157
|
method: "PATCH",
|
|
22974
24158
|
path: "/api/database",
|
|
22975
|
-
summary: "Rename a database or
|
|
24159
|
+
summary: "Rename a database, change its icon or patch its metadata. Query: ?id=<id>",
|
|
22976
24160
|
request: UpdateDatabaseReq,
|
|
22977
24161
|
response: DatabaseSchema,
|
|
22978
24162
|
handler: handle3(async (req, { db }) => {
|
|
@@ -23358,6 +24542,16 @@ var webuiRoutes = [
|
|
|
23358
24542
|
return search(db, need3(req, "q"), { limit: limit ? Number(limit) : undefined });
|
|
23359
24543
|
})
|
|
23360
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
|
+
},
|
|
23361
24555
|
{
|
|
23362
24556
|
method: "GET",
|
|
23363
24557
|
path: "/api/blob-cache",
|
|
@@ -23382,6 +24576,13 @@ var webuiRoutes = [
|
|
|
23382
24576
|
response: ClearResultSchema,
|
|
23383
24577
|
handler: handle3((_req, { db }) => clearCache(db))
|
|
23384
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
|
+
},
|
|
23385
24586
|
{
|
|
23386
24587
|
method: "POST",
|
|
23387
24588
|
path: "/api/blob-cache/pin",
|
|
@@ -23455,6 +24656,7 @@ export {
|
|
|
23455
24656
|
DOCUMENT_COLS,
|
|
23456
24657
|
DOMAIN,
|
|
23457
24658
|
HEALTH_PATH,
|
|
24659
|
+
HLC_MAX_SKEW_MS,
|
|
23458
24660
|
HealthResponseSchema,
|
|
23459
24661
|
INLINE_LIMIT,
|
|
23460
24662
|
MAX_HLC_COUNTER,
|
|
@@ -23480,6 +24682,7 @@ export {
|
|
|
23480
24682
|
advanceAckedPrefix,
|
|
23481
24683
|
appendDocument,
|
|
23482
24684
|
applyChange,
|
|
24685
|
+
asMhError,
|
|
23483
24686
|
backfillDocumentOrderKeys,
|
|
23484
24687
|
backfillRecordOrderKeys,
|
|
23485
24688
|
base64ToBytes,
|
|
@@ -23540,12 +24743,16 @@ export {
|
|
|
23540
24743
|
getSiteByName,
|
|
23541
24744
|
grouped,
|
|
23542
24745
|
hasIndex,
|
|
24746
|
+
hlcSkewBound,
|
|
23543
24747
|
idKind,
|
|
23544
24748
|
inferContentType,
|
|
23545
24749
|
ingest,
|
|
24750
|
+
ingestDetailed,
|
|
23546
24751
|
initSchema,
|
|
24752
|
+
interpolateMsg,
|
|
23547
24753
|
isImageType,
|
|
23548
24754
|
isSitePublic,
|
|
24755
|
+
isSkewedHlc,
|
|
23549
24756
|
isTextType,
|
|
23550
24757
|
keyBetween,
|
|
23551
24758
|
keysBetween,
|
|
@@ -23557,10 +24764,12 @@ export {
|
|
|
23557
24764
|
listRecords,
|
|
23558
24765
|
listSites,
|
|
23559
24766
|
liveFileRowIfSame,
|
|
24767
|
+
lookupCandidates,
|
|
23560
24768
|
loopbackUiRejection,
|
|
23561
24769
|
makeId,
|
|
23562
24770
|
maybeAutoIndex,
|
|
23563
24771
|
metahubHome,
|
|
24772
|
+
mhError,
|
|
23564
24773
|
migrateDocBlocks,
|
|
23565
24774
|
migrateDocuments,
|
|
23566
24775
|
migrateNodes,
|
|
@@ -23579,6 +24788,7 @@ export {
|
|
|
23579
24788
|
openMetahub,
|
|
23580
24789
|
parseDocBlocks,
|
|
23581
24790
|
parseHlc,
|
|
24791
|
+
persistedHlc,
|
|
23582
24792
|
platformFromProcess,
|
|
23583
24793
|
prepareRecordCells,
|
|
23584
24794
|
prependDocument,
|
|
@@ -23595,6 +24805,7 @@ export {
|
|
|
23595
24805
|
removeProperty,
|
|
23596
24806
|
removeSelectOption,
|
|
23597
24807
|
renameSelectOption,
|
|
24808
|
+
resetHlc,
|
|
23598
24809
|
resolveCandidates,
|
|
23599
24810
|
resolveData,
|
|
23600
24811
|
resolveEntity,
|
|
@@ -23614,6 +24825,7 @@ export {
|
|
|
23614
24825
|
sha256Hex,
|
|
23615
24826
|
siteCacheControl,
|
|
23616
24827
|
siteLifecycle,
|
|
24828
|
+
skewWarnings,
|
|
23617
24829
|
slugify,
|
|
23618
24830
|
startServer,
|
|
23619
24831
|
syncWithPeer,
|
|
@@ -23631,7 +24843,8 @@ export {
|
|
|
23631
24843
|
withChangeGroup,
|
|
23632
24844
|
withNodeId,
|
|
23633
24845
|
withTxnId,
|
|
23634
|
-
writeFileRow
|
|
24846
|
+
writeFileRow,
|
|
24847
|
+
writeTx
|
|
23635
24848
|
};
|
|
23636
24849
|
|
|
23637
|
-
//# debugId=
|
|
24850
|
+
//# debugId=23C620FE660E43F464756E2164756E21
|