sealkeep 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/ARCHITECTURE.md +163 -14
  2. package/CHANGELOG.md +223 -1
  3. package/CONTROL_PLANE.md +2 -2
  4. package/LICENSE +1 -1
  5. package/README.md +108 -22
  6. package/THIRD_PARTY.md +2 -2
  7. package/THREAT_MODEL.md +23 -4
  8. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
  9. package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
  10. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
  11. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
  12. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
  13. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
  14. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
  15. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +4 -4
  16. package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
  17. package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
  18. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
  19. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
  20. package/dist/site/index.html +1808 -1904
  21. package/dist/site/llms.txt +67 -0
  22. package/dist/site/trust/architecture-data-flow.html +53 -0
  23. package/dist/site/trust/audit-roadmap.html +37 -0
  24. package/dist/site/trust/deployment-responsibility.html +11 -0
  25. package/dist/site/trust/dpa-sample.html +30 -0
  26. package/dist/site/trust/release-provenance.html +21 -0
  27. package/dist/site/trust/subprocessors.html +15 -0
  28. package/dist/site/trust/threat-model.html +34 -0
  29. package/dist/site/trust/toms.html +41 -0
  30. package/dist/site/trust-document.css +32 -0
  31. package/dist/site/trust.html +73 -0
  32. package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
  33. package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
  34. package/dist/site/visual/index.html +18 -0
  35. package/dist/site.zip +0 -0
  36. package/dist/src/activity.d.ts +9 -0
  37. package/dist/src/activity.js +90 -1
  38. package/dist/src/adapters.d.ts +175 -5
  39. package/dist/src/adapters.js +961 -73
  40. package/dist/src/agent-context.d.ts +135 -0
  41. package/dist/src/agent-context.js +1059 -0
  42. package/dist/src/archive-copies.d.ts +47 -0
  43. package/dist/src/archive-copies.js +179 -0
  44. package/dist/src/audit.d.ts +1 -1
  45. package/dist/src/audit.js +29 -4
  46. package/dist/src/autopilot.d.ts +52 -7
  47. package/dist/src/autopilot.js +143 -25
  48. package/dist/src/background-bandwidth.d.ts +46 -0
  49. package/dist/src/background-bandwidth.js +301 -0
  50. package/dist/src/background-cpu.d.ts +82 -0
  51. package/dist/src/background-cpu.js +212 -0
  52. package/dist/src/background-worker-error.d.ts +12 -0
  53. package/dist/src/background-worker-error.js +18 -0
  54. package/dist/src/branding.d.ts +24 -1
  55. package/dist/src/branding.js +31 -1
  56. package/dist/src/bridge.d.ts +233 -0
  57. package/dist/src/bridge.js +604 -0
  58. package/dist/src/byte-stream.d.ts +91 -0
  59. package/dist/src/byte-stream.js +385 -0
  60. package/dist/src/chunk-store.d.ts +41 -8
  61. package/dist/src/chunk-store.js +161 -65
  62. package/dist/src/cli.js +1698 -163
  63. package/dist/src/cloud.d.ts +841 -31
  64. package/dist/src/cloud.js +3196 -277
  65. package/dist/src/context-background.d.ts +37 -0
  66. package/dist/src/context-background.js +309 -0
  67. package/dist/src/context-drain-child.d.ts +1 -0
  68. package/dist/src/context-drain-child.js +98 -0
  69. package/dist/src/context-reader.d.ts +118 -0
  70. package/dist/src/context-reader.js +447 -0
  71. package/dist/src/control-plane/auth.d.ts +32 -4
  72. package/dist/src/control-plane/auth.js +85 -24
  73. package/dist/src/control-plane/server.js +19 -6
  74. package/dist/src/control-plane.d.ts +17 -1
  75. package/dist/src/control-plane.js +32 -6
  76. package/dist/src/crypto.d.ts +1 -1
  77. package/dist/src/crypto.js +5 -5
  78. package/dist/src/daemon-lease.d.ts +70 -0
  79. package/dist/src/daemon-lease.js +420 -0
  80. package/dist/src/daemon.d.ts +94 -1
  81. package/dist/src/daemon.js +1082 -105
  82. package/dist/src/darwin-service-policy.d.ts +41 -0
  83. package/dist/src/darwin-service-policy.js +60 -0
  84. package/dist/src/dashboard-cli.js +15 -15
  85. package/dist/src/device-authorization.d.ts +37 -0
  86. package/dist/src/device-authorization.js +199 -0
  87. package/dist/src/device-enrollment.d.ts +91 -0
  88. package/dist/src/device-enrollment.js +349 -0
  89. package/dist/src/disk.d.ts +17 -12
  90. package/dist/src/disk.js +43 -17
  91. package/dist/src/doctor.d.ts +35 -1
  92. package/dist/src/doctor.js +316 -41
  93. package/dist/src/durable-ticket-lock.d.ts +24 -0
  94. package/dist/src/durable-ticket-lock.js +232 -0
  95. package/dist/src/enroll.d.ts +1 -1
  96. package/dist/src/enroll.js +13 -7
  97. package/dist/src/env.d.ts +10 -1
  98. package/dist/src/env.js +11 -3
  99. package/dist/src/errors.d.ts +8 -8
  100. package/dist/src/errors.js +6 -6
  101. package/dist/src/flush.d.ts +12 -0
  102. package/dist/src/flush.js +37 -0
  103. package/dist/src/heartbeat.d.ts +86 -12
  104. package/dist/src/heartbeat.js +415 -29
  105. package/dist/src/index-background-watchdog.d.ts +1 -0
  106. package/dist/src/index-background-watchdog.js +94 -0
  107. package/dist/src/index-background-work.d.ts +21 -0
  108. package/dist/src/index-background-work.js +25 -0
  109. package/dist/src/index-background.d.ts +64 -0
  110. package/dist/src/index-background.js +394 -0
  111. package/dist/src/index-build-child.d.ts +1 -0
  112. package/dist/src/index-build-child.js +109 -0
  113. package/dist/src/index-manifest.d.ts +52 -0
  114. package/dist/src/index-manifest.js +444 -0
  115. package/dist/src/index-publication-proof.d.ts +84 -0
  116. package/dist/src/index-publication-proof.js +380 -0
  117. package/dist/src/index-publication-state.d.ts +149 -0
  118. package/dist/src/index-publication-state.js +696 -0
  119. package/dist/src/index-publication-verifier.d.ts +89 -0
  120. package/dist/src/index-publication-verifier.js +341 -0
  121. package/dist/src/index-publish.d.ts +62 -0
  122. package/dist/src/index-publish.js +540 -0
  123. package/dist/src/index-scratch-cleanup.d.ts +19 -0
  124. package/dist/src/index-scratch-cleanup.js +166 -0
  125. package/dist/src/index-segment-types.d.ts +132 -0
  126. package/dist/src/index-segment-types.js +21 -0
  127. package/dist/src/index-segments.d.ts +9 -0
  128. package/dist/src/index-segments.js +516 -0
  129. package/dist/src/index-store.d.ts +123 -0
  130. package/dist/src/index-store.js +495 -0
  131. package/dist/src/index-sync.d.ts +91 -15
  132. package/dist/src/index-sync.js +286 -53
  133. package/dist/src/index-upgrade-publication.d.ts +30 -0
  134. package/dist/src/index-upgrade-publication.js +179 -0
  135. package/dist/src/integration-manager.d.ts +32 -0
  136. package/dist/src/integration-manager.js +394 -0
  137. package/dist/src/leakscan.js +1 -1
  138. package/dist/src/local-api.d.ts +56 -15
  139. package/dist/src/local-api.js +4979 -541
  140. package/dist/src/machine-settings.d.ts +51 -0
  141. package/dist/src/machine-settings.js +166 -0
  142. package/dist/src/managed-chunks.d.ts +5 -2
  143. package/dist/src/managed-chunks.js +14 -14
  144. package/dist/src/mcp-install.d.ts +11 -9
  145. package/dist/src/mcp-install.js +73 -28
  146. package/dist/src/mcp-workspace.d.ts +18 -0
  147. package/dist/src/mcp-workspace.js +50 -0
  148. package/dist/src/mcp.js +294 -25
  149. package/dist/src/migrate.js +27 -21
  150. package/dist/src/notify.d.ts +1 -1
  151. package/dist/src/notify.js +5 -5
  152. package/dist/src/offload.d.ts +201 -14
  153. package/dist/src/offload.js +1848 -140
  154. package/dist/src/onboarding.d.ts +8 -1
  155. package/dist/src/onboarding.js +4 -4
  156. package/dist/src/packages.d.ts +2 -2
  157. package/dist/src/packages.js +10 -2
  158. package/dist/src/passkey.d.ts +0 -1
  159. package/dist/src/passkey.js +2 -7
  160. package/dist/src/password-lock.d.ts +2 -2
  161. package/dist/src/password-lock.js +6 -6
  162. package/dist/src/paths.d.ts +2 -0
  163. package/dist/src/paths.js +2 -0
  164. package/dist/src/presence.d.ts +86 -0
  165. package/dist/src/presence.js +240 -0
  166. package/dist/src/progress-deadline.d.ts +21 -0
  167. package/dist/src/progress-deadline.js +91 -0
  168. package/dist/src/project-repair.d.ts +55 -0
  169. package/dist/src/project-repair.js +131 -0
  170. package/dist/src/providers/gcs.d.ts +28 -7
  171. package/dist/src/providers/gcs.js +35 -24
  172. package/dist/src/providers/gdrive.d.ts +71 -8
  173. package/dist/src/providers/gdrive.js +223 -50
  174. package/dist/src/providers/index.d.ts +11 -3
  175. package/dist/src/providers/index.js +30 -10
  176. package/dist/src/providers/s3.d.ts +30 -8
  177. package/dist/src/providers/s3.js +41 -30
  178. package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
  179. package/dist/src/providers/safe-storage-fetch.js +72 -0
  180. package/dist/src/queue.d.ts +136 -19
  181. package/dist/src/queue.js +862 -96
  182. package/dist/src/reclaim-transaction.d.ts +156 -0
  183. package/dist/src/reclaim-transaction.js +1027 -0
  184. package/dist/src/recovery-codes.d.ts +32 -0
  185. package/dist/src/recovery-codes.js +338 -0
  186. package/dist/src/recovery.js +12 -9
  187. package/dist/src/rehydrate.d.ts +25 -22
  188. package/dist/src/rehydrate.js +319 -23
  189. package/dist/src/restore.d.ts +57 -4
  190. package/dist/src/restore.js +272 -36
  191. package/dist/src/resume-stub.d.ts +92 -0
  192. package/dist/src/resume-stub.js +417 -0
  193. package/dist/src/retention.d.ts +98 -7
  194. package/dist/src/retention.js +1066 -62
  195. package/dist/src/rotate.js +3 -3
  196. package/dist/src/search.d.ts +566 -8
  197. package/dist/src/search.js +5897 -290
  198. package/dist/src/secrets.d.ts +51 -7
  199. package/dist/src/secrets.js +316 -24
  200. package/dist/src/service.d.ts +49 -11
  201. package/dist/src/service.js +776 -35
  202. package/dist/src/share.js +3 -3
  203. package/dist/src/shared-spaces.d.ts +98 -0
  204. package/dist/src/shared-spaces.js +214 -0
  205. package/dist/src/source-reader.d.ts +73 -0
  206. package/dist/src/source-reader.js +715 -0
  207. package/dist/src/spool.d.ts +1 -1
  208. package/dist/src/spool.js +1 -1
  209. package/dist/src/start-tui.js +2 -1
  210. package/dist/src/start.js +2 -2
  211. package/dist/src/storage-endpoint.d.ts +21 -0
  212. package/dist/src/storage-endpoint.js +122 -0
  213. package/dist/src/storage-setup.js +12 -12
  214. package/dist/src/storage-targets.d.ts +109 -6
  215. package/dist/src/storage-targets.js +975 -67
  216. package/dist/src/stream-to-cloud.d.ts +5 -1
  217. package/dist/src/stream-to-cloud.js +34 -14
  218. package/dist/src/sync-rules.d.ts +31 -6
  219. package/dist/src/sync-rules.js +153 -14
  220. package/dist/src/team-backfill-scheduling.d.ts +8 -0
  221. package/dist/src/team-backfill-scheduling.js +33 -0
  222. package/dist/src/team-backfill.d.ts +116 -0
  223. package/dist/src/team-backfill.js +1429 -0
  224. package/dist/src/team-index-cache.d.ts +16 -0
  225. package/dist/src/team-index-cache.js +152 -0
  226. package/dist/src/team-offboarding.d.ts +38 -0
  227. package/dist/src/team-offboarding.js +1043 -0
  228. package/dist/src/team-presence.d.ts +127 -0
  229. package/dist/src/team-presence.js +904 -0
  230. package/dist/src/team-publication-policy.d.ts +20 -0
  231. package/dist/src/team-publication-policy.js +140 -0
  232. package/dist/src/team-realtime.d.ts +68 -0
  233. package/dist/src/team-realtime.js +816 -0
  234. package/dist/src/team-source-facts-cache.d.ts +23 -0
  235. package/dist/src/team-source-facts-cache.js +255 -0
  236. package/dist/src/trash.d.ts +1 -1
  237. package/dist/src/trash.js +2 -2
  238. package/dist/src/tui.js +11 -12
  239. package/dist/src/types.d.ts +173 -7
  240. package/dist/src/types.js +20 -0
  241. package/dist/src/ui-server.d.ts +163 -35
  242. package/dist/src/ui-server.js +712 -72
  243. package/dist/src/ui.d.ts +1 -2
  244. package/dist/src/ui.js +1 -2
  245. package/dist/src/upload.d.ts +27 -0
  246. package/dist/src/upload.js +383 -43
  247. package/dist/src/vault.d.ts +226 -30
  248. package/dist/src/vault.js +1776 -192
  249. package/dist/src/watcher.d.ts +7 -1
  250. package/dist/src/watcher.js +198 -55
  251. package/dist/src/worker.d.ts +27 -3
  252. package/dist/src/worker.js +274 -55
  253. package/package.json +33 -12
  254. package/scripts/native-reboot-rehearsal.mjs +90 -0
  255. package/web/app.js +6032 -343
  256. package/web/bootstrap.js +17 -0
  257. package/web/index.html +255 -57
  258. package/web/rail.js +317 -40
  259. package/web/retention.html +2 -2
  260. package/web/rules-view.js +188 -16
  261. package/web/sessions-view.js +485 -62
  262. package/web/sessions.html +2 -2
  263. package/web/setup-api.js +152 -29
  264. package/web/setup-logic.js +68 -9
  265. package/web/setup.html +113 -44
  266. package/web/setup.js +604 -71
  267. package/web/style.css +513 -98
  268. package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
  269. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
  270. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
  271. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
  272. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +0 -0
  273. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
  274. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
package/web/rules-view.js CHANGED
@@ -15,6 +15,16 @@
15
15
  const policy = () => document.querySelector('input[name=policy]:checked').value;
16
16
 
17
17
  const MB = 1024 * 1024;
18
+ const COPIES_CACHE_KEY = "sealkeep.cloudCopiesView.v1";
19
+ // A large vault can contain thousands of stored copies. Keep the complete
20
+ // catalog in memory for filtering, but only create the rows a person has
21
+ // asked to see; each row carries controls and listeners, so eagerly painting
22
+ // the whole catalog can otherwise freeze the local dashboard.
23
+ const COPY_PAGE_SIZE = 100;
24
+ let copiesRefreshTimer = null;
25
+ let storedCopies = [];
26
+ let copiesVisibleLimit = COPY_PAGE_SIZE;
27
+ let rulesLoaded = false;
18
28
 
19
29
  const bytes = (value) => {
20
30
  if (!Number.isFinite(value) || value <= 0) return "0 B";
@@ -50,14 +60,14 @@
50
60
  const eligible = evaluation.candidates.filter((candidate) => candidate.eligible).length;
51
61
  $("effect").textContent = eligible === 0
52
62
  ? `Nothing is reclaimable under these rules right now — ${evaluation.candidates.length} archived session${evaluation.candidates.length === 1 ? " is" : "s are"} being tracked.`
53
- : `${bytes(evaluation.reclaimableBytes)} across ${eligible} session${eligible === 1 ? "" : "s"} could be moved to your trash under these rules.`;
63
+ : `${bytes(evaluation.reclaimableBytes)} across ${eligible} session${eligible === 1 ? "" : "s"} could be freed from this machine under these rules.`;
54
64
  }
55
65
 
56
66
  function fill(evaluation) {
57
67
  // Never type over someone's hands. A refresh landing while a retention
58
68
  // number is being edited used to revert it silently, and the next click on
59
69
  // "Save these rules" then saved the old policy — the one that decides when
60
- // original session files may be moved to the trash. loadCopies already
70
+ // original session files may be permanently reclaimed. loadCopies already
61
71
  // guards its one field this way; every field here needs the same.
62
72
  const set = (id, value) => {
63
73
  const node = $(id);
@@ -85,11 +95,31 @@
85
95
  return days === 0 ? "today" : days === 1 ? "1 day" : `${days} days`;
86
96
  };
87
97
 
98
+ const copyProviderName = (provider) => ({
99
+ vaultline: "Sealkeep Cloud",
100
+ gdrive: "Google Drive",
101
+ s3: "S3",
102
+ r2: "Cloudflare R2",
103
+ b2: "Backblaze B2",
104
+ gcs: "Google Cloud Storage"
105
+ })[provider] || provider || "unknown storage";
106
+
107
+ const copySearchText = (copy) => [copy.ref, copy.provider, copyProviderName(copy.provider)]
108
+ .filter(Boolean)
109
+ .join(" ")
110
+ .toLocaleLowerCase();
111
+
88
112
  function copyRow(copy) {
89
113
  const row = document.createElement("tr");
90
114
  const ref = document.createElement("td");
91
115
  ref.className = "path";
92
116
  ref.textContent = copy.ref;
117
+ if (copy.provider) {
118
+ const place = document.createElement("span");
119
+ place.className = "lastmark";
120
+ place.textContent = copyProviderName(copy.provider);
121
+ ref.append(place);
122
+ }
93
123
  if (copy.lastCopy) {
94
124
  const mark = document.createElement("span");
95
125
  mark.className = "lastmark";
@@ -116,6 +146,110 @@
116
146
  return row;
117
147
  }
118
148
 
149
+ function renderCopiesPage({ reset = false } = {}) {
150
+ if (reset) copiesVisibleLimit = COPY_PAGE_SIZE;
151
+ const filter = ($("copies-filter")?.value || "").trim().toLocaleLowerCase();
152
+ const matching = filter
153
+ ? storedCopies.filter((copy) => copySearchText(copy).includes(filter))
154
+ : storedCopies;
155
+ const visible = matching.slice(0, copiesVisibleLimit);
156
+ const body = $("copies-body");
157
+ body.textContent = "";
158
+ for (const copy of visible) body.append(copyRow(copy));
159
+
160
+ const table = $("copies-table");
161
+ const controls = $("copies-controls");
162
+ const count = $("copies-visible");
163
+ const more = $("copies-more");
164
+ controls.hidden = storedCopies.length === 0;
165
+ table.hidden = visible.length === 0;
166
+ if (matching.length === 0 && storedCopies.length > 0) {
167
+ count.textContent = `No stored copies match “${$("copies-filter").value.trim()}”.`;
168
+ } else if (visible.length < matching.length) {
169
+ count.textContent = `Showing ${visible.length.toLocaleString()} of ${matching.length.toLocaleString()} ${filter ? "matching " : ""}copies.`;
170
+ } else {
171
+ count.textContent = `${matching.length.toLocaleString()} ${filter ? "matching " : ""}${matching.length === 1 ? "copy" : "copies"}.`;
172
+ }
173
+ const remaining = Math.max(0, matching.length - visible.length);
174
+ more.hidden = remaining === 0;
175
+ more.textContent = remaining > COPY_PAGE_SIZE
176
+ ? `Show ${COPY_PAGE_SIZE.toLocaleString()} more`
177
+ : `Show remaining ${remaining.toLocaleString()}`;
178
+ }
179
+
180
+ const copyIdentity = (copy) => copy.copyId || `${copy.provider || "unknown"}\0${copy.ref}`;
181
+
182
+ function readCopiesCache(accountScope) {
183
+ if (!accountScope || typeof cacheBindingForToken !== "function") return null;
184
+ try {
185
+ const cached = JSON.parse(localStorage.getItem(COPIES_CACHE_KEY) || "null");
186
+ if (cached?.version !== 1 || cached.authBinding !== cacheBindingForToken(token)
187
+ || cached.accountScope !== accountScope || !Array.isArray(cached.copies)) return null;
188
+ return cached;
189
+ } catch { return null; }
190
+ }
191
+
192
+ function writeCopiesCache(data) {
193
+ const accountScope = data.catalog?.accountScope;
194
+ if (!accountScope || typeof cacheBindingForToken !== "function") return;
195
+ try {
196
+ localStorage.setItem(COPIES_CACHE_KEY, JSON.stringify({
197
+ version: 1,
198
+ authBinding: cacheBindingForToken(token),
199
+ accountScope,
200
+ copies: data.copies,
201
+ mode: data.mode,
202
+ rollingCapGb: data.rollingCapGb,
203
+ loadedAt: new Date().toISOString(),
204
+ }));
205
+ } catch {
206
+ // The live view remains correct when browser storage is disabled/full;
207
+ // only the warm last-complete fallback is unavailable on the next poll.
208
+ }
209
+ }
210
+
211
+ function mergeWithLastComplete(data) {
212
+ if (data.catalog?.complete) {
213
+ writeCopiesCache(data);
214
+ return data;
215
+ }
216
+ const cached = readCopiesCache(data.catalog?.accountScope);
217
+ if (!cached) return data;
218
+ const copies = [...data.copies];
219
+ const seen = new Set(copies.map(copyIdentity));
220
+ for (const copy of cached.copies) {
221
+ const identity = copyIdentity(copy);
222
+ if (!seen.has(identity)) { seen.add(identity); copies.push(copy); }
223
+ }
224
+ const providers = new Set(copies.map((copy) => copy.provider).filter(Boolean));
225
+ const mode = providers.size > 1
226
+ ? "mixed"
227
+ : providers.has("vaultline")
228
+ ? "managed"
229
+ : providers.size === 1 ? "own" : (data.mode === "none" ? cached.mode : data.mode);
230
+ return {
231
+ ...data,
232
+ copies,
233
+ mode,
234
+ catalog: { ...data.catalog, stale: true },
235
+ };
236
+ }
237
+
238
+ function scheduleCopiesRefresh() {
239
+ if (copiesRefreshTimer) clearTimeout(copiesRefreshTimer);
240
+ copiesRefreshTimer = setTimeout(() => {
241
+ copiesRefreshTimer = null;
242
+ // Re-entering the view calls loadCopies itself. Do not keep polling a
243
+ // hidden settings surface after the person has moved elsewhere.
244
+ if (!$('view-rules')?.hidden) void loadCopies();
245
+ }, 3200);
246
+ }
247
+
248
+ function copiesSummary(copies) {
249
+ const total = copies.reduce((sum, copy) => sum + copy.bytes, 0);
250
+ return `${copies.length} verified ${copies.length === 1 ? "copy" : "copies"} · ${bytes(total)}`;
251
+ }
252
+
119
253
  async function deleteCopy(copy, button, message) {
120
254
  // The design system's destructive pattern: the first click arms, the second
121
255
  // acts, and an armed button left alone stands down after four seconds so
@@ -133,7 +267,10 @@
133
267
  button.disabled = true;
134
268
  message.textContent = "";
135
269
  try {
136
- await api(`/v1/cloud/copies/${encodeURIComponent(copy.ref)}`, {
270
+ const endpoint = copy.archiveId && copy.copyId
271
+ ? `/v1/archives/${encodeURIComponent(copy.archiveId)}/copies/${encodeURIComponent(copy.copyId)}`
272
+ : `/v1/cloud/copies/${encodeURIComponent(copy.ref)}`;
273
+ await api(endpoint, {
137
274
  method: "DELETE",
138
275
  body: JSON.stringify({ confirmLastCopy: button.dataset.last === "yes" })
139
276
  });
@@ -161,29 +298,52 @@
161
298
  $("copies-line").textContent = token
162
299
  ? `Could not read your stored copies: ${error.message}`
163
300
  : "Connect the dashboard to see what is stored off this machine.";
301
+ scheduleCopiesRefresh();
164
302
  return;
165
303
  }
166
- const { copies, mode } = data;
304
+ // Old servers exposed in-flight rows here. Be defensive during upgrades:
305
+ // only provider-verified durable rows can count as stored copies or bytes.
306
+ data = {
307
+ ...data,
308
+ copies: Array.isArray(data.copies) ? data.copies.filter((copy) => copy?.state === "durable") : [],
309
+ };
310
+ data = mergeWithLastComplete(data);
311
+ const { copies, mode, catalog = { complete: true, status: "complete", updating: false, stale: false, error: null } } = data;
167
312
  if (mode === "none") {
313
+ if (copiesRefreshTimer) { clearTimeout(copiesRefreshTimer); copiesRefreshTimer = null; }
168
314
  $("copies-line").textContent = "No storage is connected to this vault, so nothing is stored off this machine. Everything you have archived lives here only.";
169
- $("copies-table").hidden = true;
315
+ storedCopies = [];
316
+ renderCopiesPage({ reset: true });
170
317
  return;
171
318
  }
172
- const total = copies.reduce((sum, copy) => sum + copy.bytes, 0);
173
- $("copies-line").textContent = copies.length === 0
174
- ? "Nothing is stored off this machine yet."
175
- : `${copies.length} ${copies.length === 1 ? "copy" : "copies"} · ${bytes(total)} in your storage.`;
176
- const body = $("copies-body");
177
- body.textContent = "";
178
- for (const copy of copies) body.append(copyRow(copy));
179
- $("copies-table").hidden = copies.length === 0;
319
+ if (!catalog.complete) {
320
+ const visible = copies.length ? `${copiesSummary(copies)} visible` : "No verified copy total is available yet";
321
+ const kept = catalog.stale ? "Showing the last complete view." : "This is not the complete storage total.";
322
+ if (catalog.status === "error") {
323
+ $("copies-line").textContent = `${visible}. ${kept} Could not refresh Sealkeep Cloud${catalog.error ? `: ${catalog.error}.` : "."} Retrying automatically.`;
324
+ } else if (catalog.status === "incomplete") {
325
+ $("copies-line").textContent = `${visible}. ${kept} Sealkeep Cloud returned only part of the catalog; retrying automatically.`;
326
+ } else {
327
+ $("copies-line").textContent = `${visible}. ${kept} Updating Sealkeep Cloud copies…`;
328
+ }
329
+ scheduleCopiesRefresh();
330
+ } else {
331
+ if (copiesRefreshTimer) { clearTimeout(copiesRefreshTimer); copiesRefreshTimer = null; }
332
+ $("copies-line").textContent = copies.length === 0
333
+ ? "Nothing is stored off this machine yet."
334
+ : `${copies.length} ${copies.length === 1 ? "copy" : "copies"} · ${bytes(copies.reduce((sum, copy) => sum + copy.bytes, 0))} in your storage.`;
335
+ }
336
+ storedCopies = copies;
337
+ renderCopiesPage();
180
338
  if ($("cap") !== document.activeElement) $("cap").value = data.rollingCapGb == null ? "" : data.rollingCapGb;
181
339
  }
182
340
 
183
341
  function loadRules() {
342
+ $('save').disabled = !rulesLoaded;
343
+ if (!rulesLoaded) $('effect').textContent = 'Loading your saved cleanup rules. Nothing can be saved until they arrive.';
184
344
  // Show what is actually configured rather than the markup's defaults, so
185
345
  // this view never quietly disagrees with the vault it is editing.
186
- api("/v1/retention").then(fill).catch((error) => {
346
+ api("/v1/retention").then((data) => { fill(data); rulesLoaded = true; $('save').disabled = false; }).catch((error) => {
187
347
  $("effect").textContent = token
188
348
  ? `Could not read the current rules: ${error.message}`
189
349
  : "Connect the dashboard so it can read this vault's current rules.";
@@ -191,7 +351,14 @@
191
351
  }
192
352
 
193
353
  function wire() {
354
+ $("copies-filter").addEventListener("input", () => renderCopiesPage({ reset: true }));
355
+ $("copies-more").addEventListener("click", () => {
356
+ copiesVisibleLimit += COPY_PAGE_SIZE;
357
+ renderCopiesPage();
358
+ });
359
+
194
360
  $("save").onclick = async () => {
361
+ if (!rulesLoaded) { $('saved').textContent = 'Saved rules have not loaded. Refresh before making changes.'; return; }
195
362
  $("save").disabled = true;
196
363
  $("saved").textContent = "Saving…";
197
364
  try {
@@ -210,7 +377,12 @@
210
377
  })
211
378
  });
212
379
  showEffect(evaluation);
213
- $("saved").textContent = "Saved. The background service uses these from its next pass.";
380
+ const canReclaim = evaluation.policy === "archive-and-reclaim" || evaluation.policy === "manual-approval";
381
+ $("saved").textContent = !canReclaim
382
+ ? "Saved. Automatic reclaim is off; original sessions stay on this machine."
383
+ : evaluation.automaticReclaimEnabled
384
+ ? "Saved. This machine uses these rules from its next background pass."
385
+ : "Saved. These rules describe what is eligible, but automatic reclaim remains off on this machine. Turn it on in Settings when you are ready.";
214
386
  } catch (error) {
215
387
  $("saved").textContent = `Not saved: ${error.message}`;
216
388
  } finally {
@@ -223,7 +395,7 @@
223
395
 
224
396
  // The rolling cap is a Pro setting: the account is what enforces it, so a
225
397
  // vault without that account has nowhere real to write it.
226
- api("/v1/status").then((status) => { if (status.plan === "pro") $("rolling").hidden = false; }).catch(() => {});
398
+ $("rolling").hidden = true; // Account-wide storage caps are managed online.
227
399
 
228
400
  $("cap-save").onclick = async () => {
229
401
  const raw = $("cap").value.trim();