@tensorix/metahub 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +513 -133
- package/dist/cli.js.map +20 -19
- package/dist/core/audit.d.ts.map +1 -1
- package/dist/core/blobs-core.d.ts.map +1 -1
- package/dist/core/crdt.d.ts.map +1 -1
- package/dist/core/db.d.ts +1 -1
- package/dist/core/db.d.ts.map +1 -1
- package/dist/core/integrity.d.ts +5 -0
- package/dist/core/integrity.d.ts.map +1 -1
- package/dist/core/node.d.ts +52 -6
- package/dist/core/node.d.ts.map +1 -1
- package/dist/core/schema-init.d.ts +3 -0
- package/dist/core/schema-init.d.ts.map +1 -1
- package/dist/core/schema.d.ts +1 -1
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/snapshot.d.ts.map +1 -1
- package/dist/core/sync/devices.d.ts +6 -0
- package/dist/core/sync/devices.d.ts.map +1 -1
- package/dist/core/sync/peers-routes.d.ts.map +1 -1
- package/dist/core/sync/peers.d.ts +2 -0
- package/dist/core/sync/peers.d.ts.map +1 -1
- package/dist/core/sync/server.d.ts.map +1 -1
- package/dist/core/sync/storage.d.ts +4 -0
- package/dist/core/sync/storage.d.ts.map +1 -1
- package/dist/core/tables.d.ts +16 -0
- package/dist/core/tables.d.ts.map +1 -0
- package/dist/db-worker.js +72 -56
- package/dist/edge-worker.js +35 -7
- package/dist/index.js +468 -101
- package/dist/index.js.map +17 -16
- package/dist/metahub-sdk.js +5 -5
- package/dist/sw.js +5 -5
- package/dist/webui/server/routes.d.ts.map +1 -1
- package/dist/webui.js +124 -123
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -801,7 +801,7 @@ var package_default;
|
|
|
801
801
|
var init_package = __esm(() => {
|
|
802
802
|
package_default = {
|
|
803
803
|
name: "@tensorix/metahub",
|
|
804
|
-
version: "0.5.
|
|
804
|
+
version: "0.5.2",
|
|
805
805
|
description: "Metahub core library and CLI",
|
|
806
806
|
license: "AGPL-3.0-only",
|
|
807
807
|
type: "module",
|
|
@@ -922,38 +922,6 @@ var init_ids = __esm(() => {
|
|
|
922
922
|
]);
|
|
923
923
|
});
|
|
924
924
|
|
|
925
|
-
// src/core/node.ts
|
|
926
|
-
function getNodeId(db) {
|
|
927
|
-
const row = db.query("SELECT value FROM meta WHERE key = 'node_id'").get();
|
|
928
|
-
if (row)
|
|
929
|
-
return row.value;
|
|
930
|
-
const id = randomSuffix(8);
|
|
931
|
-
db.query("INSERT INTO meta (key, value) VALUES ('node_id', ?)").run(id);
|
|
932
|
-
return id;
|
|
933
|
-
}
|
|
934
|
-
function getNodeLabel(db) {
|
|
935
|
-
const row = db.query("SELECT value FROM meta WHERE key = 'node_label'").get();
|
|
936
|
-
return row?.value ?? null;
|
|
937
|
-
}
|
|
938
|
-
function setNodeLabel(db, label) {
|
|
939
|
-
if (label == null || label.trim() === "") {
|
|
940
|
-
db.query("DELETE FROM meta WHERE key = 'node_label'").run();
|
|
941
|
-
return;
|
|
942
|
-
}
|
|
943
|
-
db.query("INSERT INTO meta (key, value) VALUES ('node_label', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(label.trim());
|
|
944
|
-
}
|
|
945
|
-
function displayNodes(db) {
|
|
946
|
-
const self = getNodeId(db);
|
|
947
|
-
const peers = db.query("SELECT node_id, label FROM peers WHERE node_id IS NOT NULL AND node_id <> '' GROUP BY node_id").all();
|
|
948
|
-
return [
|
|
949
|
-
{ node_id: self, label: getNodeLabel(db), self: true },
|
|
950
|
-
...peers.filter((p) => p.node_id !== self).map((p) => ({ ...p, self: false }))
|
|
951
|
-
];
|
|
952
|
-
}
|
|
953
|
-
var init_node = __esm(() => {
|
|
954
|
-
init_ids();
|
|
955
|
-
});
|
|
956
|
-
|
|
957
925
|
// src/core/hlc.ts
|
|
958
926
|
function carryCounter(millis, counter) {
|
|
959
927
|
if (counter <= MAX_HLC_COUNTER)
|
|
@@ -1380,6 +1348,10 @@ var init_crdt = __esm(() => {
|
|
|
1380
1348
|
blob_policy: {
|
|
1381
1349
|
table: "blob_policy",
|
|
1382
1350
|
cols: new Set(["full_nodes", "redundancy", "__deleted"])
|
|
1351
|
+
},
|
|
1352
|
+
nodes: {
|
|
1353
|
+
table: "nodes",
|
|
1354
|
+
cols: new Set(["label", "platform", "form", "app", "first_seen", "__deleted"])
|
|
1383
1355
|
}
|
|
1384
1356
|
};
|
|
1385
1357
|
RECORD_META = new Set(["database_id", "created_hlc", "order_key", "__deleted"]);
|
|
@@ -1397,10 +1369,149 @@ var init_crdt = __esm(() => {
|
|
|
1397
1369
|
"sites:spa",
|
|
1398
1370
|
"site_channels:__deleted",
|
|
1399
1371
|
"site_files:__deleted",
|
|
1400
|
-
"blob_policy:__deleted"
|
|
1372
|
+
"blob_policy:__deleted",
|
|
1373
|
+
"nodes:__deleted"
|
|
1401
1374
|
]);
|
|
1402
1375
|
});
|
|
1403
1376
|
|
|
1377
|
+
// src/core/node.ts
|
|
1378
|
+
function getNodeId(db) {
|
|
1379
|
+
const row = db.query("SELECT value FROM meta WHERE key = 'node_id'").get();
|
|
1380
|
+
if (row)
|
|
1381
|
+
return row.value;
|
|
1382
|
+
const id = randomSuffix(8);
|
|
1383
|
+
db.query("INSERT INTO meta (key, value) VALUES ('node_id', ?)").run(id);
|
|
1384
|
+
return id;
|
|
1385
|
+
}
|
|
1386
|
+
function readNodeMeta(db, nodeId) {
|
|
1387
|
+
const r = db.query(`SELECT ${NODE_COLS} FROM nodes WHERE id = ? AND __deleted = 0`).get(nodeId);
|
|
1388
|
+
return r ? rowToMeta(r) : null;
|
|
1389
|
+
}
|
|
1390
|
+
function listNodeMeta(db) {
|
|
1391
|
+
return db.query(`SELECT ${NODE_COLS} FROM nodes WHERE __deleted = 0`).all().map(rowToMeta);
|
|
1392
|
+
}
|
|
1393
|
+
function legacySelfLabel(db) {
|
|
1394
|
+
const row = db.query("SELECT value FROM meta WHERE key = 'node_label'").get();
|
|
1395
|
+
return row?.value ?? null;
|
|
1396
|
+
}
|
|
1397
|
+
function mirrorLegacySelfLabel(db, label) {
|
|
1398
|
+
if (label == null) {
|
|
1399
|
+
db.query("DELETE FROM meta WHERE key = 'node_label'").run();
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
db.query("INSERT INTO meta (key, value) VALUES ('node_label', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(label);
|
|
1403
|
+
}
|
|
1404
|
+
function nodeLabelOf(db, nodeId) {
|
|
1405
|
+
const synced = readNodeMeta(db, nodeId)?.label;
|
|
1406
|
+
if (synced)
|
|
1407
|
+
return synced;
|
|
1408
|
+
const peer = db.query("SELECT label FROM peers WHERE node_id = ? AND label IS NOT NULL AND label <> '' LIMIT 1").get(nodeId);
|
|
1409
|
+
if (peer?.label)
|
|
1410
|
+
return peer.label;
|
|
1411
|
+
return nodeId === getNodeId(db) ? legacySelfLabel(db) : null;
|
|
1412
|
+
}
|
|
1413
|
+
function getNodeLabel(db) {
|
|
1414
|
+
return nodeLabelOf(db, getNodeId(db));
|
|
1415
|
+
}
|
|
1416
|
+
function setNodeLabel(db, label, nodeId) {
|
|
1417
|
+
const id = nodeId ?? getNodeId(db);
|
|
1418
|
+
const next = label == null || label.trim() === "" ? null : label.trim();
|
|
1419
|
+
const cur = readNodeMeta(db, id);
|
|
1420
|
+
if (id === getNodeId(db))
|
|
1421
|
+
mirrorLegacySelfLabel(db, next);
|
|
1422
|
+
if (cur && cur.label === next)
|
|
1423
|
+
return;
|
|
1424
|
+
if (!cur && next == null)
|
|
1425
|
+
return;
|
|
1426
|
+
withChangeGroup("node", () => {
|
|
1427
|
+
emit(db, "nodes", id, "label", next);
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
function describeSelf(db, info = {}) {
|
|
1431
|
+
const id = getNodeId(db);
|
|
1432
|
+
const cur = readNodeMeta(db, id);
|
|
1433
|
+
const fields = {};
|
|
1434
|
+
if (info.platform !== undefined && (cur?.platform ?? null) !== info.platform)
|
|
1435
|
+
fields.platform = info.platform;
|
|
1436
|
+
if (info.form !== undefined && (cur?.form ?? null) !== info.form)
|
|
1437
|
+
fields.form = info.form;
|
|
1438
|
+
if (info.app !== undefined && (cur?.app ?? null) !== info.app)
|
|
1439
|
+
fields.app = info.app;
|
|
1440
|
+
if (cur?.first_seen == null)
|
|
1441
|
+
fields.first_seen = info.now ?? Date.now();
|
|
1442
|
+
if (!cur?.label) {
|
|
1443
|
+
const seed = legacySelfLabel(db) ?? info.defaultLabel ?? null;
|
|
1444
|
+
if (seed)
|
|
1445
|
+
fields.label = seed;
|
|
1446
|
+
}
|
|
1447
|
+
if (Object.keys(fields).length > 0) {
|
|
1448
|
+
withChangeGroup("node", () => {
|
|
1449
|
+
for (const [col, v] of Object.entries(fields))
|
|
1450
|
+
emit(db, "nodes", id, col, v);
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
return readNodeMeta(db, id);
|
|
1454
|
+
}
|
|
1455
|
+
function platformFromProcess(p) {
|
|
1456
|
+
switch (p) {
|
|
1457
|
+
case "darwin":
|
|
1458
|
+
return "macos";
|
|
1459
|
+
case "win32":
|
|
1460
|
+
return "windows";
|
|
1461
|
+
case "linux":
|
|
1462
|
+
return "linux";
|
|
1463
|
+
case "android":
|
|
1464
|
+
return "android";
|
|
1465
|
+
default:
|
|
1466
|
+
return null;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
function defaultForm(platform, app) {
|
|
1470
|
+
if (app === "web")
|
|
1471
|
+
return "browser";
|
|
1472
|
+
if (platform === "ios" || platform === "android")
|
|
1473
|
+
return "phone";
|
|
1474
|
+
if (platform === "linux")
|
|
1475
|
+
return "server";
|
|
1476
|
+
if (platform === "macos" || platform === "windows")
|
|
1477
|
+
return "laptop";
|
|
1478
|
+
return null;
|
|
1479
|
+
}
|
|
1480
|
+
function displayNodes(db) {
|
|
1481
|
+
const self = getNodeId(db);
|
|
1482
|
+
const ids = new Set([self]);
|
|
1483
|
+
for (const r of listNodeMeta(db))
|
|
1484
|
+
ids.add(r.node_id);
|
|
1485
|
+
const peers = db.query("SELECT node_id FROM peers WHERE node_id IS NOT NULL AND node_id <> '' GROUP BY node_id").all();
|
|
1486
|
+
for (const p of peers)
|
|
1487
|
+
ids.add(p.node_id);
|
|
1488
|
+
const out = [...ids].map((node_id) => {
|
|
1489
|
+
const m = readNodeMeta(db, node_id);
|
|
1490
|
+
return {
|
|
1491
|
+
node_id,
|
|
1492
|
+
label: nodeLabelOf(db, node_id),
|
|
1493
|
+
self: node_id === self,
|
|
1494
|
+
platform: m?.platform ?? null,
|
|
1495
|
+
form: m?.form ?? null,
|
|
1496
|
+
app: m?.app ?? null
|
|
1497
|
+
};
|
|
1498
|
+
});
|
|
1499
|
+
out.sort((a, b) => Number(b.self) - Number(a.self));
|
|
1500
|
+
return out;
|
|
1501
|
+
}
|
|
1502
|
+
var NODE_COLS = "id, label, platform, form, app, first_seen", rowToMeta = (r) => ({
|
|
1503
|
+
node_id: r.id,
|
|
1504
|
+
label: r.label,
|
|
1505
|
+
platform: r.platform ?? null,
|
|
1506
|
+
form: r.form ?? null,
|
|
1507
|
+
app: r.app ?? null,
|
|
1508
|
+
first_seen: r.first_seen
|
|
1509
|
+
});
|
|
1510
|
+
var init_node = __esm(() => {
|
|
1511
|
+
init_ids();
|
|
1512
|
+
init_crdt();
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1404
1515
|
// src/core/sync/publisher-lease.ts
|
|
1405
1516
|
async function isElectedPublisher(client, base, node, priority, now = Date.now()) {
|
|
1406
1517
|
const beatId = `${base}\x00${node}`;
|
|
@@ -9510,6 +9621,22 @@ CREATE TABLE IF NOT EXISTS blob_policy (
|
|
|
9510
9621
|
redundancy TEXT, -- 'all' | 'any'
|
|
9511
9622
|
__deleted INTEGER NOT NULL DEFAULT 0
|
|
9512
9623
|
);
|
|
9624
|
+
|
|
9625
|
+
-- Synced device roster (system tier, see tables.ts): one row per node id, written
|
|
9626
|
+
-- by the device itself on open (describeSelf) and by anyone who renames it.
|
|
9627
|
+
-- Replicates so every device shows the same name and icon for a peer \u2014 the only
|
|
9628
|
+
-- record a purely bucket-joined device leaves besides its oplog stream. Each
|
|
9629
|
+
-- column is its own LWW register, so a rename and a platform refresh never
|
|
9630
|
+
-- clobber each other. meta.node_label survives as a node-local read fallback.
|
|
9631
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
9632
|
+
id TEXT PRIMARY KEY, -- node_id
|
|
9633
|
+
label TEXT, -- shared display name; any device may set it
|
|
9634
|
+
platform TEXT, -- macos | windows | linux | ios | android | web
|
|
9635
|
+
form TEXT, -- laptop | desktop | phone | server | browser
|
|
9636
|
+
app TEXT, -- cli | server | desktop | web
|
|
9637
|
+
first_seen INTEGER, -- epoch ms of the device's first self-description
|
|
9638
|
+
__deleted INTEGER NOT NULL DEFAULT 0
|
|
9639
|
+
);
|
|
9513
9640
|
`, FTS_SCHEMA = `
|
|
9514
9641
|
CREATE VIRTUAL TABLE IF NOT EXISTS search_fts USING fts5(
|
|
9515
9642
|
kind UNINDEXED,
|
|
@@ -10009,17 +10136,14 @@ function cacheStats(db) {
|
|
|
10009
10136
|
}
|
|
10010
10137
|
function knownNodes(db) {
|
|
10011
10138
|
const self = getNodeId(db);
|
|
10012
|
-
const
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
for (const p of peers)
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
if (!byId.has(g.node_id))
|
|
10021
|
-
byId.set(g.node_id, null);
|
|
10022
|
-
return [...byId.entries()].map(([nodeId, label]) => ({ nodeId, label, self: nodeId === self }));
|
|
10139
|
+
const ids = new Set([self]);
|
|
10140
|
+
for (const r of db.query("SELECT id FROM nodes WHERE __deleted = 0").all())
|
|
10141
|
+
ids.add(r.id);
|
|
10142
|
+
for (const p of db.query("SELECT DISTINCT node_id FROM peers WHERE node_id IS NOT NULL").all())
|
|
10143
|
+
ids.add(p.node_id);
|
|
10144
|
+
for (const g of db.query("SELECT DISTINCT node_id FROM peer_grants WHERE node_id IS NOT NULL").all())
|
|
10145
|
+
ids.add(g.node_id);
|
|
10146
|
+
return [...ids].map((nodeId) => ({ nodeId, label: nodeLabelOf(db, nodeId), self: nodeId === self }));
|
|
10023
10147
|
}
|
|
10024
10148
|
var POLICY_ID = "default", blobBytesResolver = null, setFullNodes = (db, nodeIds) => {
|
|
10025
10149
|
withChangeGroup(null, () => {
|
|
@@ -10253,21 +10377,28 @@ function migrateSitesAccess(db) {
|
|
|
10253
10377
|
backfill("public_grants", "NULL");
|
|
10254
10378
|
}
|
|
10255
10379
|
}
|
|
10256
|
-
function
|
|
10257
|
-
if (!tableExists3(db,
|
|
10380
|
+
function replayDataset(db, dataset) {
|
|
10381
|
+
if (!tableExists3(db, dataset))
|
|
10258
10382
|
return;
|
|
10259
|
-
const
|
|
10383
|
+
const key = `${dataset}_replay_seq`;
|
|
10384
|
+
const wm = db.query("SELECT value FROM meta WHERE key = ?").get(key);
|
|
10260
10385
|
const since = wm ? Number(wm.value) : 0;
|
|
10261
10386
|
const changes = db.query(`SELECT seq, ${CHANGE_SELECT} FROM crdt_changes
|
|
10262
|
-
WHERE dataset =
|
|
10387
|
+
WHERE dataset = ? AND seq > ? ORDER BY seq`).all(dataset, since);
|
|
10263
10388
|
if (changes.length === 0)
|
|
10264
10389
|
return;
|
|
10265
10390
|
db.transaction((rows) => {
|
|
10266
10391
|
for (const change of rows)
|
|
10267
10392
|
applyChange(db, change);
|
|
10268
|
-
db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(
|
|
10393
|
+
db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, String(rows[rows.length - 1].seq));
|
|
10269
10394
|
})(changes);
|
|
10270
10395
|
}
|
|
10396
|
+
function migrateSiteChannels(db) {
|
|
10397
|
+
replayDataset(db, "site_channels");
|
|
10398
|
+
}
|
|
10399
|
+
function migrateNodes(db) {
|
|
10400
|
+
replayDataset(db, "nodes");
|
|
10401
|
+
}
|
|
10271
10402
|
function initSchema(db) {
|
|
10272
10403
|
runSchema(db);
|
|
10273
10404
|
migrateOplog(db);
|
|
@@ -10282,8 +10413,8 @@ function initSchema(db) {
|
|
|
10282
10413
|
migrateShares(db);
|
|
10283
10414
|
migrateSitesAccess(db);
|
|
10284
10415
|
migrateSiteChannels(db);
|
|
10416
|
+
migrateNodes(db);
|
|
10285
10417
|
}
|
|
10286
|
-
var SITE_CHANNELS_REPLAY_KEY = "site_channels_replay_seq";
|
|
10287
10418
|
var init_schema_init = __esm(() => {
|
|
10288
10419
|
init_records();
|
|
10289
10420
|
init_documents();
|
|
@@ -11802,6 +11933,8 @@ async function addAndSyncStoragePeer(db, spec) {
|
|
|
11802
11933
|
publish: spec.publish,
|
|
11803
11934
|
priority: spec.priority
|
|
11804
11935
|
};
|
|
11936
|
+
if (spec.provider)
|
|
11937
|
+
config.provider = spec.provider;
|
|
11805
11938
|
if (encrypt) {
|
|
11806
11939
|
if (spec.recoveryCode) {
|
|
11807
11940
|
const rawKey = await decodeRecoveryCode(spec.recoveryCode);
|
|
@@ -13297,10 +13430,14 @@ function listDevices(db) {
|
|
|
13297
13430
|
const ensure = (nodeId) => {
|
|
13298
13431
|
let v = byNode.get(nodeId);
|
|
13299
13432
|
if (!v) {
|
|
13433
|
+
const meta = readNodeMeta(db, nodeId);
|
|
13300
13434
|
v = {
|
|
13301
13435
|
nodeId,
|
|
13302
|
-
label:
|
|
13436
|
+
label: nodeLabelOf(db, nodeId),
|
|
13303
13437
|
self: nodeId === self,
|
|
13438
|
+
platform: meta?.platform ?? null,
|
|
13439
|
+
form: meta?.form ?? null,
|
|
13440
|
+
app: meta?.app ?? null,
|
|
13304
13441
|
channels: [],
|
|
13305
13442
|
lastActivityAt: null,
|
|
13306
13443
|
revocable: "none",
|
|
@@ -13311,7 +13448,9 @@ function listDevices(db) {
|
|
|
13311
13448
|
}
|
|
13312
13449
|
return v;
|
|
13313
13450
|
};
|
|
13314
|
-
ensure(self)
|
|
13451
|
+
ensure(self);
|
|
13452
|
+
for (const r of db.query("SELECT id FROM nodes WHERE __deleted = 0").all())
|
|
13453
|
+
ensure(r.id);
|
|
13315
13454
|
const oplog = db.query("SELECT node_id, MAX(hlc) AS h FROM crdt_changes GROUP BY node_id").all();
|
|
13316
13455
|
for (const r of oplog)
|
|
13317
13456
|
ensure(r.node_id).channels.push({ kind: "oplog", ref: "", lastSeenAt: parseHlc(r.h).millis });
|
|
@@ -13320,8 +13459,6 @@ function listDevices(db) {
|
|
|
13320
13459
|
if (!p.node_id)
|
|
13321
13460
|
continue;
|
|
13322
13461
|
const v = ensure(p.node_id);
|
|
13323
|
-
if (!v.label)
|
|
13324
|
-
v.label = p.label;
|
|
13325
13462
|
v.channels.push({
|
|
13326
13463
|
kind: "paired_out",
|
|
13327
13464
|
ref: p.url,
|
|
@@ -13339,6 +13476,9 @@ function listDevices(db) {
|
|
|
13339
13476
|
nodeId: null,
|
|
13340
13477
|
label: null,
|
|
13341
13478
|
self: false,
|
|
13479
|
+
platform: null,
|
|
13480
|
+
form: null,
|
|
13481
|
+
app: null,
|
|
13342
13482
|
channels: [ch],
|
|
13343
13483
|
lastActivityAt: g.created_at,
|
|
13344
13484
|
revocable: "yes",
|
|
@@ -14265,7 +14405,8 @@ function s3PeerViews(db) {
|
|
|
14265
14405
|
prefix: c.prefix ?? null,
|
|
14266
14406
|
accessKeyId: c.accessKeyId ?? null,
|
|
14267
14407
|
encrypt: c.encrypt !== false,
|
|
14268
|
-
virtualHostedStyle: c.virtualHostedStyle ?? null
|
|
14408
|
+
virtualHostedStyle: c.virtualHostedStyle ?? null,
|
|
14409
|
+
provider: c.provider ?? null
|
|
14269
14410
|
};
|
|
14270
14411
|
});
|
|
14271
14412
|
}
|
|
@@ -14342,7 +14483,8 @@ var init_peers_routes = __esm(() => {
|
|
|
14342
14483
|
prefix: string2().optional(),
|
|
14343
14484
|
encrypt: boolean2().optional(),
|
|
14344
14485
|
passphrase: string2().optional(),
|
|
14345
|
-
corsOrigins: array(string2()).optional()
|
|
14486
|
+
corsOrigins: array(string2()).optional(),
|
|
14487
|
+
provider: string2().optional()
|
|
14346
14488
|
});
|
|
14347
14489
|
S3PeerSchema = object({
|
|
14348
14490
|
url: string2(),
|
|
@@ -14359,7 +14501,8 @@ var init_peers_routes = __esm(() => {
|
|
|
14359
14501
|
prefix: string2().nullable(),
|
|
14360
14502
|
accessKeyId: string2().nullable(),
|
|
14361
14503
|
encrypt: boolean2(),
|
|
14362
|
-
virtualHostedStyle: boolean2().nullable()
|
|
14504
|
+
virtualHostedStyle: boolean2().nullable(),
|
|
14505
|
+
provider: string2().nullable()
|
|
14363
14506
|
});
|
|
14364
14507
|
DataPlaceSchema = object({
|
|
14365
14508
|
kind: _enum(["self", "device", "bucket"]),
|
|
@@ -17706,20 +17849,42 @@ init_blobs();
|
|
|
17706
17849
|
// src/core/db.ts
|
|
17707
17850
|
init_paths();
|
|
17708
17851
|
init_schema_init();
|
|
17852
|
+
init_node();
|
|
17709
17853
|
init_schema_init();
|
|
17710
17854
|
import { Database } from "bun:sqlite";
|
|
17711
17855
|
import { mkdirSync } from "fs";
|
|
17856
|
+
import { hostname } from "os";
|
|
17712
17857
|
function ensureDirs() {
|
|
17713
17858
|
mkdirSync(metahubHome(), { recursive: true });
|
|
17714
17859
|
mkdirSync(cacheDir(), { recursive: true });
|
|
17715
17860
|
}
|
|
17861
|
+
function appKind() {
|
|
17862
|
+
const v = process.env.METAHUB_APP;
|
|
17863
|
+
return v === "server" || v === "desktop" || v === "web" ? v : "cli";
|
|
17864
|
+
}
|
|
17716
17865
|
function openMetahub() {
|
|
17717
17866
|
ensureDirs();
|
|
17718
17867
|
const db = new Database(dbPath(), { create: true });
|
|
17719
17868
|
db.exec("PRAGMA journal_mode = WAL;");
|
|
17720
17869
|
initSchema(db);
|
|
17870
|
+
const app = appKind();
|
|
17871
|
+
const platform = platformFromProcess(process.platform);
|
|
17872
|
+
describeSelf(db, {
|
|
17873
|
+
app,
|
|
17874
|
+
platform,
|
|
17875
|
+
form: defaultForm(platform, app),
|
|
17876
|
+
defaultLabel: safeHostname()
|
|
17877
|
+
});
|
|
17721
17878
|
return db;
|
|
17722
17879
|
}
|
|
17880
|
+
function safeHostname() {
|
|
17881
|
+
try {
|
|
17882
|
+
const h = hostname();
|
|
17883
|
+
return h ? h.replace(/\.local$/i, "") : null;
|
|
17884
|
+
} catch {
|
|
17885
|
+
return null;
|
|
17886
|
+
}
|
|
17887
|
+
}
|
|
17723
17888
|
|
|
17724
17889
|
// src/cli/commands/init.ts
|
|
17725
17890
|
init_node();
|
|
@@ -22302,6 +22467,24 @@ import { join as join8 } from "path";
|
|
|
22302
22467
|
// src/core/integrity.ts
|
|
22303
22468
|
init_crdt();
|
|
22304
22469
|
init_properties();
|
|
22470
|
+
function rematerializeDataset(db, dataset) {
|
|
22471
|
+
const d = DOMAIN[dataset];
|
|
22472
|
+
if (!d)
|
|
22473
|
+
throw new Error(`not a replicated dataset: ${dataset}`);
|
|
22474
|
+
const changes = db.query(`SELECT ${CHANGE_SELECT} FROM crdt_changes WHERE dataset = ? ORDER BY seq`).all(dataset);
|
|
22475
|
+
db.transaction((rows) => {
|
|
22476
|
+
db.query(`DELETE FROM ${d.table}`).run();
|
|
22477
|
+
for (const c of rows)
|
|
22478
|
+
applyChange(db, c);
|
|
22479
|
+
})(changes);
|
|
22480
|
+
return changes.length;
|
|
22481
|
+
}
|
|
22482
|
+
function rematerializeAll(db) {
|
|
22483
|
+
const out = {};
|
|
22484
|
+
for (const ds of Object.keys(DOMAIN))
|
|
22485
|
+
out[ds] = rematerializeDataset(db, ds);
|
|
22486
|
+
return out;
|
|
22487
|
+
}
|
|
22305
22488
|
var REF_RULES = [
|
|
22306
22489
|
{ srcTable: "properties", fk: "database_id", tgtTable: "databases", fix: "tombstone-src" },
|
|
22307
22490
|
{ srcTable: "records", fk: "database_id", tgtTable: "databases", fix: "tombstone-src" },
|
|
@@ -22601,6 +22784,39 @@ var repairHub = grouped(function repairHub(db) {
|
|
|
22601
22784
|
init_paths();
|
|
22602
22785
|
init_cache();
|
|
22603
22786
|
init_errors();
|
|
22787
|
+
|
|
22788
|
+
// src/core/tables.ts
|
|
22789
|
+
var TABLES = {
|
|
22790
|
+
databases: { tier: "content", synced: true },
|
|
22791
|
+
properties: { tier: "content", synced: true },
|
|
22792
|
+
records: { tier: "content", synced: true },
|
|
22793
|
+
documents: { tier: "content", synced: true },
|
|
22794
|
+
doc_blocks: { tier: "content", synced: true },
|
|
22795
|
+
sites: { tier: "content", synced: true },
|
|
22796
|
+
site_files: { tier: "content", synced: true },
|
|
22797
|
+
site_channels: { tier: "system", synced: true },
|
|
22798
|
+
blob_policy: { tier: "system", synced: true },
|
|
22799
|
+
nodes: { tier: "system", synced: true },
|
|
22800
|
+
meta: { tier: "local", synced: false },
|
|
22801
|
+
crdt_changes: { tier: "local", synced: false },
|
|
22802
|
+
peers: { tier: "local", synced: false },
|
|
22803
|
+
storage_cursors: { tier: "local", synced: false },
|
|
22804
|
+
room_rows: { tier: "local", synced: false },
|
|
22805
|
+
peer_grants: { tier: "local", synced: false },
|
|
22806
|
+
pairing_codes: { tier: "local", synced: false },
|
|
22807
|
+
shares: { tier: "local", synced: false },
|
|
22808
|
+
site_channel_observations: { tier: "local", synced: false },
|
|
22809
|
+
blob_cache: { tier: "local", synced: false },
|
|
22810
|
+
drop_rejects: { tier: "local", synced: false },
|
|
22811
|
+
search_fts: { tier: "local", synced: false }
|
|
22812
|
+
};
|
|
22813
|
+
var names = (pred) => Object.entries(TABLES).filter(([, s]) => pred(s)).map(([n]) => n);
|
|
22814
|
+
var SYNCED_TABLES = names((s) => s.synced);
|
|
22815
|
+
var CONTENT_TABLES = names((s) => s.tier === "content");
|
|
22816
|
+
var SYSTEM_DATASETS = names((s) => s.tier === "system");
|
|
22817
|
+
var LOCAL_TABLES = names((s) => s.tier === "local");
|
|
22818
|
+
|
|
22819
|
+
// src/core/snapshot.ts
|
|
22604
22820
|
var SNAPSHOT_FORMAT = "metahub-snapshot";
|
|
22605
22821
|
var SNAPSHOT_VERSION = 1;
|
|
22606
22822
|
function readMeta3(db, key) {
|
|
@@ -22692,16 +22908,9 @@ async function restoreSnapshot(db, pkg, opts = {}) {
|
|
|
22692
22908
|
await writeSnapshot(await createSnapshot(db), safetyPath);
|
|
22693
22909
|
let applied = 0;
|
|
22694
22910
|
const tx = db.transaction(() => {
|
|
22695
|
-
for (const t of [
|
|
22696
|
-
"crdt_changes",
|
|
22697
|
-
"databases",
|
|
22698
|
-
"properties",
|
|
22699
|
-
"records",
|
|
22700
|
-
"documents",
|
|
22701
|
-
"doc_blocks",
|
|
22702
|
-
"peers"
|
|
22703
|
-
])
|
|
22911
|
+
for (const t of ["crdt_changes", ...SYNCED_TABLES, "peers"])
|
|
22704
22912
|
db.query(`DELETE FROM ${t}`).run();
|
|
22913
|
+
db.query("DELETE FROM meta WHERE key LIKE '%_replay_seq'").run();
|
|
22705
22914
|
db.query("DELETE FROM meta WHERE key IN ('search_seq', 'search_index_version')").run();
|
|
22706
22915
|
if (ftsAvailable(db))
|
|
22707
22916
|
db.query("DELETE FROM search_fts").run();
|
|
@@ -22932,10 +23141,19 @@ var repair_default = defineCommand({
|
|
|
22932
23141
|
"dry-run": {
|
|
22933
23142
|
type: "boolean",
|
|
22934
23143
|
description: "Report what would be fixed without changing anything (same as `mh doctor`)"
|
|
23144
|
+
},
|
|
23145
|
+
rematerialize: {
|
|
23146
|
+
type: "boolean",
|
|
23147
|
+
description: "First rebuild every replicated table from the oplog (fixes a dropped/emptied/corrupted table; local-only, emits nothing)"
|
|
22935
23148
|
}
|
|
22936
23149
|
},
|
|
22937
23150
|
run: ({ args }) => {
|
|
22938
23151
|
const db = openMetahub();
|
|
23152
|
+
if (args.rematerialize && !args["dry-run"]) {
|
|
23153
|
+
const replayed = rematerializeAll(db);
|
|
23154
|
+
const total = Object.values(replayed).reduce((a, b) => a + b, 0);
|
|
23155
|
+
console.error(`rematerialized ${Object.keys(replayed).length} table(s) from ${total} oplog change(s)`);
|
|
23156
|
+
}
|
|
22939
23157
|
if (args["dry-run"]) {
|
|
22940
23158
|
const report = validateHub(db);
|
|
22941
23159
|
const fixable = report.issues.filter((i) => i.fixable);
|
|
@@ -26415,9 +26633,10 @@ function isMetaCol(dataset, col) {
|
|
|
26415
26633
|
return col === "created_hlc" || col === "__deleted";
|
|
26416
26634
|
}
|
|
26417
26635
|
function oplogExclude() {
|
|
26636
|
+
const excluded = [...OPLOG_ONLY_DATASETS, ...SYSTEM_DATASETS];
|
|
26418
26637
|
return {
|
|
26419
|
-
sql:
|
|
26420
|
-
params:
|
|
26638
|
+
sql: excluded.map(() => "dataset != ?"),
|
|
26639
|
+
params: excluded
|
|
26421
26640
|
};
|
|
26422
26641
|
}
|
|
26423
26642
|
function changesOfTxn(db, txn, beforeHlc) {
|
|
@@ -26881,6 +27100,7 @@ function scalarHtml(specUrl) {
|
|
|
26881
27100
|
}
|
|
26882
27101
|
function startServer(opts = {}) {
|
|
26883
27102
|
registerRoomBlobResolver(resolveBlob);
|
|
27103
|
+
process.env.METAHUB_APP ??= "server";
|
|
26884
27104
|
const tDb = Date.now();
|
|
26885
27105
|
const db = openMetahub();
|
|
26886
27106
|
const dbMs = Date.now() - tDb;
|
|
@@ -27379,9 +27599,34 @@ var styles_default = ` :root {
|
|
|
27379
27599
|
.set-row-line { display:flex; align-items:center; gap:14px; min-height:44px; padding:5px 0; }
|
|
27380
27600
|
.set-row.clickable > .set-row-line { cursor:pointer; }
|
|
27381
27601
|
.set-row-text { flex:1 1 auto; min-width:0; }
|
|
27382
|
-
.set-row-title { font-size:14px; font-weight:550; color:var(--fg); }
|
|
27383
|
-
.set-row-
|
|
27602
|
+
.set-row-title { font-size:14px; font-weight:550; color:var(--fg); display:flex; align-items:center; gap:8px; min-width:0; }
|
|
27603
|
+
.set-row-title > .set-row-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
27604
|
+
.set-row-caption { font-size:12px; color:var(--muted); margin-top:1px; display:flex; align-items:center; gap:6px;
|
|
27384
27605
|
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
27606
|
+
/* leading glyph (device form / bucket) \u2014 a soft tile so rows scan by shape */
|
|
27607
|
+
.set-row-lead { flex:none; width:28px; height:28px; display:grid; place-items:center; color:var(--muted);
|
|
27608
|
+
border-radius:7px; background:var(--surface-2); }
|
|
27609
|
+
.set-row-lead .ico { width:15px; height:15px; }
|
|
27610
|
+
.set-row.dim > .set-row-line { opacity:.62; }
|
|
27611
|
+
/* status dot in a caption: the only colour a row carries */
|
|
27612
|
+
.cap-dot { display:inline-block; flex:none; width:7px; height:7px; border-radius:50%; background:var(--muted); }
|
|
27613
|
+
.cap-dot.ok { background:var(--success); } .cap-dot.warn { background:var(--warning,#9a6700); } .cap-dot.err { background:var(--danger); }
|
|
27614
|
+
.cap-err { color:var(--danger); }
|
|
27615
|
+
/* tag variants next to a title: neutral outline (default), accent (roles), self (soft fill) */
|
|
27616
|
+
.set-row-title .set-row-tag { margin-left:0; vertical-align:0; flex:none; }
|
|
27617
|
+
.set-row-tag.accent { color:var(--accent); background:var(--accent-soft); border-color:transparent; }
|
|
27618
|
+
.set-row-tag.self { color:var(--fg-soft); background:var(--surface-2); border-color:transparent; }
|
|
27619
|
+
.set-row-tag.mono { font-family:var(--mono); letter-spacing:-.01em; }
|
|
27620
|
+
/* quiet "+ add" row closing a list, and a "N more\u2026" fold */
|
|
27621
|
+
.add-row { display:flex; align-items:center; gap:8px; width:100%; min-height:40px; margin-top:2px; padding:0 2px;
|
|
27622
|
+
color:var(--muted); font-size:var(--fs-ui); border-radius:var(--radius-sm); text-align:left; }
|
|
27623
|
+
.add-row:hover { color:var(--fg); background:var(--hover); }
|
|
27624
|
+
.add-row .ico { width:15px; height:15px; }
|
|
27625
|
+
.more-row { display:inline-flex; align-items:center; gap:6px; padding:8px 2px; font-size:var(--fs-sm); color:var(--muted); }
|
|
27626
|
+
.more-row:hover { color:var(--fg); }
|
|
27627
|
+
.set-section-head { display:flex; align-items:baseline; gap:10px; margin:0 0 4px 2px; }
|
|
27628
|
+
.set-section-head .set-section-label { margin:0; }
|
|
27629
|
+
.set-section-count { font-size:12px; color:var(--muted); font-variant-numeric:tabular-nums; }
|
|
27385
27630
|
.set-row-control { flex:none; display:flex; align-items:center; gap:8px; }
|
|
27386
27631
|
.set-row-detail { padding:2px 0 12px; }
|
|
27387
27632
|
.set-row.disabled > .set-row-line { opacity:.55; pointer-events:none; }
|
|
@@ -27446,8 +27691,12 @@ var styles_default = ` :root {
|
|
|
27446
27691
|
.audit-detail { padding:0 4px 10px 86px; }
|
|
27447
27692
|
.audit-detail-ent-name { font-size:var(--fs-sm); font-weight:600; margin-top:6px; }
|
|
27448
27693
|
.audit-blocks { font-size:var(--fs-sm); margin-top:2px; }
|
|
27449
|
-
|
|
27450
|
-
.audit-
|
|
27694
|
+
/* quiet tail: left-aligned text button while more exists; a rule once exhausted */
|
|
27695
|
+
.audit-more { display:inline-flex; align-items:center; gap:6px; margin:14px 0 0 2px; font-size:var(--fs-sm); color:var(--muted); }
|
|
27696
|
+
.audit-more:hover { color:var(--fg); }
|
|
27697
|
+
.audit-more.loading { opacity:.6; pointer-events:none; }
|
|
27698
|
+
.audit-end { display:flex; align-items:center; gap:12px; margin:18px 0 0; color:var(--muted); font-size:var(--fs-xs); }
|
|
27699
|
+
.audit-end::before, .audit-end::after { content:""; flex:1; height:1px; background:var(--line); }
|
|
27451
27700
|
@media (hover:none) { .audit-revert { opacity:1; } }
|
|
27452
27701
|
.set-callout.warn { border-color:color-mix(in srgb,var(--warning,#9a6700) 32%,var(--line)); }
|
|
27453
27702
|
/* two-fact meta line under a block title (who publishes \xB7 where creds live) \u2014
|
|
@@ -27562,28 +27811,31 @@ var styles_default = ` :root {
|
|
|
27562
27811
|
/* concurrent problems under the headline (only when >1, else redundant) */
|
|
27563
27812
|
.sync-health-issues { padding:2px 0 4px; }
|
|
27564
27813
|
.sync-health-issue { font-size:12px; color:var(--muted); padding:2px 0 2px 18px; }
|
|
27565
|
-
|
|
27566
|
-
.sync-health-
|
|
27814
|
+
/* \u67E5\u770B\u5404\u5904: a bordered place list (kind \xB7 name \xB7 freshness \xB7 inline retry) */
|
|
27815
|
+
.sync-health-places { margin-top:6px; border:1px solid var(--line); border-radius:var(--radius); overflow:hidden; }
|
|
27816
|
+
.sync-health-place { display:flex; align-items:center; gap:10px; padding:9px 12px; min-width:0; font-size:var(--fs-sm); }
|
|
27567
27817
|
.sync-health-place + .sync-health-place { border-top:1px solid var(--line); }
|
|
27568
|
-
.sync-health-place-kind { flex:none; font-size:
|
|
27569
|
-
|
|
27570
|
-
.sync-health-place-
|
|
27571
|
-
|
|
27572
|
-
.sync-health-place-cap { flex:1; min-width:0; font-size:12px; color:var(--muted);
|
|
27573
|
-
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
27818
|
+
.sync-health-place-kind { flex:none; width:64px; font-size:var(--fs-xs); color:var(--muted); }
|
|
27819
|
+
.sync-health-place-label { flex:none; font-weight:550; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:40%; }
|
|
27820
|
+
.sync-health-place-cap { flex:1; min-width:0; color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
27821
|
+
.sync-health-place .btn { margin-left:auto; }
|
|
27574
27822
|
.sync-health-role { flex:none; font-size:11px; color:var(--accent); background:var(--accent-soft);
|
|
27575
27823
|
border-radius:999px; padding:2px 7px; }
|
|
27576
27824
|
|
|
27577
|
-
/*
|
|
27578
|
-
|
|
27579
|
-
.device-
|
|
27580
|
-
.device-
|
|
27581
|
-
.device-
|
|
27582
|
-
.device-
|
|
27583
|
-
.device-
|
|
27584
|
-
.device-
|
|
27585
|
-
.device-
|
|
27586
|
-
|
|
27825
|
+
/* device roster (settings/devices-page.tsx): expanded detail = key/value
|
|
27826
|
+
table + one action row + one plain sentence \u2014 no gray callout box. */
|
|
27827
|
+
.device-kv { display:grid; grid-template-columns:max-content 1fr; gap:6px 18px; padding:4px 0 10px 42px; font-size:var(--fs-sm); margin:0; }
|
|
27828
|
+
.device-kv dt { color:var(--muted); margin:0; }
|
|
27829
|
+
.device-kv dd { margin:0; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
27830
|
+
.device-kv .mono { font-family:var(--mono); font-size:var(--fs-xs); letter-spacing:-.01em; }
|
|
27831
|
+
.device-actions { display:flex; align-items:center; gap:4px; padding-left:42px; flex-wrap:wrap; }
|
|
27832
|
+
.device-actions .btn { padding:3px 8px; font-size:12px; }
|
|
27833
|
+
.device-actions .btn.danger { color:var(--danger); }
|
|
27834
|
+
.device-actions .btn.danger:hover { background:var(--danger-soft); color:var(--danger); }
|
|
27835
|
+
.device-note { font-size:12px; color:var(--muted); line-height:1.6; padding:4px 0 6px 42px; max-width:60ch; }
|
|
27836
|
+
.device-note b { color:var(--fg-soft); font-weight:600; }
|
|
27837
|
+
.device-note a { color:var(--accent); text-decoration:none; }
|
|
27838
|
+
.device-note a:hover { text-decoration:underline; }
|
|
27587
27839
|
|
|
27588
27840
|
/* cloud accessories panel (CloudPanel: bucket + edge in one glance) */
|
|
27589
27841
|
.cloud-cta { border:1px solid var(--line); border-radius:10px; padding:14px 16px; margin:4px 0 12px;
|
|
@@ -27658,6 +27910,7 @@ var styles_default = ` :root {
|
|
|
27658
27910
|
.btn-primary { background:var(--accent); color:var(--accent-fg); box-shadow:var(--shadow-sm); }
|
|
27659
27911
|
.btn-primary:hover { filter:brightness(1.07); }
|
|
27660
27912
|
.btn-danger { background:var(--danger); color:#fff; } .btn-danger:hover { filter:brightness(1.05); }
|
|
27913
|
+
.btn.sm { padding:3px 8px; font-size:12px; }
|
|
27661
27914
|
/* Notion-style ambient sync indicator: a quiet ghost control living in the
|
|
27662
27915
|
topbar icon row. State reads through the *text/icon colour* \u2014 no filled pill
|
|
27663
27916
|
or drop shadow \u2014 and only the error state escalates to a visible danger tone.
|
|
@@ -27687,6 +27940,26 @@ var styles_default = ` :root {
|
|
|
27687
27940
|
@keyframes save-pop { 0% { transform:scale(.7); } 100% { transform:scale(1); } }
|
|
27688
27941
|
@keyframes save-shake { 10%,90% { transform:translateX(-1px); } 20%,80% { transform:translateX(1.5px); } 30%,50%,70% { transform:translateX(-2px); } 40%,60% { transform:translateX(2px); } }
|
|
27689
27942
|
@keyframes save-kbd-pulse { 0% { transform:scale(1); } 45% { transform:scale(1.05); } 100% { transform:scale(1); } }
|
|
27943
|
+
/* Ambient replica-sync indicator (sync-indicator.tsx): same ghost-control
|
|
27944
|
+
idiom as .share-save \u2014 zero footprint when idle, colour carries state,
|
|
27945
|
+
only error escalates to danger. Lives in .sb-footer (desktop), .sb-head
|
|
27946
|
+
(mobile home) and the topbar (sidebar collapsed). */
|
|
27947
|
+
.sync-ind { display:inline-flex; align-items:center; gap:6px; height:28px; padding:0 8px; border-radius:var(--radius-sm);
|
|
27948
|
+
font-size:var(--fs-xs); color:var(--fg-soft); white-space:nowrap; font-variant-numeric:tabular-nums; flex:none;
|
|
27949
|
+
animation:save-rise .22s ease both; }
|
|
27950
|
+
.sync-ind:hover { background:var(--hover-2); color:var(--fg); }
|
|
27951
|
+
.sync-ind-ico { display:inline-flex; align-items:center; }
|
|
27952
|
+
.sync-ring { width:12px; height:12px; border-radius:50%; border:1.5px solid var(--line-strong);
|
|
27953
|
+
border-top-color:var(--accent); animation:mh-spin .8s linear infinite; }
|
|
27954
|
+
.sync-ind-done { color:var(--success); }
|
|
27955
|
+
.sync-ind-done .sync-ind-ico { animation:save-pop .24s cubic-bezier(.18,.9,.24,1.22) both; }
|
|
27956
|
+
.sync-ind-error { color:var(--danger); }
|
|
27957
|
+
.sync-ind-error:hover { background:var(--danger-soft); color:var(--danger); }
|
|
27958
|
+
/* footer: push right, version stays flush right after it */
|
|
27959
|
+
.sb-footer .sync-ind { margin-left:auto; }
|
|
27960
|
+
.sb-footer .sync-ind + .sbf-ver { margin-left:8px; }
|
|
27961
|
+
/* head slot is mobile-only (mirrors the .sb-head .iconbtn gating) */
|
|
27962
|
+
.sb-head .sync-ind { display:none; }
|
|
27690
27963
|
|
|
27691
27964
|
/* macOS desktop (hiddenInset frameless): drop the sidebar logo and reserve a
|
|
27692
27965
|
draggable strip so the inset traffic lights never overlap our controls.
|
|
@@ -28376,55 +28649,148 @@ var styles_default = ` :root {
|
|
|
28376
28649
|
.site-empty .et { font-size:15px; color:var(--fg); margin-bottom:5px; font-weight:650; letter-spacing:-.01em; }
|
|
28377
28650
|
.site-empty .ed { font-size:var(--fs-sm); margin-bottom:18px; max-width:42ch; line-height:1.55; }
|
|
28378
28651
|
|
|
28379
|
-
/* \u2500\u2500 \u5206\u4EAB (global ShareView) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
28380
|
-
|
|
28381
|
-
|
|
28652
|
+
/* \u2500\u2500 \u5206\u4EAB (global ShareView) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
28653
|
+
One row = object \xB7 status \xB7 one primary action \xB7 overflow. State is the
|
|
28654
|
+
organizing idea: colour lives only in the status dot and the \u53EF\u7F16\u8F91 word;
|
|
28655
|
+
expiry is a share's normal ending and reads muted, never danger. */
|
|
28656
|
+
.shares-page .db-head { align-items:flex-start; gap:16px; margin-bottom:6px; }
|
|
28657
|
+
.shares-page .db-desc { max-width:56ch; }
|
|
28658
|
+
.shv-toolbar { display:flex; align-items:center; gap:10px; flex-wrap:nowrap; padding:10px 0; margin-bottom:8px; }
|
|
28659
|
+
.shv-tools-right { display:flex; align-items:center; gap:10px; flex:none; margin-left:auto; }
|
|
28382
28660
|
.shv-seg { display:inline-flex; background:var(--surface-2); border:1px solid var(--line); border-radius:var(--radius-sm); padding:2px; }
|
|
28383
|
-
.shv-seg-btn { padding:5px 11px; border-radius:calc(var(--radius-sm) - 2px); font-size:13px; color:var(--fg-soft); }
|
|
28661
|
+
.shv-seg-btn { display:inline-flex; align-items:baseline; padding:5px 11px; border-radius:calc(var(--radius-sm) - 2px); font-size:13px; color:var(--fg-soft); }
|
|
28384
28662
|
.shv-seg-btn.active { background:var(--bg); color:var(--fg); box-shadow:var(--shadow-sm); }
|
|
28385
|
-
.shv-
|
|
28386
|
-
.shv-
|
|
28663
|
+
.shv-seg-btn .n { display:inline-flex; justify-content:flex-end; align-items:center; min-width:2ch; margin-left:5px; font-family:var(--mono); font-size:var(--fs-micro); color:var(--muted); font-variant-numeric:tabular-nums; }
|
|
28664
|
+
.shv-seg-btn .skel-n { width:2ch; height:.85em; border-radius:3px; animation:skel-in .25s ease .12s both; }
|
|
28665
|
+
.shv-seg-btn.active .n { color:var(--fg-soft); }
|
|
28666
|
+
.shv-src { gap:5px; color:var(--fg-soft); }
|
|
28667
|
+
.shv-src.on { color:var(--fg); }
|
|
28668
|
+
.shv-src .ico { width:14px; height:14px; opacity:.7; }
|
|
28669
|
+
.shv-search { display:flex; align-items:center; gap:6px; flex:1 1 220px; min-width:140px; max-width:360px; background:var(--bg); border:1px solid var(--line-strong); border-radius:var(--radius-sm); padding:5px 10px; color:var(--muted); }
|
|
28670
|
+
.shv-search:focus-within { border-color:var(--accent); }
|
|
28671
|
+
.shv-search input { border:0; background:none; outline:none; color:var(--fg); font-size:13px; width:100%; min-width:0; }
|
|
28387
28672
|
|
|
28388
28673
|
.shv-list { display:flex; flex-direction:column; gap:8px; list-style:none; padding:0; margin:0; }
|
|
28389
|
-
.shv-row { display:
|
|
28674
|
+
.shv-row { display:grid; grid-template-columns:38px minmax(0,1fr) auto auto; gap:6px 13px; align-items:center;
|
|
28675
|
+
padding:12px 15px; background:var(--bg); border:1px solid var(--line);
|
|
28390
28676
|
border-radius:var(--radius-lg); transition:border-color .12s, box-shadow .12s; }
|
|
28391
28677
|
.shv-row:hover { border-color:var(--line-strong); box-shadow:var(--shadow-sm); }
|
|
28392
28678
|
.shv-ico { width:38px; height:38px; flex:none; display:grid; place-items:center; border-radius:var(--radius-md, 10px);
|
|
28393
28679
|
background:var(--surface-2); color:var(--fg-soft); }
|
|
28394
28680
|
.shv-ico svg { width:19px; height:19px; }
|
|
28395
28681
|
.shv-ico.k-doc { color:var(--accent); background:var(--accent-soft); }
|
|
28682
|
+
.shv-ico.k-database { color:var(--fg-soft); }
|
|
28396
28683
|
.shv-ico.k-site { color:#1a7f5a; }
|
|
28397
|
-
.shv-main {
|
|
28398
|
-
.shv-
|
|
28399
|
-
|
|
28684
|
+
.shv-main { min-width:0; }
|
|
28685
|
+
.shv-title { display:block; max-width:100%; font-weight:600; color:var(--fg); letter-spacing:-.01em;
|
|
28686
|
+
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:left; }
|
|
28400
28687
|
.shv-title:hover { color:var(--accent); }
|
|
28401
|
-
.shv-
|
|
28402
|
-
|
|
28403
|
-
.shv-
|
|
28404
|
-
.shv-
|
|
28405
|
-
.shv-
|
|
28406
|
-
.shv-
|
|
28407
|
-
|
|
28688
|
+
.shv-sub { display:flex; align-items:center; gap:6px; margin-top:3px; min-width:0; font-size:var(--fs-sm); color:var(--muted);
|
|
28689
|
+
white-space:nowrap; overflow:hidden; }
|
|
28690
|
+
.shv-sub > * { flex:none; }
|
|
28691
|
+
.shv-sub > .shv-link { flex:0 1 auto; }
|
|
28692
|
+
.shv-link-text { min-width:0; overflow:hidden; text-overflow:ellipsis; }
|
|
28693
|
+
.shv-sep { color:var(--line-strong); }
|
|
28694
|
+
/* the link itself: mono, copy on click, copy glyph slides in on hover */
|
|
28695
|
+
.shv-link { display:inline-flex; align-items:center; gap:4px; min-width:0; max-width:46ch; font-family:var(--mono); font-size:var(--fs-xs);
|
|
28696
|
+
color:var(--fg-soft); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; border-radius:4px; }
|
|
28697
|
+
.shv-link .ico { width:12px; height:12px; opacity:0; transition:opacity .12s; flex:none; }
|
|
28698
|
+
.shv-link:not(:disabled):hover { color:var(--accent); }
|
|
28699
|
+
.shv-link:not(:disabled):hover .ico { opacity:1; }
|
|
28700
|
+
.shv-link:disabled { cursor:default; text-decoration:line-through; text-decoration-color:var(--line-strong); }
|
|
28701
|
+
.shv-snap { font-variant-numeric:tabular-nums; }
|
|
28702
|
+
.shv-perm.edit { color:var(--danger); font-weight:500; }
|
|
28703
|
+
.shv-lock { display:inline-flex; color:var(--fg-soft); }
|
|
28704
|
+
.shv-lock .ico { width:12px; height:12px; }
|
|
28705
|
+
/* status column: dot + short text, fixed floor so rows never jitter */
|
|
28706
|
+
.shv-status { display:inline-flex; align-items:center; gap:7px; min-width:9ch; font-size:var(--fs-sm); color:var(--fg-soft);
|
|
28707
|
+
font-variant-numeric:tabular-nums; white-space:nowrap; }
|
|
28708
|
+
.shv-dot { width:8px; height:8px; border-radius:50%; background:var(--muted); flex:none; }
|
|
28709
|
+
.shv-status.tone-ok .shv-dot { background:var(--success); }
|
|
28710
|
+
.shv-status.tone-warn { color:var(--warning,#9a6700); }
|
|
28711
|
+
.shv-status.tone-warn .shv-dot { background:var(--warning,#9a6700); }
|
|
28712
|
+
.shv-status.tone-muted { color:var(--muted); }
|
|
28713
|
+
.shv-status.tone-muted .shv-dot { background:transparent; border:1.5px solid var(--line-strong); }
|
|
28714
|
+
.shv-status .sync-ring { width:10px; height:10px; }
|
|
28408
28715
|
.shv-actions { display:flex; align-items:center; gap:6px; flex:none; }
|
|
28409
|
-
.shv-actions .btn { padding:5px 10px; font-size:12px; }
|
|
28716
|
+
.shv-actions .btn { padding:5px 10px; font-size:12px; min-width:76px; justify-content:center; }
|
|
28717
|
+
.shv-actions .btn.txt-danger { color:var(--danger); }
|
|
28718
|
+
.shv-row.st-expired .shv-title { color:var(--fg-soft); }
|
|
28719
|
+
/* expired group sinks below the live list */
|
|
28720
|
+
.shv-group-head { display:flex; align-items:center; gap:8px; margin:22px 2px 8px; font-size:var(--fs-sm); font-weight:600; color:var(--muted); }
|
|
28721
|
+
.shv-group-head .n { font-family:var(--mono); font-size:var(--fs-micro); font-weight:400; }
|
|
28722
|
+
.shv-group-head .btn { margin-left:auto; padding:4px 10px; font-size:12px; }
|
|
28723
|
+
/* list swap: data arriving over the skeleton fades in rather than hard-cutting */
|
|
28724
|
+
.shv-list { animation:skel-in .18s ease both; }
|
|
28725
|
+
|
|
28726
|
+
/* \u2500\u2500 skeletons (skeleton.tsx, primitives.tsx SetRowSkeleton, shares-view
|
|
28727
|
+
ShareListSkeleton). Rows are the REAL row markup; only content is swapped
|
|
28728
|
+
for sheen blocks. .skel-list fades in after 120ms so an instant load
|
|
28729
|
+
never flashes it; --i offsets each row's sheen into one rolling wave. */
|
|
28730
|
+
.skel-list { animation:skel-in .25s ease .12s both; }
|
|
28731
|
+
.skel-lines { display:flex; flex-direction:column; gap:8px; }
|
|
28732
|
+
.skel-lines .skel-s { width:calc(58% + var(--i, 0) * 9%); max-width:100%; }
|
|
28733
|
+
.page-header-sub .skel-s { display:block; width:22ch; }
|
|
28734
|
+
.set-row.skel { pointer-events:none; }
|
|
28735
|
+
.set-row.skel .set-row-text { display:flex; flex-direction:column; gap:5px; padding:3px 0; }
|
|
28736
|
+
.set-row.skel .skel-t { height:14px; width:calc(30% + var(--i, 0) * 8%); border-radius:5px; }
|
|
28737
|
+
.set-row.skel .skel-s { height:11px; width:calc(44% + var(--i, 0) * 7%); border-radius:4px; }
|
|
28738
|
+
.set-row.skel .set-row-lead.skel-b { border-radius:7px; }
|
|
28739
|
+
.skel-switch { width:38px; height:22px; border-radius:999px; }
|
|
28740
|
+
.skel-chevron { width:14px; height:14px; border-radius:4px; }
|
|
28741
|
+
.skel-menu { width:28px; height:28px; border-radius:var(--radius-sm); }
|
|
28742
|
+
.skel-button { width:76px; height:29px; border-radius:var(--radius-sm); }
|
|
28743
|
+
.audit-item.skel .audit-item-line { cursor:default; pointer-events:none; }
|
|
28744
|
+
.audit-item.skel .skel-when { width:56px; height:12px; border-radius:4px; flex:none; }
|
|
28745
|
+
.audit-item.skel .skel-t { height:14px; width:calc(34% + var(--i, 0) * 9%); border-radius:5px; }
|
|
28746
|
+
.site-card.skel { pointer-events:none; }
|
|
28747
|
+
.site-card.skel:hover { transform:none; box-shadow:none; border-color:var(--line); }
|
|
28748
|
+
.site-card.skel .si.skel-b { background:var(--surface-2); }
|
|
28749
|
+
.site-card.skel .skel-t { height:14px; width:calc(40% + var(--i, 0) * 8%); border-radius:5px; margin-bottom:6px; }
|
|
28750
|
+
.site-card.skel .skel-s { height:11px; width:calc(55% + var(--i, 0) * 6%); border-radius:4px; }
|
|
28751
|
+
.site-card.skel .skel-addr { height:30px; width:80%; border-radius:var(--radius-sm); }
|
|
28752
|
+
.filerow.skel { pointer-events:none; }
|
|
28753
|
+
.filerow.skel .fi.skel-b { background:var(--surface-2); }
|
|
28754
|
+
.filerow.skel .skel-t { height:13px; width:calc(36% + var(--i, 0) * 10%); border-radius:4px; }
|
|
28755
|
+
.blob-hero.skel .blob-legend .skel-s { height:12px; width:calc(70% + var(--i, 0) * 8%); }
|
|
28756
|
+
.blob-hero.skel .blob-total .skel-s { display:block; height:11px; width:14ch; }
|
|
28757
|
+
.skel-b { display:block; position:relative; overflow:hidden; background:var(--surface-2); border-radius:6px; }
|
|
28758
|
+
.skel-b::after { content:""; position:absolute; inset:0;
|
|
28759
|
+
background:linear-gradient(90deg, transparent 20%, color-mix(in srgb, var(--fg) 5%, transparent) 50%, transparent 80%);
|
|
28760
|
+
background-size:200% 100%; animation:skel-sweep 1.6s ease-in-out infinite;
|
|
28761
|
+
animation-delay:calc(var(--i, 0) * 120ms); }
|
|
28762
|
+
.shv-row.skel { pointer-events:none; }
|
|
28763
|
+
.shv-row.skel:hover { border-color:var(--line); box-shadow:none; }
|
|
28764
|
+
.skel-t { height:1.55em; height:1lh; width:calc(52% + var(--i, 0) * 9%); border-radius:5px; }
|
|
28765
|
+
.skel-s { height:1.55em; height:1lh; width:min(44ch, 60%); border-radius:4px; }
|
|
28766
|
+
.shv-status .skel-st { height:1.55em; height:1lh; width:7ch; border-radius:4px; }
|
|
28767
|
+
.skel-btn { width:76px; height:29px; border-radius:var(--radius-sm); }
|
|
28768
|
+
.skel-ib { width:30px; height:30px; border-radius:var(--radius-sm); }
|
|
28769
|
+
@keyframes skel-in { from { opacity:0; } to { opacity:1; } }
|
|
28770
|
+
@keyframes skel-sweep { from { background-position:100% 0; } to { background-position:-100% 0; } }
|
|
28410
28771
|
@media (max-width:640px){
|
|
28411
|
-
|
|
28772
|
+
.shv-actions .skel-btn { flex:1; width:auto; height:36px; }
|
|
28773
|
+
.shv-actions .skel-ib { width:40px; height:36px; }
|
|
28774
|
+
/* head stacks: title/desc full row, then segmented tabs, then [source][search] */
|
|
28412
28775
|
.shares-page .db-head { flex-direction:column; align-items:stretch; gap:12px; }
|
|
28413
|
-
.shv-
|
|
28414
|
-
.shv-
|
|
28415
|
-
.shv-seg
|
|
28416
|
-
.shv-
|
|
28417
|
-
.shv-
|
|
28418
|
-
|
|
28419
|
-
|
|
28420
|
-
|
|
28421
|
-
.shv-
|
|
28422
|
-
|
|
28423
|
-
.shv-
|
|
28424
|
-
|
|
28425
|
-
|
|
28426
|
-
.shv-actions {
|
|
28427
|
-
.shv-actions .btn { flex:1;
|
|
28776
|
+
.shv-toolbar { flex-wrap:wrap; }
|
|
28777
|
+
.shv-tools-right { display:contents; }
|
|
28778
|
+
.shv-seg { display:flex; width:100%; order:-1; }
|
|
28779
|
+
.shv-seg-btn { flex:1; justify-content:center; padding:6px 4px; }
|
|
28780
|
+
.shv-src { flex:1; justify-content:center; }
|
|
28781
|
+
.shv-search { flex:1; min-width:0; max-width:none; }
|
|
28782
|
+
|
|
28783
|
+
/* rows: ico + main on top; status, then the action row, each full width */
|
|
28784
|
+
.shv-row { grid-template-columns:38px minmax(0,1fr); gap:8px 13px; align-items:start; }
|
|
28785
|
+
.shv-title { white-space:normal; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; }
|
|
28786
|
+
.shv-sub { white-space:normal; flex-wrap:wrap; row-gap:2px; }
|
|
28787
|
+
.shv-link { max-width:100%; }
|
|
28788
|
+
.shv-status { grid-column:1 / -1; min-width:0; }
|
|
28789
|
+
.shv-actions { grid-column:1 / -1; }
|
|
28790
|
+
.shv-actions .btn { flex:1; padding:8px 10px; }
|
|
28791
|
+
.shv-actions .iconbtn { width:40px; height:36px; flex:none; }
|
|
28792
|
+
.shv-group-head { flex-wrap:wrap; }
|
|
28793
|
+
.shv-group-head .btn { margin-left:0; width:100%; justify-content:center; padding:7px 10px; }
|
|
28428
28794
|
}
|
|
28429
28795
|
|
|
28430
28796
|
/* in-context "\u5DF2\u5206\u4EAB" badge on docs / tables / site cards */
|
|
@@ -29109,6 +29475,7 @@ var styles_default = ` :root {
|
|
|
29109
29475
|
html:has(body.mobile), body.mobile { background:var(--sidebar); }
|
|
29110
29476
|
html:has(body.mobile-content), body.mobile-content { background:var(--bg); }
|
|
29111
29477
|
.sb-head .iconbtn { display:none; } /* no collapse on mobile */
|
|
29478
|
+
.sb-head .sync-ind { display:inline-flex; height:32px; } /* .brand's flex:1 already pushes it right */
|
|
29112
29479
|
.sb-resizer { display:none; }
|
|
29113
29480
|
.topbar .hamburger { display:grid; } /* serves as the back button */
|
|
29114
29481
|
/* site config topbar: icon-only publish button so the crumb keeps its last segment */
|
|
@@ -29566,6 +29933,12 @@ var styles_default = ` :root {
|
|
|
29566
29933
|
.blob-dot.keep { background:var(--line-strong); }
|
|
29567
29934
|
.blob-dot.pin { background:var(--muted); opacity:.6; }
|
|
29568
29935
|
.blob-total { margin:13px 0 14px; font-size:var(--fs-xs); color:var(--muted); }
|
|
29936
|
+
/* headline number over the legend (\u6570\u636E\u4E0E\u5907\u4EFD \xB7 \u9644\u4EF6) */
|
|
29937
|
+
.blob-stat { font-size:20px; font-weight:700; letter-spacing:-.02em; font-variant-numeric:tabular-nums; margin-bottom:10px; }
|
|
29938
|
+
.blob-stat small { font-size:var(--fs-sm); font-weight:500; color:var(--muted); margin-left:8px; letter-spacing:0; }
|
|
29939
|
+
/* a status sentence in the action row (never a disabled button) */
|
|
29940
|
+
.blob-actions .blob-status { display:inline-flex; align-items:center; gap:6px; font-size:var(--fs-sm); color:var(--muted); }
|
|
29941
|
+
.blob-actions .blob-status .ico { width:14px; height:14px; color:var(--success); }
|
|
29569
29942
|
|
|
29570
29943
|
.blob-ring { position:relative; width:152px; height:152px; flex:none; }
|
|
29571
29944
|
.blob-ring-svg { width:100%; height:100%; display:block; }
|
|
@@ -29600,7 +29973,7 @@ var styles_default = ` :root {
|
|
|
29600
29973
|
}
|
|
29601
29974
|
|
|
29602
29975
|
@media (prefers-reduced-motion: reduce) {
|
|
29603
|
-
.switch, .switch-knob, .ico.spin, .blob-seg, .blob-ring-track { transition:none; animation:none; }
|
|
29976
|
+
.switch, .switch-knob, .ico.spin, .sync-ring, .blob-seg, .blob-ring-track { transition:none; animation:none; }
|
|
29604
29977
|
}
|
|
29605
29978
|
|
|
29606
29979
|
/* Blob manager popup (Settings storage \u2192 Blob \u7BA1\u7406): a sortable/filterable list
|
|
@@ -31296,8 +31669,11 @@ var FieldHistoryEntrySchema = object({
|
|
|
31296
31669
|
});
|
|
31297
31670
|
var NodeInfoSchema = object({
|
|
31298
31671
|
node_id: string2(),
|
|
31299
|
-
label: string2().nullable().describe("
|
|
31300
|
-
self: boolean2().describe("True for the node serving this request")
|
|
31672
|
+
label: string2().nullable().describe("Synced roster name \u2192 pairing label \u2192 null when unnamed"),
|
|
31673
|
+
self: boolean2().describe("True for the node serving this request"),
|
|
31674
|
+
platform: _enum(["macos", "windows", "linux", "ios", "android", "web"]).nullable(),
|
|
31675
|
+
form: _enum(["laptop", "desktop", "phone", "server", "browser"]).nullable(),
|
|
31676
|
+
app: _enum(["cli", "server", "desktop", "web"]).nullable()
|
|
31301
31677
|
});
|
|
31302
31678
|
var CacheStatsSchema = object({
|
|
31303
31679
|
totalBytes: number2(),
|
|
@@ -31807,13 +32183,17 @@ var webuiRoutes = [
|
|
|
31807
32183
|
{
|
|
31808
32184
|
method: "PATCH",
|
|
31809
32185
|
path: "/api/node",
|
|
31810
|
-
summary: "
|
|
31811
|
-
request: object({
|
|
32186
|
+
summary: "Rename a device (default: this one). Writes the synced roster; empty/null clears",
|
|
32187
|
+
request: object({
|
|
32188
|
+
label: string2().nullable().optional(),
|
|
32189
|
+
node_id: string2().optional().describe("Target node; omitted = this device")
|
|
32190
|
+
}),
|
|
31812
32191
|
response: NodeInfoSchema,
|
|
31813
32192
|
handler: handle3(async (req, { db }) => {
|
|
31814
32193
|
const body = await req.json();
|
|
31815
|
-
|
|
31816
|
-
|
|
32194
|
+
const id = body.node_id ?? getNodeId(db);
|
|
32195
|
+
setNodeLabel(db, body.label ?? null, id);
|
|
32196
|
+
return displayNodes(db).find((n) => n.node_id === id);
|
|
31817
32197
|
})
|
|
31818
32198
|
},
|
|
31819
32199
|
{
|
|
@@ -32535,4 +32915,4 @@ if (scopedConfigHelp) {
|
|
|
32535
32915
|
runMain(main, { showUsage: showUsage2 });
|
|
32536
32916
|
}
|
|
32537
32917
|
|
|
32538
|
-
//# debugId=
|
|
32918
|
+
//# debugId=A6FF3D00837917E364756E2164756E21
|