@camstack/system 1.2.163 → 1.2.165

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 (66) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  14. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  15. package/dist/builtins/device-manager/device-manager.addon.js +17 -5
  16. package/dist/builtins/device-manager/device-manager.addon.mjs +17 -5
  17. package/dist/builtins/device-manager/device-projection.d.ts +12 -2
  18. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  19. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  20. package/dist/builtins/hub-forwarder/index.js +1 -1
  21. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  22. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  23. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  24. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  25. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  26. package/dist/builtins/local-network/local-network.addon.js +1 -1
  27. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  28. package/dist/builtins/loki-logging/index.js +1 -1
  29. package/dist/builtins/loki-logging/index.mjs +1 -1
  30. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  31. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  32. package/dist/builtins/platform-probe/index.js +1 -1
  33. package/dist/builtins/platform-probe/index.mjs +1 -1
  34. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  35. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  36. package/dist/builtins/snapshot/index.js +1 -1
  37. package/dist/builtins/snapshot/index.mjs +1 -1
  38. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  39. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  40. package/dist/builtins/sqlite-storage/index.d.ts +1 -1
  41. package/dist/builtins/sqlite-storage/index.js +2 -0
  42. package/dist/builtins/sqlite-storage/index.mjs +2 -2
  43. package/dist/builtins/sqlite-storage/query-profiler.d.ts +21 -0
  44. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  45. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  46. package/dist/builtins/storage-orchestrator/storage-migration-coordinator.d.ts +1 -18
  47. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +43 -75
  48. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +43 -75
  49. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +6 -7
  50. package/dist/builtins/system-config/system-config.addon.js +1 -1
  51. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  52. package/dist/builtins/winston-logging/index.js +1 -1
  53. package/dist/builtins/winston-logging/index.mjs +1 -1
  54. package/dist/{dist-B29Skpzo.js → dist-BhE8zNfY.js} +72 -0
  55. package/dist/{dist-DxFjna2a.mjs → dist-BpsHt11f.mjs} +72 -0
  56. package/dist/index.js +2 -2
  57. package/dist/index.mjs +2 -2
  58. package/dist/kernel/deps/manifest-system-deps.d.ts +5 -2
  59. package/dist/kernel/deps/system-package-allowlist.d.ts +12 -0
  60. package/dist/kernel/deps/system-package-broker.d.ts +31 -0
  61. package/dist/kernel/deps/system-package-client.d.ts +9 -0
  62. package/dist/{manifest-system-deps-CG9ESHhv.mjs → manifest-system-deps-D2GebQTp.mjs} +142 -6
  63. package/dist/{manifest-system-deps-DBje540e.js → manifest-system-deps-uflHvHDB.js} +140 -5
  64. package/dist/{retired-settings-keys-CRL0qOnU.js → retired-settings-keys-CqDbI-vK.js} +1 -1
  65. package/dist/{retired-settings-keys-N0NZzDE3.mjs → retired-settings-keys-DLwieFXR.mjs} +1 -1
  66. package/package.json +1 -1
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-B29Skpzo.js");
6
+ const require_dist = require("../../dist-BhE8zNfY.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -237,39 +237,15 @@ var STORAGE_CLASSES = [
237
237
  "recordings",
238
238
  "recordingsLow",
239
239
  "eventMedia",
240
- "backups",
241
- "galleryMedia"
240
+ "backups"
242
241
  ];
243
242
  /** The classes footage/media movers actually own. Everything else is planned,
244
243
  * reported and refused rather than silently accepted. */
245
244
  var MOVER_CLASSES = [
246
245
  "recordings",
247
246
  "recordingsLow",
248
- "eventMedia",
249
- "galleryMedia"
247
+ "eventMedia"
250
248
  ];
251
- /**
252
- * Classes that can never span two locations at once.
253
- *
254
- * `galleryMedia` is `cardinality: 'single'` and `upsertLocation` refuses a
255
- * second instance of a single type, so the "both locations are first-class for
256
- * reads while the past drains" transient the non-blocking mode rests on is not
257
- * merely undesirable there — it is not constructible. Its move is therefore
258
- * stop-the-world, always. That is acceptable only because the enrolled gallery
259
- * is a few KB per sample, and it is said here rather than discovered.
260
- */
261
- var BLOCKING_ONLY_CLASSES = ["galleryMedia"];
262
- /**
263
- * Location types whose volume `galleryMedia` must NEVER land on.
264
- *
265
- * Verbatim from its own declaration: *"Deliberately NOT on the recordings
266
- * volume: these images survive every retention sweep, so they must also survive
267
- * a footage wipe, a quota eviction and a recordings disk swap."* A planner that
268
- * accepted `galleryMedia` → the recordings volume would let an operator destroy
269
- * the enrolment gallery on the next disk swap, months later, with no visible
270
- * link back to this decision.
271
- */
272
- var GALLERY_FORBIDDEN_NEIGHBOUR_TYPES = ["recordings", "recordingsLow"];
273
249
  /** The two single-flight mover engines. Lanes run in parallel; within a lane,
274
250
  * classes run FIFO because the engine refuses a second concurrent job. */
275
251
  var MOVE_LANES = ["media", "footage"];
@@ -309,6 +285,7 @@ var StorageMigrationCoordinator = class {
309
285
  this.deps = deps;
310
286
  }
311
287
  async plan(input) {
288
+ refuseRetiredGalleryClass(input.destinations);
312
289
  const mode = input.mode ?? "blocking";
313
290
  const moves = [];
314
291
  const findings = [];
@@ -322,8 +299,6 @@ var StorageMigrationCoordinator = class {
322
299
  if (target.type !== storageClass) throw new Error(`Storage location "${targetId}" is type "${target.type}", expected "${storageClass}"`);
323
300
  if (source.id === target.id) throw new Error(`Storage location "${targetId}" is already the "${storageClass}" default`);
324
301
  if (!MOVER_CLASSES.includes(storageClass)) throw new Error(`No mover owns "${storageClass}" — the migration can repoint its default but cannot move its bytes. Move it by hand, then repoint the default with upsertLocation.`);
325
- if (mode === "nonBlocking" && BLOCKING_ONLY_CLASSES.includes(storageClass)) throw new Error(`"${storageClass}" is a single-cardinality storage class: it can never exist at two locations at once, so it cannot be repointed first and drained behind. Use mode "blocking".`);
326
- await this.refuseGalleryOntoRecordingsVolume(storageClass, targetId);
327
302
  moves.push({
328
303
  storageClass,
329
304
  fromLocationId: source.id,
@@ -333,11 +308,6 @@ var StorageMigrationCoordinator = class {
333
308
  error: null,
334
309
  progress: null
335
310
  });
336
- if (BLOCKING_ONLY_CLASSES.includes(storageClass)) findings.push({
337
- code: "blockingOnly",
338
- storageClass,
339
- message: `"${storageClass}" is single-cardinality: this move is stop-the-world for its whole duration, in every mode. It is a few KB per enrolled sample, which is why that is acceptable.`
340
- });
341
311
  findings.push(...await this.deviceFindings(storageClass, source.id, target.id));
342
312
  }
343
313
  if (moves.length === 0) throw new Error("select at least one storage class");
@@ -372,29 +342,6 @@ var StorageMigrationCoordinator = class {
372
342
  findings
373
343
  };
374
344
  }
375
- /**
376
- * HARD refusal: `galleryMedia` onto a volume a recordings class lives on.
377
- *
378
- * This is the one same-device case that is a refusal rather than a finding,
379
- * because its consequence is silent and arrives months later — the gallery is
380
- * wiped by the next footage wipe, quota eviction or recordings disk swap, and
381
- * nothing at that moment points back at this migration.
382
- *
383
- * It can only refuse what it can SEE. `deviceKeyOf` is a realpath, so it
384
- * catches a symlink, a bind mount and a literally-shared directory — and
385
- * misses two distinct directories on one filesystem, and any location pinned
386
- * to another node. Those come back `null`/distinct and produce a
387
- * `deviceIdentityUnknown` finding instead.
388
- */
389
- async refuseGalleryOntoRecordingsVolume(storageClass, targetId) {
390
- if (storageClass !== "galleryMedia") return;
391
- const targetKey = await this.deviceKey(targetId);
392
- if (targetKey === null) return;
393
- for (const type of GALLERY_FORBIDDEN_NEIGHBOUR_TYPES) for (const neighbour of this.deps.locations.listLocations({ type })) {
394
- if (await this.deviceKey(neighbour.id) !== targetKey) continue;
395
- throw new Error(`Storage location "${targetId}" shares a volume with "${neighbour.id}" (${type}). galleryMedia is deliberately NOT on the recordings volume: enrolment images survive every retention sweep, so they must also survive a footage wipe, a quota eviction and a recordings disk swap. Choose a destination on another volume.`);
396
- }
397
- }
398
345
  /** Same-device observations that are reported, not refused. */
399
346
  async deviceFindings(storageClass, sourceId, targetId) {
400
347
  if (this.deps.deviceKeyOf === void 0) return [];
@@ -526,7 +473,7 @@ var StorageMigrationCoordinator = class {
526
473
  if (laneOf(storageClass) === "media") {
527
474
  const count = await unanswerable(this.deps.participants.analytics.residue({
528
475
  toLocationId: target.id,
529
- mode: storageClass === "galleryMedia" ? "gallery" : "move"
476
+ mode: "move"
530
477
  }));
531
478
  if (count !== null && count.rows === 0) continue;
532
479
  out.push({
@@ -805,9 +752,7 @@ var StorageMigrationCoordinator = class {
805
752
  *
806
753
  * Two lanes — the recorder's footage engine and post-analysis's media engine
807
754
  * — run in parallel with each other and strictly FIFO within themselves.
808
- * `galleryMedia` shares the media lane with `eventMedia` because it shares
809
- * the same `MediaRelocateEngine`, which refuses a second running job; starting
810
- * both would fail the second one instantly.
755
+ * `eventMedia` is the only class on the media lane.
811
756
  */
812
757
  async startOrResumeMoves(job) {
813
758
  if (job.cancelRequested) return;
@@ -937,7 +882,7 @@ var StorageMigrationCoordinator = class {
937
882
  const input = {
938
883
  toLocationId: move.toLocationId,
939
884
  throttleMbps: job.throttleMbps,
940
- mode: move.storageClass === "galleryMedia" ? "gallery" : "move"
885
+ mode: "move"
941
886
  };
942
887
  return (unleased ? await this.deps.participants.analytics.startDrain(input) : await this.deps.participants.analytics.startMove({
943
888
  ...input,
@@ -1011,13 +956,18 @@ function isTerminal(job) {
1011
956
  }
1012
957
  /**
1013
958
  * Which single-flight engine owns a class.
1014
- *
1015
- * `galleryMedia` is in the media lane not because it is event media — it
1016
- * explicitly is not (D197) — but because it is moved by the same
1017
- * `MediaRelocateEngine`, which refuses a second running job.
1018
959
  */
1019
960
  function laneOf(storageClass) {
1020
- return storageClass === "eventMedia" || storageClass === "galleryMedia" ? "media" : "footage";
961
+ return storageClass === "eventMedia" ? "media" : "footage";
962
+ }
963
+ /**
964
+ * The enrolled gallery is the analytics addon's data dir, not a storage class
965
+ * an operator migrates. A leftover destination from an older client is refused
966
+ * rather than silently dropped.
967
+ */
968
+ function refuseRetiredGalleryClass(destinations) {
969
+ if (destinations.galleryMedia === void 0) return;
970
+ throw new Error("galleryMedia is no longer a storage class. Enrolled gallery lives in the pipeline-analytics addon data directory, not a location the operator places.");
1021
971
  }
1022
972
  /**
1023
973
  * True when this phase runs with every writer already back up, so no
@@ -1472,14 +1422,13 @@ var StorageOrchestratorService = class {
1472
1422
  /**
1473
1423
  * Shout about a persisted row that is BOTH the type default and disabled.
1474
1424
  *
1475
- * `upsertLocation` cannot produce this (`resolveEnabled` force-enables a
1476
- * default) and neither can `setDefaultLocations` but hydrate writes rows
1477
- * straight into the map without either, so a hand-edited store, a partial
1478
- * migration or a row written by another build can. It matters because every
1479
- * bare-type ref resolves through {@link getDefaultLocation}: on such a row
1425
+ * `upsertLocation` can now produce this (enabled is the write set; `isDefault`
1426
+ * is store residue). Hydrate also writes rows straight into the map, so a
1427
+ * hand-edited store or a row written by another build can too. It matters
1428
+ * because every bare-type ref still resolves through {@link getDefaultLocation}
1429
+ * until Stage 5 of the enabled-write-set design: on such a row
1480
1430
  * `storage.write({ location: 'eventMedia' })` and the migration coordinator's
1481
- * drain destination both point at a disk the operator turned off. That is the
1482
- * one thing this model must never do silently.
1431
+ * drain destination both point at a disk the operator turned off.
1483
1432
  *
1484
1433
  * Reported, never repaired: flipping an operator's flag back on at boot would
1485
1434
  * be the system overruling the switch instead of obeying it. The coordinator
@@ -1763,6 +1712,7 @@ var StorageOrchestratorService = class {
1763
1712
  createdAt: existing?.createdAt ?? now,
1764
1713
  updatedAt: now
1765
1714
  };
1715
+ refuseLastWriteTargetDisabled(existing, next, this.locations.values());
1766
1716
  const demoted = [];
1767
1717
  if (next.isDefault) {
1768
1718
  for (const [otherId, otherLoc] of this.locations) if (otherLoc.type === next.type && otherLoc.id !== next.id && otherLoc.isDefault) {
@@ -2061,9 +2011,27 @@ function resolveEnabled(input, existing, typeAlreadyHasLocation) {
2061
2011
  if (existing) return {};
2062
2012
  return { enabled: input.isDefault || !typeAlreadyHasLocation };
2063
2013
  }
2064
- if (input.isDefault && requested === false) return { enabled: true };
2065
2014
  return { enabled: requested };
2066
2015
  }
2016
+ /** Types that may have zero enabled locations. Backups with none is sad, not illegal. */
2017
+ var OPTIONAL_WRITE_TYPES = new Set(["backups"]);
2018
+ /**
2019
+ * Refuse an UPDATE that would leave a write-serving type with no enabled
2020
+ * location. Creates are exempt (an explicit `enabled: false` on the first
2021
+ * location is already a named test). `isDefault` is not consulted — enabled
2022
+ * is the write set.
2023
+ */
2024
+ function refuseLastWriteTargetDisabled(existing, next, locations) {
2025
+ if (OPTIONAL_WRITE_TYPES.has(next.type)) return;
2026
+ if (next.enabled !== false) return;
2027
+ if (existing === void 0) return;
2028
+ if (existing.enabled === false) return;
2029
+ for (const loc of locations) {
2030
+ if (loc.id === next.id || loc.type !== next.type) continue;
2031
+ if (loc.enabled !== false) return;
2032
+ }
2033
+ throw new Error(`Cannot disable "${next.id}": it is the last enabled location of type "${next.type}". Enable another location of this type first.`);
2034
+ }
2067
2035
  //#endregion
2068
2036
  //#region src/builtins/storage-orchestrator/storage-pressure-manager.ts
2069
2037
  var DEFAULT_MAX_ROUNDS = 8;
@@ -1,4 +1,4 @@
1
- import { Mt as BaseAddon, N as collectSecretConfigKeys, Qt as parseJsonObject, S as StorageMigrationJobSchema, St as settingsStoreCapability, Tt as storageMigrationCapability, m as REDACTED_SECRET, wt as storageCapability, x as StorageLocationTypeSchema } from "../../dist-DxFjna2a.mjs";
1
+ import { Mt as BaseAddon, N as collectSecretConfigKeys, Qt as parseJsonObject, S as StorageMigrationJobSchema, St as settingsStoreCapability, Tt as storageMigrationCapability, m as REDACTED_SECRET, wt as storageCapability, x as StorageLocationTypeSchema } from "../../dist-BpsHt11f.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -230,39 +230,15 @@ var STORAGE_CLASSES = [
230
230
  "recordings",
231
231
  "recordingsLow",
232
232
  "eventMedia",
233
- "backups",
234
- "galleryMedia"
233
+ "backups"
235
234
  ];
236
235
  /** The classes footage/media movers actually own. Everything else is planned,
237
236
  * reported and refused rather than silently accepted. */
238
237
  var MOVER_CLASSES = [
239
238
  "recordings",
240
239
  "recordingsLow",
241
- "eventMedia",
242
- "galleryMedia"
240
+ "eventMedia"
243
241
  ];
244
- /**
245
- * Classes that can never span two locations at once.
246
- *
247
- * `galleryMedia` is `cardinality: 'single'` and `upsertLocation` refuses a
248
- * second instance of a single type, so the "both locations are first-class for
249
- * reads while the past drains" transient the non-blocking mode rests on is not
250
- * merely undesirable there — it is not constructible. Its move is therefore
251
- * stop-the-world, always. That is acceptable only because the enrolled gallery
252
- * is a few KB per sample, and it is said here rather than discovered.
253
- */
254
- var BLOCKING_ONLY_CLASSES = ["galleryMedia"];
255
- /**
256
- * Location types whose volume `galleryMedia` must NEVER land on.
257
- *
258
- * Verbatim from its own declaration: *"Deliberately NOT on the recordings
259
- * volume: these images survive every retention sweep, so they must also survive
260
- * a footage wipe, a quota eviction and a recordings disk swap."* A planner that
261
- * accepted `galleryMedia` → the recordings volume would let an operator destroy
262
- * the enrolment gallery on the next disk swap, months later, with no visible
263
- * link back to this decision.
264
- */
265
- var GALLERY_FORBIDDEN_NEIGHBOUR_TYPES = ["recordings", "recordingsLow"];
266
242
  /** The two single-flight mover engines. Lanes run in parallel; within a lane,
267
243
  * classes run FIFO because the engine refuses a second concurrent job. */
268
244
  var MOVE_LANES = ["media", "footage"];
@@ -302,6 +278,7 @@ var StorageMigrationCoordinator = class {
302
278
  this.deps = deps;
303
279
  }
304
280
  async plan(input) {
281
+ refuseRetiredGalleryClass(input.destinations);
305
282
  const mode = input.mode ?? "blocking";
306
283
  const moves = [];
307
284
  const findings = [];
@@ -315,8 +292,6 @@ var StorageMigrationCoordinator = class {
315
292
  if (target.type !== storageClass) throw new Error(`Storage location "${targetId}" is type "${target.type}", expected "${storageClass}"`);
316
293
  if (source.id === target.id) throw new Error(`Storage location "${targetId}" is already the "${storageClass}" default`);
317
294
  if (!MOVER_CLASSES.includes(storageClass)) throw new Error(`No mover owns "${storageClass}" — the migration can repoint its default but cannot move its bytes. Move it by hand, then repoint the default with upsertLocation.`);
318
- if (mode === "nonBlocking" && BLOCKING_ONLY_CLASSES.includes(storageClass)) throw new Error(`"${storageClass}" is a single-cardinality storage class: it can never exist at two locations at once, so it cannot be repointed first and drained behind. Use mode "blocking".`);
319
- await this.refuseGalleryOntoRecordingsVolume(storageClass, targetId);
320
295
  moves.push({
321
296
  storageClass,
322
297
  fromLocationId: source.id,
@@ -326,11 +301,6 @@ var StorageMigrationCoordinator = class {
326
301
  error: null,
327
302
  progress: null
328
303
  });
329
- if (BLOCKING_ONLY_CLASSES.includes(storageClass)) findings.push({
330
- code: "blockingOnly",
331
- storageClass,
332
- message: `"${storageClass}" is single-cardinality: this move is stop-the-world for its whole duration, in every mode. It is a few KB per enrolled sample, which is why that is acceptable.`
333
- });
334
304
  findings.push(...await this.deviceFindings(storageClass, source.id, target.id));
335
305
  }
336
306
  if (moves.length === 0) throw new Error("select at least one storage class");
@@ -365,29 +335,6 @@ var StorageMigrationCoordinator = class {
365
335
  findings
366
336
  };
367
337
  }
368
- /**
369
- * HARD refusal: `galleryMedia` onto a volume a recordings class lives on.
370
- *
371
- * This is the one same-device case that is a refusal rather than a finding,
372
- * because its consequence is silent and arrives months later — the gallery is
373
- * wiped by the next footage wipe, quota eviction or recordings disk swap, and
374
- * nothing at that moment points back at this migration.
375
- *
376
- * It can only refuse what it can SEE. `deviceKeyOf` is a realpath, so it
377
- * catches a symlink, a bind mount and a literally-shared directory — and
378
- * misses two distinct directories on one filesystem, and any location pinned
379
- * to another node. Those come back `null`/distinct and produce a
380
- * `deviceIdentityUnknown` finding instead.
381
- */
382
- async refuseGalleryOntoRecordingsVolume(storageClass, targetId) {
383
- if (storageClass !== "galleryMedia") return;
384
- const targetKey = await this.deviceKey(targetId);
385
- if (targetKey === null) return;
386
- for (const type of GALLERY_FORBIDDEN_NEIGHBOUR_TYPES) for (const neighbour of this.deps.locations.listLocations({ type })) {
387
- if (await this.deviceKey(neighbour.id) !== targetKey) continue;
388
- throw new Error(`Storage location "${targetId}" shares a volume with "${neighbour.id}" (${type}). galleryMedia is deliberately NOT on the recordings volume: enrolment images survive every retention sweep, so they must also survive a footage wipe, a quota eviction and a recordings disk swap. Choose a destination on another volume.`);
389
- }
390
- }
391
338
  /** Same-device observations that are reported, not refused. */
392
339
  async deviceFindings(storageClass, sourceId, targetId) {
393
340
  if (this.deps.deviceKeyOf === void 0) return [];
@@ -519,7 +466,7 @@ var StorageMigrationCoordinator = class {
519
466
  if (laneOf(storageClass) === "media") {
520
467
  const count = await unanswerable(this.deps.participants.analytics.residue({
521
468
  toLocationId: target.id,
522
- mode: storageClass === "galleryMedia" ? "gallery" : "move"
469
+ mode: "move"
523
470
  }));
524
471
  if (count !== null && count.rows === 0) continue;
525
472
  out.push({
@@ -798,9 +745,7 @@ var StorageMigrationCoordinator = class {
798
745
  *
799
746
  * Two lanes — the recorder's footage engine and post-analysis's media engine
800
747
  * — run in parallel with each other and strictly FIFO within themselves.
801
- * `galleryMedia` shares the media lane with `eventMedia` because it shares
802
- * the same `MediaRelocateEngine`, which refuses a second running job; starting
803
- * both would fail the second one instantly.
748
+ * `eventMedia` is the only class on the media lane.
804
749
  */
805
750
  async startOrResumeMoves(job) {
806
751
  if (job.cancelRequested) return;
@@ -930,7 +875,7 @@ var StorageMigrationCoordinator = class {
930
875
  const input = {
931
876
  toLocationId: move.toLocationId,
932
877
  throttleMbps: job.throttleMbps,
933
- mode: move.storageClass === "galleryMedia" ? "gallery" : "move"
878
+ mode: "move"
934
879
  };
935
880
  return (unleased ? await this.deps.participants.analytics.startDrain(input) : await this.deps.participants.analytics.startMove({
936
881
  ...input,
@@ -1004,13 +949,18 @@ function isTerminal(job) {
1004
949
  }
1005
950
  /**
1006
951
  * Which single-flight engine owns a class.
1007
- *
1008
- * `galleryMedia` is in the media lane not because it is event media — it
1009
- * explicitly is not (D197) — but because it is moved by the same
1010
- * `MediaRelocateEngine`, which refuses a second running job.
1011
952
  */
1012
953
  function laneOf(storageClass) {
1013
- return storageClass === "eventMedia" || storageClass === "galleryMedia" ? "media" : "footage";
954
+ return storageClass === "eventMedia" ? "media" : "footage";
955
+ }
956
+ /**
957
+ * The enrolled gallery is the analytics addon's data dir, not a storage class
958
+ * an operator migrates. A leftover destination from an older client is refused
959
+ * rather than silently dropped.
960
+ */
961
+ function refuseRetiredGalleryClass(destinations) {
962
+ if (destinations.galleryMedia === void 0) return;
963
+ throw new Error("galleryMedia is no longer a storage class. Enrolled gallery lives in the pipeline-analytics addon data directory, not a location the operator places.");
1014
964
  }
1015
965
  /**
1016
966
  * True when this phase runs with every writer already back up, so no
@@ -1465,14 +1415,13 @@ var StorageOrchestratorService = class {
1465
1415
  /**
1466
1416
  * Shout about a persisted row that is BOTH the type default and disabled.
1467
1417
  *
1468
- * `upsertLocation` cannot produce this (`resolveEnabled` force-enables a
1469
- * default) and neither can `setDefaultLocations` but hydrate writes rows
1470
- * straight into the map without either, so a hand-edited store, a partial
1471
- * migration or a row written by another build can. It matters because every
1472
- * bare-type ref resolves through {@link getDefaultLocation}: on such a row
1418
+ * `upsertLocation` can now produce this (enabled is the write set; `isDefault`
1419
+ * is store residue). Hydrate also writes rows straight into the map, so a
1420
+ * hand-edited store or a row written by another build can too. It matters
1421
+ * because every bare-type ref still resolves through {@link getDefaultLocation}
1422
+ * until Stage 5 of the enabled-write-set design: on such a row
1473
1423
  * `storage.write({ location: 'eventMedia' })` and the migration coordinator's
1474
- * drain destination both point at a disk the operator turned off. That is the
1475
- * one thing this model must never do silently.
1424
+ * drain destination both point at a disk the operator turned off.
1476
1425
  *
1477
1426
  * Reported, never repaired: flipping an operator's flag back on at boot would
1478
1427
  * be the system overruling the switch instead of obeying it. The coordinator
@@ -1756,6 +1705,7 @@ var StorageOrchestratorService = class {
1756
1705
  createdAt: existing?.createdAt ?? now,
1757
1706
  updatedAt: now
1758
1707
  };
1708
+ refuseLastWriteTargetDisabled(existing, next, this.locations.values());
1759
1709
  const demoted = [];
1760
1710
  if (next.isDefault) {
1761
1711
  for (const [otherId, otherLoc] of this.locations) if (otherLoc.type === next.type && otherLoc.id !== next.id && otherLoc.isDefault) {
@@ -2054,9 +2004,27 @@ function resolveEnabled(input, existing, typeAlreadyHasLocation) {
2054
2004
  if (existing) return {};
2055
2005
  return { enabled: input.isDefault || !typeAlreadyHasLocation };
2056
2006
  }
2057
- if (input.isDefault && requested === false) return { enabled: true };
2058
2007
  return { enabled: requested };
2059
2008
  }
2009
+ /** Types that may have zero enabled locations. Backups with none is sad, not illegal. */
2010
+ var OPTIONAL_WRITE_TYPES = new Set(["backups"]);
2011
+ /**
2012
+ * Refuse an UPDATE that would leave a write-serving type with no enabled
2013
+ * location. Creates are exempt (an explicit `enabled: false` on the first
2014
+ * location is already a named test). `isDefault` is not consulted — enabled
2015
+ * is the write set.
2016
+ */
2017
+ function refuseLastWriteTargetDisabled(existing, next, locations) {
2018
+ if (OPTIONAL_WRITE_TYPES.has(next.type)) return;
2019
+ if (next.enabled !== false) return;
2020
+ if (existing === void 0) return;
2021
+ if (existing.enabled === false) return;
2022
+ for (const loc of locations) {
2023
+ if (loc.id === next.id || loc.type !== next.type) continue;
2024
+ if (loc.enabled !== false) return;
2025
+ }
2026
+ throw new Error(`Cannot disable "${next.id}": it is the last enabled location of type "${next.type}". Enable another location of this type first.`);
2027
+ }
2060
2028
  //#endregion
2061
2029
  //#region src/builtins/storage-orchestrator/storage-pressure-manager.ts
2062
2030
  var DEFAULT_MAX_ROUNDS = 8;
@@ -186,14 +186,13 @@ export declare class StorageOrchestratorService {
186
186
  /**
187
187
  * Shout about a persisted row that is BOTH the type default and disabled.
188
188
  *
189
- * `upsertLocation` cannot produce this (`resolveEnabled` force-enables a
190
- * default) and neither can `setDefaultLocations` but hydrate writes rows
191
- * straight into the map without either, so a hand-edited store, a partial
192
- * migration or a row written by another build can. It matters because every
193
- * bare-type ref resolves through {@link getDefaultLocation}: on such a row
189
+ * `upsertLocation` can now produce this (enabled is the write set; `isDefault`
190
+ * is store residue). Hydrate also writes rows straight into the map, so a
191
+ * hand-edited store or a row written by another build can too. It matters
192
+ * because every bare-type ref still resolves through {@link getDefaultLocation}
193
+ * until Stage 5 of the enabled-write-set design: on such a row
194
194
  * `storage.write({ location: 'eventMedia' })` and the migration coordinator's
195
- * drain destination both point at a disk the operator turned off. That is the
196
- * one thing this model must never do silently.
195
+ * drain destination both point at a disk the operator turned off.
197
196
  *
198
197
  * Reported, never repaired: flipping an operator's flag back on at boot would
199
198
  * be the system overruling the switch instead of obeying it. The coordinator
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-B29Skpzo.js");
6
+ const require_dist = require("../../dist-BhE8zNfY.js");
7
7
  //#region src/builtins/system-config/system-config.addon.ts
8
8
  /**
9
9
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -1,4 +1,4 @@
1
- import { Mt as BaseAddon, Yt as hydrateSchema, jt as errMsg } from "../../dist-DxFjna2a.mjs";
1
+ import { Mt as BaseAddon, Yt as hydrateSchema, jt as errMsg } from "../../dist-BpsHt11f.mjs";
2
2
  //#region src/builtins/system-config/system-config.addon.ts
3
3
  /**
4
4
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-B29Skpzo.js");
6
+ const require_dist = require("../../dist-BhE8zNfY.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  let node_path = require("node:path");
9
9
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { Mt as BaseAddon, ot as logDestinationCapability } from "../../dist-DxFjna2a.mjs";
1
+ import { Mt as BaseAddon, ot as logDestinationCapability } from "../../dist-BpsHt11f.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  import * as path$1 from "node:path";
4
4
  import path from "node:path";
@@ -18151,6 +18151,47 @@ var RebuildStatusSchema = zod.z.object({
18151
18151
  /** Present when the pass ended by throwing. */
18152
18152
  error: zod.z.string().nullable()
18153
18153
  });
18154
+ /**
18155
+ * Acknowledgement that a debug-media reclaim STARTED.
18156
+ *
18157
+ * The pass is paced at ~2 MB/s over a standing 100 GB — tens of minutes — so
18158
+ * it runs detached and this returns immediately. Awaiting it is how the
18159
+ * `addons.custom` door hit the 60 s UDS deadline while the walk carried on
18160
+ * with no status. Poll {@link pipelineAnalyticsCapability.methods.getMediaReclaimStatus}.
18161
+ */
18162
+ var MediaReclaimStartResultSchema = zod.z.object({
18163
+ started: zod.z.boolean(),
18164
+ /** True when a pass was already running; the new request is ignored. */
18165
+ alreadyRunning: zod.z.boolean()
18166
+ });
18167
+ var MediaReclaimInputSchema = zod.z.object({
18168
+ mode: zod.z.enum(["report", "reclaim"]).default("report"),
18169
+ scopes: zod.z.array(zod.z.enum(["motion-still", "track-filmstrip"])).min(1).optional(),
18170
+ deviceIds: zod.z.array(zod.z.number().int()).min(1).optional(),
18171
+ restart: zod.z.boolean().optional(),
18172
+ pageSize: zod.z.number().int().min(50).max(5e3).optional(),
18173
+ maxRowsPerDevice: zod.z.number().int().min(1).max(5e5).optional(),
18174
+ maxReclaimPerDevice: zod.z.number().int().min(1).max(5e5).optional(),
18175
+ maxBytesPerRun: zod.z.number().int().min(1).optional(),
18176
+ budgetMinutes: zod.z.number().int().min(1).max(720).optional(),
18177
+ throttleBytesPerSec: zod.z.number().int().min(64 * 1024).optional(),
18178
+ graceMinutes: zod.z.number().int().min(1).max(10080).optional()
18179
+ });
18180
+ var MediaReclaimStatusSchema = zod.z.object({
18181
+ running: zod.z.boolean(),
18182
+ mode: zod.z.enum(["report", "reclaim"]).nullable(),
18183
+ totalExamined: zod.z.number(),
18184
+ totalEligible: zod.z.number(),
18185
+ totalReclaimed: zod.z.number(),
18186
+ totalBytesReclaimed: zod.z.number(),
18187
+ totalRefused: zod.z.number(),
18188
+ /** Device+scope windows finished in this pass. */
18189
+ devicesDone: zod.z.number(),
18190
+ complete: zod.z.boolean().nullable(),
18191
+ startedAtMs: zod.z.number().nullable(),
18192
+ finishedAtMs: zod.z.number().nullable(),
18193
+ error: zod.z.string().nullable()
18194
+ });
18154
18195
  var ReplayFrameInputSchema = zod.z.object({
18155
18196
  timestamp: zod.z.number(),
18156
18197
  frame: PipelineRunResultBridge
@@ -18574,6 +18615,20 @@ var pipelineAnalyticsCapability = {
18574
18615
  auth: "admin"
18575
18616
  }),
18576
18617
  /**
18618
+ * Reclaim the standing debug-media backlog (motion-visit stills + track
18619
+ * filmstrips). DETACHED: returns `{ started }` immediately. The pass is
18620
+ * tens of minutes; awaiting it is a timeout, not a verdict. Dry-run unless
18621
+ * `mode: 'reclaim'`. Poll `getMediaReclaimStatus`.
18622
+ */
18623
+ reclaimDebugMedia: method(MediaReclaimInputSchema, MediaReclaimStartResultSchema, {
18624
+ kind: "mutation",
18625
+ auth: "admin"
18626
+ }),
18627
+ getMediaReclaimStatus: method(zod.z.object({}), MediaReclaimStatusSchema, {
18628
+ kind: "query",
18629
+ auth: "admin"
18630
+ }),
18631
+ /**
18577
18632
  * The CHEAP QUESTION, asked before any media moves: how big is the dataset
18578
18633
  * the marked (`markForTrain`) tracks would produce?
18579
18634
  *
@@ -37160,6 +37215,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
37160
37215
  addonId: null,
37161
37216
  access: "view"
37162
37217
  },
37218
+ "pipelineAnalytics.getMediaReclaimStatus": {
37219
+ capName: "pipeline-analytics",
37220
+ capScope: "device",
37221
+ addonId: null,
37222
+ access: "view"
37223
+ },
37163
37224
  "pipelineAnalytics.getMotionEvents": {
37164
37225
  capName: "pipeline-analytics",
37165
37226
  capScope: "device",
@@ -37334,6 +37395,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
37334
37395
  addonId: null,
37335
37396
  access: "create"
37336
37397
  },
37398
+ "pipelineAnalytics.reclaimDebugMedia": {
37399
+ capName: "pipeline-analytics",
37400
+ capScope: "device",
37401
+ addonId: null,
37402
+ access: "create"
37403
+ },
37337
37404
  "pipelineAnalytics.reconcileFromDisk": {
37338
37405
  capName: "pipeline-analytics",
37339
37406
  capScope: "device",
@@ -40950,6 +41017,11 @@ Object.freeze({
40950
41017
  form: "single",
40951
41018
  optional: true
40952
41019
  }],
41020
+ "pipelineAnalytics.reclaimDebugMedia": [{
41021
+ name: "deviceIds",
41022
+ form: "array",
41023
+ optional: true
41024
+ }],
40953
41025
  "pipelineAnalytics.reconcileFromDisk": [{
40954
41026
  name: "deviceId",
40955
41027
  form: "single",