sealkeep 0.9.0 → 0.11.1
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/ARCHITECTURE.md +163 -14
- package/CHANGELOG.md +252 -1
- package/CONTROL_PLANE.md +2 -2
- package/LICENSE +1 -1
- package/README.md +108 -22
- package/THIRD_PARTY.md +2 -2
- package/THREAT_MODEL.md +23 -4
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
- package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +53 -13
- package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
- package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
- package/dist/site/index.html +1808 -1904
- package/dist/site/llms.txt +67 -0
- package/dist/site/trust/architecture-data-flow.html +53 -0
- package/dist/site/trust/audit-roadmap.html +37 -0
- package/dist/site/trust/deployment-responsibility.html +11 -0
- package/dist/site/trust/dpa-sample.html +30 -0
- package/dist/site/trust/release-provenance.html +21 -0
- package/dist/site/trust/subprocessors.html +15 -0
- package/dist/site/trust/threat-model.html +34 -0
- package/dist/site/trust/toms.html +41 -0
- package/dist/site/trust-document.css +32 -0
- package/dist/site/trust.html +73 -0
- package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
- package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
- package/dist/site/visual/index.html +18 -0
- package/dist/site.zip +0 -0
- package/dist/src/activity.d.ts +9 -0
- package/dist/src/activity.js +90 -1
- package/dist/src/adapters.d.ts +175 -5
- package/dist/src/adapters.js +961 -73
- package/dist/src/agent-context.d.ts +135 -0
- package/dist/src/agent-context.js +1059 -0
- package/dist/src/archive-copies.d.ts +47 -0
- package/dist/src/archive-copies.js +179 -0
- package/dist/src/audit.d.ts +1 -1
- package/dist/src/audit.js +29 -4
- package/dist/src/autopilot.d.ts +52 -7
- package/dist/src/autopilot.js +143 -25
- package/dist/src/background-bandwidth.d.ts +46 -0
- package/dist/src/background-bandwidth.js +301 -0
- package/dist/src/background-cpu.d.ts +82 -0
- package/dist/src/background-cpu.js +212 -0
- package/dist/src/background-worker-error.d.ts +12 -0
- package/dist/src/background-worker-error.js +18 -0
- package/dist/src/branding.d.ts +24 -1
- package/dist/src/branding.js +31 -1
- package/dist/src/bridge.d.ts +233 -0
- package/dist/src/bridge.js +604 -0
- package/dist/src/byte-stream.d.ts +91 -0
- package/dist/src/byte-stream.js +385 -0
- package/dist/src/chunk-store.d.ts +41 -8
- package/dist/src/chunk-store.js +161 -65
- package/dist/src/cli.js +1698 -163
- package/dist/src/cloud.d.ts +841 -31
- package/dist/src/cloud.js +3196 -277
- package/dist/src/context-background.d.ts +37 -0
- package/dist/src/context-background.js +309 -0
- package/dist/src/context-drain-child.d.ts +1 -0
- package/dist/src/context-drain-child.js +98 -0
- package/dist/src/context-reader.d.ts +118 -0
- package/dist/src/context-reader.js +447 -0
- package/dist/src/control-plane/auth.d.ts +32 -4
- package/dist/src/control-plane/auth.js +85 -24
- package/dist/src/control-plane/server.js +19 -6
- package/dist/src/control-plane.d.ts +17 -1
- package/dist/src/control-plane.js +32 -6
- package/dist/src/crypto.d.ts +1 -1
- package/dist/src/crypto.js +5 -5
- package/dist/src/daemon-lease.d.ts +70 -0
- package/dist/src/daemon-lease.js +420 -0
- package/dist/src/daemon.d.ts +94 -1
- package/dist/src/daemon.js +1082 -105
- package/dist/src/darwin-service-policy.d.ts +41 -0
- package/dist/src/darwin-service-policy.js +60 -0
- package/dist/src/dashboard-cli.js +15 -15
- package/dist/src/device-authorization.d.ts +37 -0
- package/dist/src/device-authorization.js +199 -0
- package/dist/src/device-enrollment.d.ts +91 -0
- package/dist/src/device-enrollment.js +349 -0
- package/dist/src/disk.d.ts +17 -12
- package/dist/src/disk.js +43 -17
- package/dist/src/doctor.d.ts +35 -1
- package/dist/src/doctor.js +332 -41
- package/dist/src/durable-ticket-lock.d.ts +24 -0
- package/dist/src/durable-ticket-lock.js +232 -0
- package/dist/src/enroll.d.ts +1 -1
- package/dist/src/enroll.js +13 -7
- package/dist/src/env.d.ts +10 -1
- package/dist/src/env.js +11 -3
- package/dist/src/errors.d.ts +8 -8
- package/dist/src/errors.js +6 -6
- package/dist/src/flush.d.ts +12 -0
- package/dist/src/flush.js +37 -0
- package/dist/src/heartbeat.d.ts +86 -12
- package/dist/src/heartbeat.js +415 -29
- package/dist/src/index-background-watchdog.d.ts +1 -0
- package/dist/src/index-background-watchdog.js +94 -0
- package/dist/src/index-background-work.d.ts +21 -0
- package/dist/src/index-background-work.js +25 -0
- package/dist/src/index-background.d.ts +64 -0
- package/dist/src/index-background.js +394 -0
- package/dist/src/index-build-child.d.ts +1 -0
- package/dist/src/index-build-child.js +109 -0
- package/dist/src/index-manifest.d.ts +52 -0
- package/dist/src/index-manifest.js +444 -0
- package/dist/src/index-publication-proof.d.ts +84 -0
- package/dist/src/index-publication-proof.js +380 -0
- package/dist/src/index-publication-state.d.ts +149 -0
- package/dist/src/index-publication-state.js +696 -0
- package/dist/src/index-publication-verifier.d.ts +89 -0
- package/dist/src/index-publication-verifier.js +341 -0
- package/dist/src/index-publish.d.ts +62 -0
- package/dist/src/index-publish.js +540 -0
- package/dist/src/index-scratch-cleanup.d.ts +19 -0
- package/dist/src/index-scratch-cleanup.js +166 -0
- package/dist/src/index-segment-types.d.ts +132 -0
- package/dist/src/index-segment-types.js +21 -0
- package/dist/src/index-segments.d.ts +9 -0
- package/dist/src/index-segments.js +516 -0
- package/dist/src/index-store.d.ts +123 -0
- package/dist/src/index-store.js +495 -0
- package/dist/src/index-sync.d.ts +91 -15
- package/dist/src/index-sync.js +286 -53
- package/dist/src/index-upgrade-publication.d.ts +30 -0
- package/dist/src/index-upgrade-publication.js +179 -0
- package/dist/src/integration-manager.d.ts +32 -0
- package/dist/src/integration-manager.js +394 -0
- package/dist/src/leakscan.js +1 -1
- package/dist/src/local-api.d.ts +56 -15
- package/dist/src/local-api.js +4979 -541
- package/dist/src/machine-settings.d.ts +51 -0
- package/dist/src/machine-settings.js +166 -0
- package/dist/src/managed-chunks.d.ts +5 -2
- package/dist/src/managed-chunks.js +14 -14
- package/dist/src/mcp-install.d.ts +11 -9
- package/dist/src/mcp-install.js +73 -28
- package/dist/src/mcp-workspace.d.ts +18 -0
- package/dist/src/mcp-workspace.js +50 -0
- package/dist/src/mcp.js +294 -25
- package/dist/src/migrate.js +27 -21
- package/dist/src/notify.d.ts +1 -1
- package/dist/src/notify.js +5 -5
- package/dist/src/offload.d.ts +201 -14
- package/dist/src/offload.js +1848 -140
- package/dist/src/onboarding.d.ts +8 -1
- package/dist/src/onboarding.js +4 -4
- package/dist/src/packages.d.ts +2 -2
- package/dist/src/packages.js +10 -2
- package/dist/src/passkey.d.ts +0 -1
- package/dist/src/passkey.js +2 -7
- package/dist/src/password-lock.d.ts +2 -2
- package/dist/src/password-lock.js +6 -6
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/presence.d.ts +86 -0
- package/dist/src/presence.js +240 -0
- package/dist/src/progress-deadline.d.ts +21 -0
- package/dist/src/progress-deadline.js +91 -0
- package/dist/src/project-repair.d.ts +55 -0
- package/dist/src/project-repair.js +131 -0
- package/dist/src/providers/gcs.d.ts +28 -7
- package/dist/src/providers/gcs.js +35 -24
- package/dist/src/providers/gdrive.d.ts +71 -8
- package/dist/src/providers/gdrive.js +223 -50
- package/dist/src/providers/index.d.ts +11 -3
- package/dist/src/providers/index.js +30 -10
- package/dist/src/providers/s3.d.ts +30 -8
- package/dist/src/providers/s3.js +41 -30
- package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
- package/dist/src/providers/safe-storage-fetch.js +72 -0
- package/dist/src/queue.d.ts +136 -19
- package/dist/src/queue.js +862 -96
- package/dist/src/reclaim-transaction.d.ts +156 -0
- package/dist/src/reclaim-transaction.js +1027 -0
- package/dist/src/recovery-codes.d.ts +32 -0
- package/dist/src/recovery-codes.js +338 -0
- package/dist/src/recovery.js +12 -9
- package/dist/src/rehydrate.d.ts +25 -22
- package/dist/src/rehydrate.js +319 -23
- package/dist/src/restore.d.ts +57 -4
- package/dist/src/restore.js +272 -36
- package/dist/src/resume-stub.d.ts +92 -0
- package/dist/src/resume-stub.js +417 -0
- package/dist/src/retention.d.ts +98 -7
- package/dist/src/retention.js +1066 -62
- package/dist/src/rotate.js +3 -3
- package/dist/src/search.d.ts +566 -8
- package/dist/src/search.js +5940 -297
- package/dist/src/secrets.d.ts +51 -7
- package/dist/src/secrets.js +316 -24
- package/dist/src/service.d.ts +49 -11
- package/dist/src/service.js +776 -35
- package/dist/src/share.js +3 -3
- package/dist/src/shared-spaces.d.ts +98 -0
- package/dist/src/shared-spaces.js +214 -0
- package/dist/src/source-reader.d.ts +73 -0
- package/dist/src/source-reader.js +715 -0
- package/dist/src/spool.d.ts +1 -1
- package/dist/src/spool.js +1 -1
- package/dist/src/start-tui.js +2 -1
- package/dist/src/start.js +2 -2
- package/dist/src/storage-endpoint.d.ts +21 -0
- package/dist/src/storage-endpoint.js +122 -0
- package/dist/src/storage-setup.js +12 -12
- package/dist/src/storage-targets.d.ts +109 -6
- package/dist/src/storage-targets.js +975 -67
- package/dist/src/stream-to-cloud.d.ts +5 -1
- package/dist/src/stream-to-cloud.js +34 -14
- package/dist/src/sync-rules.d.ts +31 -6
- package/dist/src/sync-rules.js +153 -14
- package/dist/src/team-backfill-scheduling.d.ts +8 -0
- package/dist/src/team-backfill-scheduling.js +33 -0
- package/dist/src/team-backfill.d.ts +116 -0
- package/dist/src/team-backfill.js +1429 -0
- package/dist/src/team-index-cache.d.ts +16 -0
- package/dist/src/team-index-cache.js +152 -0
- package/dist/src/team-offboarding.d.ts +38 -0
- package/dist/src/team-offboarding.js +1043 -0
- package/dist/src/team-presence.d.ts +127 -0
- package/dist/src/team-presence.js +904 -0
- package/dist/src/team-publication-policy.d.ts +20 -0
- package/dist/src/team-publication-policy.js +140 -0
- package/dist/src/team-realtime.d.ts +68 -0
- package/dist/src/team-realtime.js +816 -0
- package/dist/src/team-source-facts-cache.d.ts +23 -0
- package/dist/src/team-source-facts-cache.js +255 -0
- package/dist/src/trash.d.ts +1 -1
- package/dist/src/trash.js +2 -2
- package/dist/src/tui.js +11 -12
- package/dist/src/types.d.ts +173 -7
- package/dist/src/types.js +20 -0
- package/dist/src/ui-server.d.ts +163 -35
- package/dist/src/ui-server.js +712 -72
- package/dist/src/ui.d.ts +1 -2
- package/dist/src/ui.js +1 -2
- package/dist/src/upload.d.ts +27 -0
- package/dist/src/upload.js +383 -43
- package/dist/src/vault.d.ts +226 -30
- package/dist/src/vault.js +1776 -192
- package/dist/src/watcher.d.ts +7 -1
- package/dist/src/watcher.js +198 -55
- package/dist/src/worker.d.ts +27 -3
- package/dist/src/worker.js +274 -55
- package/package.json +33 -12
- package/scripts/native-reboot-rehearsal.mjs +90 -0
- package/web/app.js +6032 -343
- package/web/bootstrap.js +17 -0
- package/web/index.html +255 -57
- package/web/rail.js +317 -40
- package/web/retention.html +2 -2
- package/web/rules-view.js +188 -16
- package/web/sessions-view.js +485 -62
- package/web/sessions.html +2 -2
- package/web/setup-api.js +152 -29
- package/web/setup-logic.js +68 -9
- package/web/setup.html +113 -44
- package/web/setup.js +604 -71
- package/web/style.css +513 -98
- package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
package/web/rail.js
CHANGED
|
@@ -8,10 +8,22 @@
|
|
|
8
8
|
* `.acct` markup and an `api(path)` helper can render the same card with one
|
|
9
9
|
* call; pages without the markup are a no-op, never an error.
|
|
10
10
|
*/
|
|
11
|
-
function renderAccountRail(api, status) {
|
|
11
|
+
function renderAccountRail(api, status, options = {}) {
|
|
12
12
|
const $id = (id) => document.getElementById(id);
|
|
13
|
+
// Dashboard callers bind this render to one vault/account generation. A
|
|
14
|
+
// setup, target, or offer request can outlive a sign-in change, so every
|
|
15
|
+
// continuation checks the binding again before it touches the shared rail
|
|
16
|
+
// or its remembered snapshot.
|
|
17
|
+
const isCurrent = typeof options.isCurrent === "function" ? options.isCurrent : () => true;
|
|
18
|
+
if (!isCurrent()) return Promise.resolve();
|
|
19
|
+
try { localStorage.removeItem("sealkeep.localApiToken"); } catch { /* upgrade cleanup is best-effort */ }
|
|
13
20
|
const card = $id("acct-card");
|
|
14
21
|
if (!card) return Promise.resolve();
|
|
22
|
+
// A newer rail render owns upload polling from this point onward. Without
|
|
23
|
+
// stopping the prior generation here, its timer can remain installed until
|
|
24
|
+
// the next tick; the new generation then sees a timer and never starts its
|
|
25
|
+
// own poll after the old one retires itself as stale.
|
|
26
|
+
if (!options.cachedOnly) railStopUploadPoll();
|
|
15
27
|
|
|
16
28
|
// Identical to the app's own bytes(): binary units, one decimal. The first
|
|
17
29
|
// version divided by 1e9, so the same quota read "536.9 GB" in this card and
|
|
@@ -27,39 +39,119 @@ function renderAccountRail(api, status) {
|
|
|
27
39
|
|
|
28
40
|
// The card is a fixed skeleton: the markup reserves every slot, and this
|
|
29
41
|
// function only ever overwrites text in place — nothing appears or grows, so
|
|
30
|
-
// the nav below never jumps.
|
|
31
|
-
//
|
|
32
|
-
// live
|
|
33
|
-
const SNAPSHOT_KEY = "
|
|
42
|
+
// the nav below never jumps. After live status identifies the vault, two
|
|
43
|
+
// paints: first from the last visit's matching snapshot, then from the
|
|
44
|
+
// remaining live answers, which also refresh the snapshot.
|
|
45
|
+
const SNAPSHOT_KEY = "sealkeep.railSnapshot";
|
|
46
|
+
let activeVaultId = status?.vaultId || null;
|
|
47
|
+
const authBinding = (() => {
|
|
48
|
+
const value = sessionStorage.getItem("sealkeep.localApiToken");
|
|
49
|
+
if (!value) return null;
|
|
50
|
+
let left = 0x811c9dc5;
|
|
51
|
+
let right = 0x9e3779b9;
|
|
52
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
53
|
+
const code = value.charCodeAt(index);
|
|
54
|
+
left = Math.imul(left ^ code, 0x01000193);
|
|
55
|
+
right = Math.imul(right ^ (code + index), 0x85ebca6b);
|
|
56
|
+
}
|
|
57
|
+
return `${(left >>> 0).toString(36)}.${(right >>> 0).toString(36)}.${value.length}`;
|
|
58
|
+
})();
|
|
59
|
+
const readSnapshot = (expectedVaultId = null) => {
|
|
60
|
+
try {
|
|
61
|
+
const snapshot = JSON.parse(localStorage.getItem(SNAPSHOT_KEY) || sessionStorage.getItem(SNAPSHOT_KEY) || "{}");
|
|
62
|
+
// The same loopback origin can later serve another vault. Refuse a legacy
|
|
63
|
+
// or differently bound snapshot before painting even one account fact;
|
|
64
|
+
// live status will repopulate it for this service.
|
|
65
|
+
return authBinding && expectedVaultId && snapshot.authBinding === authBinding && snapshot.vaultId === expectedVaultId ? snapshot : {};
|
|
66
|
+
}
|
|
67
|
+
catch { return {}; }
|
|
68
|
+
};
|
|
69
|
+
const saveSnapshot = (patch) => {
|
|
70
|
+
if (!isCurrent()) return;
|
|
71
|
+
const vaultId = patch.vaultId || activeVaultId;
|
|
72
|
+
if (!vaultId) return;
|
|
73
|
+
try { localStorage.setItem(SNAPSHOT_KEY, JSON.stringify({ ...readSnapshot(vaultId), ...patch, vaultId })); }
|
|
74
|
+
catch { /* a browser refusing storage costs only the instant repaint */ }
|
|
75
|
+
};
|
|
76
|
+
// The HTML reserves these slots so the navigation never jumps, but a bare
|
|
77
|
+
// dash is not a fact. Start each render with unknown slots hidden; the last
|
|
78
|
+
// verified snapshot (or the live response below) unhides only what it can
|
|
79
|
+
// actually name. Re-renders happen in one JS turn, so a remembered rail does
|
|
80
|
+
// not flash away while it is repainted.
|
|
81
|
+
for (const id of ["acct-plan", "acct-email", "acct-meter", "acct-usage", "acct-phrase", "acct-storage", "acct-disk"]) {
|
|
82
|
+
const node = $id(id);
|
|
83
|
+
// Visibility keeps the skeleton's footprint, so the navigation below does
|
|
84
|
+
// not jump while still ensuring no placeholder is user-visible.
|
|
85
|
+
if (node) node.style.visibility = "hidden";
|
|
86
|
+
}
|
|
34
87
|
const paintIdentity = (snap) => {
|
|
88
|
+
if (!isCurrent()) return;
|
|
35
89
|
const plan = snap.plan;
|
|
36
90
|
const isPro = plan === "pro" || plan === "pro_lite";
|
|
37
91
|
$id("acct-plan").className = `acct-plan${isPro ? " pro" : ""}`;
|
|
38
92
|
$id("acct-plan-name").textContent = plan === "pro" ? "Sealkeep Pro" : plan === "pro_lite" ? "Sealkeep Pro Lite" : plan ? plan.replace(/^./, (c) => c.toUpperCase()) : "Local vault";
|
|
93
|
+
$id("acct-plan").style.visibility = "visible";
|
|
39
94
|
const emailEl = $id("acct-email");
|
|
40
|
-
if (emailEl)
|
|
95
|
+
if (emailEl) {
|
|
96
|
+
emailEl.textContent = snap.email || "no account — this machine only";
|
|
97
|
+
emailEl.style.visibility = "visible";
|
|
98
|
+
}
|
|
41
99
|
if (snap.limit) {
|
|
42
100
|
const used = Number(snap.used) || 0;
|
|
43
101
|
$id("acct-fill").style.width = `${Math.min(100, (used / snap.limit) * 100).toFixed(1)}%`;
|
|
44
102
|
// A remembered answer is labelled as remembered, never passed off as live.
|
|
45
103
|
$id("acct-usage").textContent = `${bytes(used)} of ${bytes(snap.limit)}${snap.cached ? " · last known" : ""}`;
|
|
104
|
+
$id("acct-meter").style.visibility = "visible";
|
|
105
|
+
$id("acct-usage").style.visibility = "visible";
|
|
46
106
|
} else {
|
|
47
107
|
$id("acct-fill").style.width = "0%";
|
|
48
|
-
|
|
108
|
+
// A paid-plan response without quota is incomplete, not a zero or a
|
|
109
|
+
// dash. Local-only is a complete answer and can be stated plainly.
|
|
110
|
+
$id("acct-meter").style.visibility = "hidden";
|
|
111
|
+
$id("acct-usage").style.visibility = snap.plan ? "hidden" : "visible";
|
|
112
|
+
if (!snap.plan) $id("acct-usage").textContent = "nothing stored off this machine";
|
|
49
113
|
}
|
|
50
114
|
if (snap.diskText) railWriteDiskFact(snap.diskText);
|
|
115
|
+
if (typeof snap.phraseHeld === "boolean") {
|
|
116
|
+
$id("acct-phrase").style.visibility = "visible";
|
|
117
|
+
$id("acct-phrase").className = snap.phraseHeld ? "good" : "warn";
|
|
118
|
+
$id("acct-phrase-text").textContent = snap.phraseHeld ? "Seals unattended — phrase stored here" : "Recovery phrase is not stored on this machine";
|
|
119
|
+
}
|
|
120
|
+
if (snap.storageText) {
|
|
121
|
+
$id("acct-storage").style.visibility = "visible";
|
|
122
|
+
$id("acct-storage").className = snap.storageWarn ? "warn" : "good";
|
|
123
|
+
$id("acct-storage-text").textContent = snap.storageText;
|
|
124
|
+
}
|
|
125
|
+
if (typeof snap.escrowVisible === "boolean") {
|
|
126
|
+
const escrow = $id("acct-escrow");
|
|
127
|
+
if (escrow) {
|
|
128
|
+
escrow.hidden = !snap.escrowVisible;
|
|
129
|
+
if (snap.escrowVisible && typeof snap.escrowBackedUp === "boolean") {
|
|
130
|
+
escrow.className = snap.escrowBackedUp ? "good" : "warn";
|
|
131
|
+
$id("acct-escrow-text").innerHTML = snap.escrowBackedUp
|
|
132
|
+
? 'Key backup previously confirmed — <a href="./index.html#recovery-access" class="rail-connect">check access</a>'
|
|
133
|
+
: 'Key backup needs checking — <a href="./index.html#recovery-access" class="rail-connect">check access</a>';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
51
137
|
if (snap.panelUrl && snap.plan) railPlaceDoor(card, snap.panelUrl);
|
|
52
138
|
};
|
|
53
139
|
try {
|
|
54
|
-
const remembered =
|
|
55
|
-
if (remembered) paintIdentity({ ...
|
|
140
|
+
const remembered = readSnapshot(status?.vaultId);
|
|
141
|
+
if (Object.keys(remembered).length > 0) paintIdentity({ ...remembered, cached: true });
|
|
56
142
|
} catch { /* a torn snapshot paints nothing; the live answer is seconds away */ }
|
|
57
143
|
|
|
144
|
+
if (options.cachedOnly) return Promise.resolve();
|
|
145
|
+
|
|
58
146
|
const ready = status ? Promise.resolve(status) : api("/v1/status");
|
|
59
147
|
return ready.then((state) => {
|
|
148
|
+
if (!isCurrent()) return;
|
|
149
|
+
activeVaultId = state.vaultId || null;
|
|
60
150
|
const plan = state.plan;
|
|
61
151
|
const quota = state.quota;
|
|
62
152
|
const snapshot = {
|
|
153
|
+
authBinding,
|
|
154
|
+
vaultId: state.vaultId || null,
|
|
63
155
|
plan,
|
|
64
156
|
email: state.accountEmail || null,
|
|
65
157
|
used: quota ? Number(quota.usedBytes) || 0 : null,
|
|
@@ -68,24 +160,121 @@ function renderAccountRail(api, status) {
|
|
|
68
160
|
panelUrl: state.panelUrl || null
|
|
69
161
|
};
|
|
70
162
|
paintIdentity(snapshot);
|
|
71
|
-
|
|
72
|
-
const prior = JSON.parse(sessionStorage.getItem(SNAPSHOT_KEY) || "{}");
|
|
73
|
-
sessionStorage.setItem(SNAPSHOT_KEY, JSON.stringify({ ...snapshot, diskText: prior.diskText || null }));
|
|
74
|
-
} catch { /* a browser refusing storage costs only the instant repaint */ }
|
|
163
|
+
saveSnapshot(snapshot);
|
|
75
164
|
|
|
76
165
|
// The two facts come from setup state; tolerate its absence rather than
|
|
77
166
|
// blocking the card.
|
|
78
167
|
return api("/v1/setup/state").catch(() => null).then((setup) => {
|
|
79
|
-
|
|
168
|
+
if (!isCurrent()) return;
|
|
169
|
+
if (!setup) {
|
|
170
|
+
// No answer is not "no phrase". Keep the last verified facts painted
|
|
171
|
+
// while this request retries on the next refresh.
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const held = Boolean(setup.vault && setup.vault.phraseRemembered);
|
|
175
|
+
$id("acct-phrase").style.visibility = "visible";
|
|
80
176
|
$id("acct-phrase").className = held ? "good" : "warn";
|
|
81
|
-
$id("acct-phrase-text").innerHTML = held ? "Seals
|
|
177
|
+
$id("acct-phrase-text").innerHTML = held ? "Seals unattended — phrase stored here" : '<b>Can\u2019t seal unattended</b> — <a href="./index.html#settings" class="rail-connect" id="rail-phrase-link">store your recovery phrase</a>';
|
|
178
|
+
const phraseLink = $id("rail-phrase-link");
|
|
179
|
+
if (phraseLink) phraseLink.addEventListener("click", () => {
|
|
180
|
+
// Same-page navigation: place the cursor. Cross-page, the settings
|
|
181
|
+
// loader finds the hash and does its own focusing on arrival.
|
|
182
|
+
setTimeout(() => document.getElementById("f-remember-phrase")?.focus(), 300);
|
|
183
|
+
});
|
|
82
184
|
|
|
185
|
+
// The sealed key backup: a lost laptop is not a lost vault only if a
|
|
186
|
+
// sealed copy of the key lives in the account. Surfaced from setup state,
|
|
187
|
+
// with the fix one click into Settings.
|
|
188
|
+
const escrowLi = $id("acct-escrow");
|
|
189
|
+
const backedUp = typeof setup.vault?.escrowStored === 'boolean' ? setup.vault.escrowStored : null;
|
|
190
|
+
const onAccount = Boolean(plan);
|
|
191
|
+
if (escrowLi) {
|
|
192
|
+
if (!onAccount) { escrowLi.hidden = true; }
|
|
193
|
+
else {
|
|
194
|
+
escrowLi.hidden = false;
|
|
195
|
+
escrowLi.className = backedUp ? "good" : "warn";
|
|
196
|
+
$id("acct-escrow-text").innerHTML = backedUp === true
|
|
197
|
+
? "Key backed up to your account"
|
|
198
|
+
: backedUp === false
|
|
199
|
+
? '<b>No sealed key backup found</b> — <a href="./index.html#recovery-access" class="rail-connect">check recovery options</a>'
|
|
200
|
+
: 'Key backup not verified right now — <a href="./index.html#recovery-access" class="rail-connect">check recovery access</a>';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// (No "setup unfinished" chip: a local-only vault is a finished state by
|
|
204
|
+
// design, and managed sign-in configures storage without a BYO bucket, so
|
|
205
|
+
// storage.configured is false for a fully set-up account — there is no
|
|
206
|
+
// honest rail-wide signal that setup is incomplete. The road back to the
|
|
207
|
+
// wizard lives on the overview metric when a vault truly has no storage.)
|
|
208
|
+
|
|
209
|
+
const NAMES = { s3: "S3-compatible", r2: "Cloudflare R2", b2: "Backblaze B2", gcs: "Google Cloud Storage", gdrive: "Google Drive", vaultline: "Sealkeep Cloud" };
|
|
83
210
|
const provider = (state.remoteStorage && state.remoteStorage.provider) || (setup && setup.storage && setup.storage.provider);
|
|
84
211
|
const label = provider
|
|
85
|
-
?
|
|
212
|
+
? NAMES[provider] || provider
|
|
86
213
|
: (plan ? "Sealkeep managed" : null);
|
|
87
|
-
|
|
88
|
-
|
|
214
|
+
const fallbackStorageText = label ? `Storage: ${label}` : "This disk only";
|
|
215
|
+
const rememberedStorageText = readSnapshot(state.vaultId).storageText;
|
|
216
|
+
// `status` only knows the account's default provider. The routing call
|
|
217
|
+
// below knows the complete list. Keep the previous complete answer on
|
|
218
|
+
// screen instead of briefly downgrading it to "Sealkeep managed".
|
|
219
|
+
if (!rememberedStorageText) {
|
|
220
|
+
$id("acct-storage").style.visibility = "visible";
|
|
221
|
+
$id("acct-storage").className = label ? "good" : "";
|
|
222
|
+
$id("acct-storage-text").textContent = fallbackStorageText;
|
|
223
|
+
}
|
|
224
|
+
saveSnapshot({
|
|
225
|
+
phraseHeld: held,
|
|
226
|
+
escrowVisible: onAccount,
|
|
227
|
+
escrowBackedUp: backedUp,
|
|
228
|
+
...(!rememberedStorageText ? { storageText: fallbackStorageText, storageWarn: false } : {})
|
|
229
|
+
});
|
|
230
|
+
// Every destination, not the first one: a vault routing to managed
|
|
231
|
+
// storage AND Google Drive showed only one, and the other simply did
|
|
232
|
+
// not exist anywhere on the page.
|
|
233
|
+
api("/v1/storage/targets").catch(() => null).then((routed) => {
|
|
234
|
+
if (!isCurrent()) return;
|
|
235
|
+
if (!routed) return;
|
|
236
|
+
const targets = Array.isArray(routed.targets) ? routed.targets : [];
|
|
237
|
+
const finalStorageText = targets.length
|
|
238
|
+
? `Storage: ${targets.map((t) => NAMES[t.provider] || t.provider).join(" + ")}`
|
|
239
|
+
: fallbackStorageText;
|
|
240
|
+
$id("acct-storage").style.visibility = "visible";
|
|
241
|
+
$id("acct-storage").className = targets.length || label ? "good" : "";
|
|
242
|
+
$id("acct-storage-text").textContent = finalStorageText;
|
|
243
|
+
// A destination this machine cannot reach is named as such, with the
|
|
244
|
+
// fix one click away — not discovered at seal time as a routing error.
|
|
245
|
+
// This must cover every provider. Showing Cloud/R2/S3 as green merely
|
|
246
|
+
// because Drive happened to be the first failed row hid broken routes.
|
|
247
|
+
const disconnected = targets.filter((target) => target.connected === false);
|
|
248
|
+
if (disconnected.length === 0) {
|
|
249
|
+
saveSnapshot({ storageText: finalStorageText, storageWarn: false });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const line = $id("acct-storage-text");
|
|
253
|
+
disconnected.forEach((target, index) => {
|
|
254
|
+
line.append(index === 0 ? " · " : " / ");
|
|
255
|
+
const providerName = NAMES[target.provider] || target.provider || "storage";
|
|
256
|
+
const fix = document.createElement("a");
|
|
257
|
+
fix.className = "rail-connect";
|
|
258
|
+
if (target.provider === "gdrive") {
|
|
259
|
+
fix.href = "#";
|
|
260
|
+
fix.textContent = `connect ${providerName}`;
|
|
261
|
+
fix.title = `${providerName} is a destination, but this machine is not signed in. Connect this exact destination to let seals land there.`;
|
|
262
|
+
fix.addEventListener("click", (event) => {
|
|
263
|
+
event.preventDefault();
|
|
264
|
+
fix.textContent = "opening Google…";
|
|
265
|
+
connectGdrive(api, fix, target.id);
|
|
266
|
+
});
|
|
267
|
+
} else {
|
|
268
|
+
fix.href = "./index.html#settings";
|
|
269
|
+
fix.textContent = `reconnect ${providerName}`;
|
|
270
|
+
fix.title = `${providerName} is configured for this account but unavailable on this machine. Open Settings to reconnect it.`;
|
|
271
|
+
}
|
|
272
|
+
line.append(fix);
|
|
273
|
+
});
|
|
274
|
+
$id("acct-storage").className = "warn";
|
|
275
|
+
const unavailable = disconnected.map((target) => NAMES[target.provider] || target.provider || "storage").join(" + ");
|
|
276
|
+
saveSnapshot({ storageText: `${finalStorageText} · ${unavailable} disconnected on this machine`, storageWarn: true });
|
|
277
|
+
});
|
|
89
278
|
|
|
90
279
|
// A paying plan gets its door: the panel where the subscription,
|
|
91
280
|
// machines and stored copies are managed. The slot for it is reserved in
|
|
@@ -98,9 +287,10 @@ function renderAccountRail(api, status) {
|
|
|
98
287
|
// dismissed until the backend publishes a different one (new id).
|
|
99
288
|
if (!state.plan && !document.getElementById("rail-offer")) {
|
|
100
289
|
return api("/v1/offer").catch(() => null).then((body) => {
|
|
290
|
+
if (!isCurrent()) return;
|
|
101
291
|
const offer = body && body.offer;
|
|
102
292
|
if (!offer || !offer.headline) return;
|
|
103
|
-
const dismissed = localStorage.getItem("
|
|
293
|
+
const dismissed = localStorage.getItem("sealkeep.offer.dismissed");
|
|
104
294
|
if (dismissed && String(offer.id) === dismissed) return;
|
|
105
295
|
const box = document.createElement("div");
|
|
106
296
|
box.id = "rail-offer";
|
|
@@ -112,7 +302,7 @@ function renderAccountRail(api, status) {
|
|
|
112
302
|
const go = document.createElement("a");
|
|
113
303
|
go.className = "action";
|
|
114
304
|
go.textContent = offer.cta_label || "Learn more";
|
|
115
|
-
go.href = offer.cta_url || "#";
|
|
305
|
+
go.href = sealkeepCloudLink(offer.cta_url || "#");
|
|
116
306
|
go.target = "_blank";
|
|
117
307
|
go.rel = "noreferrer";
|
|
118
308
|
const close = document.createElement("button");
|
|
@@ -120,20 +310,23 @@ function renderAccountRail(api, status) {
|
|
|
120
310
|
close.className = "rail-offer-close";
|
|
121
311
|
close.textContent = "✕";
|
|
122
312
|
close.title = "Dismiss";
|
|
123
|
-
close.onclick = () => { localStorage.setItem("
|
|
313
|
+
close.onclick = () => { localStorage.setItem("sealkeep.offer.dismissed", String(offer.id)); box.remove(); };
|
|
124
314
|
box.append(close, head, text, go);
|
|
125
315
|
card.after(box);
|
|
126
316
|
});
|
|
127
317
|
}
|
|
128
318
|
}).then(() => {
|
|
319
|
+
if (!isCurrent()) return;
|
|
129
320
|
// Rendered last, and each self-guarded: neither the upload strip nor the
|
|
130
321
|
// disk-footprint fact may cost the card above them a single field.
|
|
131
|
-
railSyncUpload(api, bytes, card, state && state.upload);
|
|
132
|
-
|
|
322
|
+
railSyncUpload(api, bytes, card, state && state.upload, isCurrent);
|
|
323
|
+
if (options.skipDisk) return;
|
|
324
|
+
return railDiskFact(api, bytes, isCurrent);
|
|
133
325
|
});
|
|
134
326
|
}).catch((error) => {
|
|
135
|
-
|
|
136
|
-
//
|
|
327
|
+
if (!isCurrent()) return;
|
|
328
|
+
// The last verified facts stay put on failure. Slots that have never had
|
|
329
|
+
// an answer remain hidden, and the reason is kept for inspection.
|
|
137
330
|
if (typeof window !== "undefined") window.__railError = String((error && error.message) || error);
|
|
138
331
|
});
|
|
139
332
|
}
|
|
@@ -148,7 +341,12 @@ function renderAccountRail(api, status) {
|
|
|
148
341
|
*/
|
|
149
342
|
function railWriteDiskFact(text) {
|
|
150
343
|
const slot = document.getElementById("acct-disk-text");
|
|
151
|
-
if (slot) {
|
|
344
|
+
if (slot) {
|
|
345
|
+
slot.textContent = text;
|
|
346
|
+
const row = document.getElementById("acct-disk");
|
|
347
|
+
if (row) row.style.visibility = "visible";
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
152
350
|
// Older markup without the reserved row: append one, as before.
|
|
153
351
|
const storage = document.getElementById("acct-storage");
|
|
154
352
|
const list = storage && storage.parentElement;
|
|
@@ -174,27 +372,50 @@ function railPlaceDoor(card, panelUrl) {
|
|
|
174
372
|
door.className = "action acct-door";
|
|
175
373
|
door.target = "_blank";
|
|
176
374
|
door.rel = "noreferrer";
|
|
177
|
-
door.textContent = "
|
|
375
|
+
door.textContent = "Account panel ↗";
|
|
178
376
|
const slot = document.getElementById("acct-door-slot");
|
|
179
377
|
if (slot) slot.append(door);
|
|
180
378
|
else if (card && typeof card.append === "function") card.append(door);
|
|
181
379
|
}
|
|
182
|
-
door.href = panelUrl;
|
|
380
|
+
door.href = sealkeepCloudLink(panelUrl);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Only a public loopback address travels to Cloud, never the local bearer.
|
|
384
|
+
function sealkeepCloudLink(value) {
|
|
385
|
+
try {
|
|
386
|
+
const url = new URL(value);
|
|
387
|
+
if (url.origin === 'https://sealkeep.spala.ai' && location.protocol === 'http:' && ['127.0.0.1','localhost','[::1]'].includes(location.hostname)) {
|
|
388
|
+
url.searchParams.set('local_return', `${location.origin}/index.html`);
|
|
389
|
+
}
|
|
390
|
+
return url.href;
|
|
391
|
+
} catch { return value; }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Static account links need the same return path as dynamically rendered ones.
|
|
395
|
+
// Run after the page markup (rail.js is loaded at the end of the document).
|
|
396
|
+
function sealkeepCloudLinks(root = document) {
|
|
397
|
+
for (const link of root.querySelectorAll('a[href]')) {
|
|
398
|
+
link.href = sealkeepCloudLink(link.href);
|
|
399
|
+
}
|
|
183
400
|
}
|
|
184
401
|
|
|
185
402
|
let railDiskFactAsked = false;
|
|
186
|
-
function railDiskFact(api, bytes) {
|
|
187
|
-
if (railDiskFactAsked) return;
|
|
403
|
+
function railDiskFact(api, bytes, isCurrent = () => true) {
|
|
404
|
+
if (!isCurrent() || railDiskFactAsked) return;
|
|
188
405
|
railDiskFactAsked = true;
|
|
189
406
|
return api("/v1/sessions").catch(() => null).then((disk) => {
|
|
407
|
+
if (!isCurrent()) return;
|
|
190
408
|
const total = disk == null ? NaN : Number(disk.totalBytes);
|
|
191
409
|
if (!Number.isFinite(total)) return;
|
|
192
|
-
|
|
410
|
+
// Reworded: "…of sessions on this disk" collided with the Overview
|
|
411
|
+
// headline's "still on this disk", which counts only the UNARCHIVED
|
|
412
|
+
// subset — same words, different numbers, one screen apart.
|
|
413
|
+
const line = `${bytes(total)} across ${(disk.sessions || []).length} sessions found on disk`;
|
|
193
414
|
railWriteDiskFact(line);
|
|
194
415
|
// The slow fact joins the snapshot so the NEXT load paints it instantly.
|
|
195
416
|
try {
|
|
196
|
-
const prior = JSON.parse(sessionStorage.getItem("
|
|
197
|
-
|
|
417
|
+
const prior = JSON.parse(localStorage.getItem("sealkeep.railSnapshot") || sessionStorage.getItem("sealkeep.railSnapshot") || "{}");
|
|
418
|
+
localStorage.setItem("sealkeep.railSnapshot", JSON.stringify({ ...prior, diskText: line }));
|
|
198
419
|
} catch { /* snapshot is a convenience, never a requirement */ }
|
|
199
420
|
});
|
|
200
421
|
}
|
|
@@ -228,8 +449,10 @@ function railDrawUpload(run, bytes) {
|
|
|
228
449
|
if (!railUploadEls) return;
|
|
229
450
|
const total = Number(run.totalBytes) || 0;
|
|
230
451
|
const done = Number(run.doneBytes) || 0;
|
|
452
|
+
const resolved = Array.isArray(run.resolvedAfterRun) ? run.resolvedAfterRun : [];
|
|
453
|
+
const resolvedBytes = resolved.reduce((sum, item) => sum + (Number.isFinite(item?.bytes) ? item.bytes : 0), 0);
|
|
231
454
|
const active = !run.finishedAt;
|
|
232
|
-
const pct = total > 0 ? Math.min(100, (done / total) * 100) : active ? 0 : 100;
|
|
455
|
+
const pct = total > 0 ? Math.min(100, ((active ? done : done + resolvedBytes) / total) * 100) : active ? 0 : 100;
|
|
233
456
|
railUploadEls.fill.style.width = `${pct.toFixed(1)}%`;
|
|
234
457
|
const sent = (run.done || []).length;
|
|
235
458
|
if (active) {
|
|
@@ -239,8 +462,10 @@ function railDrawUpload(run, bytes) {
|
|
|
239
462
|
const failed = run.failed || [];
|
|
240
463
|
const word = run.cancelled ? "Stopped" : "Done";
|
|
241
464
|
railUploadEls.line.textContent = failed.length
|
|
242
|
-
? `${word} — ${sent} sent, ${failed.length} failed — ${failed[0].error}`
|
|
243
|
-
:
|
|
465
|
+
? `${word} — ${sent} sent, ${resolved.length} stored later, ${failed.length} failed — ${failed[0].error}`
|
|
466
|
+
: resolved.length
|
|
467
|
+
? `${word} — ${sent} sent, ${resolved.length} stored later automatically`
|
|
468
|
+
: `${word} — ${sent} sent, ${bytes(Number(run.freedBytes) || 0)} freed locally`;
|
|
244
469
|
}
|
|
245
470
|
}
|
|
246
471
|
|
|
@@ -278,8 +503,9 @@ function railInjectUploadStrip(card) {
|
|
|
278
503
|
railUploadEls = { strip, fill, line, done: false };
|
|
279
504
|
}
|
|
280
505
|
|
|
281
|
-
function railSyncUpload(api, bytes, card, run) {
|
|
506
|
+
function railSyncUpload(api, bytes, card, run, isCurrent = () => true) {
|
|
282
507
|
try {
|
|
508
|
+
if (!isCurrent()) return;
|
|
283
509
|
const active = Boolean(run && !run.finishedAt);
|
|
284
510
|
if (!active) {
|
|
285
511
|
railStopUploadPoll();
|
|
@@ -299,10 +525,61 @@ function railSyncUpload(api, bytes, card, run) {
|
|
|
299
525
|
railUploadEls.done = false;
|
|
300
526
|
railDrawUpload(run, bytes);
|
|
301
527
|
if (railUploadTimer) return;
|
|
302
|
-
|
|
528
|
+
const timer = setInterval(() => {
|
|
529
|
+
if (!isCurrent()) {
|
|
530
|
+
clearInterval(timer);
|
|
531
|
+
if (railUploadTimer === timer) railUploadTimer = null;
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
303
534
|
api("/v1/status")
|
|
304
|
-
.then((fresh) =>
|
|
305
|
-
|
|
535
|
+
.then((fresh) => {
|
|
536
|
+
if (isCurrent()) railSyncUpload(api, bytes, card, fresh && fresh.upload, isCurrent);
|
|
537
|
+
})
|
|
538
|
+
.catch(() => {
|
|
539
|
+
if (!isCurrent()) {
|
|
540
|
+
clearInterval(timer);
|
|
541
|
+
if (railUploadTimer === timer) railUploadTimer = null;
|
|
542
|
+
}
|
|
543
|
+
// A current missed poll is the next poll's problem.
|
|
544
|
+
});
|
|
306
545
|
}, 3000);
|
|
546
|
+
railUploadTimer = timer;
|
|
307
547
|
} catch { /* the strip is a garnish: no failure here may cost the rail */ }
|
|
308
548
|
}
|
|
549
|
+
|
|
550
|
+
// Drive consent, driven from the rail: ask the daemon to start the flow, open
|
|
551
|
+
// Google's consent screen in the user's own tab, then watch until the grant
|
|
552
|
+
// lands in this machine's keystore (or the attempt fails with a reason).
|
|
553
|
+
async function connectGdrive(api, anchor, targetId) {
|
|
554
|
+
try {
|
|
555
|
+
const consentTab = window.open("about:blank", "_blank");
|
|
556
|
+
const started = await api("/v1/storage/targets/gdrive/connect", {
|
|
557
|
+
method: "POST",
|
|
558
|
+
body: JSON.stringify({ targetId })
|
|
559
|
+
}).catch(() => null);
|
|
560
|
+
if (!started || !started.consent_url) {
|
|
561
|
+
consentTab?.close();
|
|
562
|
+
anchor.textContent = (started && started.error) ? `Drive: ${started.error}` : "Drive: could not start";
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (consentTab) consentTab.location = started.consent_url;
|
|
566
|
+
else window.open(started.consent_url, "_blank", "noopener");
|
|
567
|
+
anchor.textContent = "waiting for Google…";
|
|
568
|
+
const until = Date.now() + 5 * 60 * 1000;
|
|
569
|
+
while (Date.now() < until) {
|
|
570
|
+
await new Promise((rest) => setTimeout(rest, 2000));
|
|
571
|
+
const state = await api(`/v1/storage/targets/gdrive/connect?targetId=${encodeURIComponent(targetId)}`).catch(() => null);
|
|
572
|
+
if (state && state.status === "connected") {
|
|
573
|
+
anchor.textContent = "Drive connected ✓";
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (state && state.status === "failed") {
|
|
577
|
+
anchor.textContent = `Drive: ${state.error || "consent failed"}`;
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
anchor.textContent = "Drive: no consent arrived — click to retry";
|
|
582
|
+
} catch (error) {
|
|
583
|
+
anchor.textContent = "Drive: " + (error && error.message ? error.message : "connect failed");
|
|
584
|
+
}
|
|
585
|
+
}
|
package/web/retention.html
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
<body>
|
|
10
10
|
<!-- The rules editor is a dashboard view now (index.html#rules) — one page,
|
|
11
11
|
no reloads between views. This stub keeps old bookmarks and links working,
|
|
12
|
-
preserving a #
|
|
12
|
+
preserving a #ticket=… fragment when one is carried. -->
|
|
13
13
|
<script>
|
|
14
14
|
const h = location.hash.replace(/^#/, "");
|
|
15
|
-
location.replace(h.
|
|
15
|
+
location.replace(new URLSearchParams(h).has("ticket") ? `./index.html#${h}` : "./index.html#rules");
|
|
16
16
|
</script>
|
|
17
17
|
</body>
|
|
18
18
|
</html>
|