@camstack/system 1.2.201 → 1.2.202

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 (54) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  9. package/dist/builtins/console-logging/index.js +1 -1
  10. package/dist/builtins/console-logging/index.mjs +1 -1
  11. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  12. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  13. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  14. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  15. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  16. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  17. package/dist/builtins/hub-forwarder/index.js +1 -1
  18. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  19. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  20. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  21. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  22. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  23. package/dist/builtins/local-network/local-network.addon.js +1 -1
  24. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  25. package/dist/builtins/loki-logging/index.js +1 -1
  26. package/dist/builtins/loki-logging/index.mjs +1 -1
  27. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  28. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  29. package/dist/builtins/platform-probe/index.js +1 -1
  30. package/dist/builtins/platform-probe/index.mjs +1 -1
  31. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  32. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  33. package/dist/builtins/snapshot/index.js +1 -1
  34. package/dist/builtins/snapshot/index.mjs +1 -1
  35. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  36. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  37. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  38. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  39. package/dist/builtins/storage-orchestrator/location-store.d.ts +12 -3
  40. package/dist/builtins/storage-orchestrator/storage-migration-coordinator.d.ts +9 -9
  41. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +181 -129
  42. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +181 -129
  43. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +54 -31
  44. package/dist/builtins/system-config/system-config.addon.js +1 -1
  45. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  46. package/dist/builtins/winston-logging/index.js +1 -1
  47. package/dist/builtins/winston-logging/index.mjs +1 -1
  48. package/dist/{dist-BmJNnb_s.mjs → dist-CiHYspij.mjs} +202 -185
  49. package/dist/{dist-CcV3k-hD.js → dist-jZb-HKI5.js} +213 -184
  50. package/dist/index.js +1 -1
  51. package/dist/index.mjs +1 -1
  52. package/dist/{retired-settings-keys-nLVKlhe-.mjs → retired-settings-keys-DeRxo3vg.mjs} +1 -1
  53. package/dist/{retired-settings-keys-Bd8fuwO1.js → retired-settings-keys-Dxv9S_lO.js} +1 -1
  54. 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-CcV3k-hD.js");
6
+ const require_dist = require("../../dist-jZb-HKI5.js");
7
7
  const require_hub_cap_forward = require("../../hub-cap-forward-DmHNjbB0.js");
8
8
  let zod = require("zod");
9
9
  let node_crypto = require("node:crypto");
@@ -193,7 +193,7 @@ function mapLocationToRow(loc) {
193
193
  display_name: loc.displayName,
194
194
  provider_id: loc.providerId,
195
195
  config: storedConfig,
196
- is_default: loc.isDefault ? 1 : 0,
196
+ is_default: 0,
197
197
  is_system: loc.isSystem ? 1 : 0,
198
198
  created_at: loc.createdAt,
199
199
  updated_at: loc.updatedAt
@@ -227,7 +227,7 @@ function mapRowToLocation(row) {
227
227
  config,
228
228
  ...nodeId !== void 0 ? { nodeId } : {},
229
229
  ...enabled !== void 0 ? { enabled } : {},
230
- isDefault: row["is_default"] === 1 || row["is_default"] === true,
230
+ ...require_dist.LegacyStorageLocationDefaultSchema.parse({ isDefault: row["is_default"] === 1 || row["is_default"] === true }),
231
231
  isSystem: row["is_system"] === 1 || row["is_system"] === true,
232
232
  createdAt: Number(row["created_at"] ?? 0),
233
233
  updatedAt: Number(row["updated_at"] ?? 0)
@@ -295,16 +295,18 @@ var StorageMigrationCoordinator = class {
295
295
  const targetId = input.destinations[storageClass];
296
296
  if (targetId === void 0) continue;
297
297
  const namedSourceId = input.sources?.[storageClass];
298
- const defaultLoc = this.deps.locations.getDefaultLocation(storageClass);
299
- const source = namedSourceId ? this.deps.locations.getLocationById(namedSourceId) : defaultLoc;
300
- if (!source) throw new Error(namedSourceId ? `Storage location "${namedSourceId}" not found` : `No default storage location for "${storageClass}"`);
298
+ const writeSet = this.deps.locations.writeTargets(storageClass);
299
+ const implicitSources = writeSet.filter((loc) => loc.id !== targetId);
300
+ if (!namedSourceId && implicitSources.length > 1) throw new Error(`"${storageClass}" writes to ${String(implicitSources.length)} locations (${implicitSources.map((loc) => loc.id).join(", ")}) — name the one to cut over explicitly with \`sources\`.`);
301
+ const source = namedSourceId ? this.deps.locations.getLocationById(namedSourceId) : implicitSources[0];
302
+ if (!source) throw new Error(namedSourceId ? `Storage location "${namedSourceId}" not found` : `Storage location "${targetId}" is already the only write target for "${storageClass}" — there is nothing to cut over. Use storageMigration.drain to move what is left behind.`);
301
303
  if (source.type !== storageClass) throw new Error(`Storage location "${source.id}" is type "${source.type}", expected "${storageClass}"`);
302
304
  const target = this.deps.locations.getLocationById(targetId);
303
305
  if (!target) throw new Error(`Storage location "${targetId}" not found`);
304
306
  if (target.type !== storageClass) throw new Error(`Storage location "${targetId}" is type "${target.type}", expected "${storageClass}"`);
305
- if (source.id === target.id) throw new Error(namedSourceId ? `Source and destination are the same location ("${targetId}")` : `Storage location "${targetId}" is already the "${storageClass}" default`);
306
- const freezeSource = defaultLoc === null || source.id !== defaultLoc.id;
307
- 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.`);
307
+ if (source.id === target.id) throw new Error(namedSourceId ? `Source and destination are the same location ("${targetId}")` : `Storage location "${targetId}" is already the only write target for "${storageClass}"`);
308
+ const freezeSource = !writeSet.some((loc) => loc.id === source.id);
309
+ if (!MOVER_CLASSES.includes(storageClass)) throw new Error(`No mover owns "${storageClass}" — the migration can move its write set but cannot move its bytes. Move it by hand, then enable the destination (and disable the source) with upsertLocation.`);
308
310
  moves.push({
309
311
  storageClass,
310
312
  fromLocationId: source.id,
@@ -321,7 +323,7 @@ var StorageMigrationCoordinator = class {
321
323
  if (moves.some((move) => move.storageClass === "eventMedia")) {
322
324
  const unstamped = await this.deps.participants.analytics.countUnstamped();
323
325
  if (unstamped === null) {
324
- if (mode === "nonBlocking") throw new Error("could not determine whether any event-media row still carries no locationId. A non-blocking cutover repoints the default and would silently orphan every such row, so an unmeasured collection is refused exactly like a non-empty one. Retry, or run the blocking mode, which stamps rows as it moves them.");
326
+ if (mode === "nonBlocking") throw new Error("could not determine whether any event-media row still carries no locationId. A non-blocking cutover moves the write set and would silently orphan every such row, so an unmeasured collection is refused exactly like a non-empty one. Retry, or run the blocking mode, which stamps rows as it moves them.");
325
327
  findings.push({
326
328
  code: "unstampedEventMediaRows",
327
329
  storageClass: "eventMedia",
@@ -330,7 +332,7 @@ var StorageMigrationCoordinator = class {
330
332
  } else if (unstamped.anyPresent) {
331
333
  const scale = unstamped.total === null ? "an unknown number of" : unstamped.total.toString();
332
334
  const perLane = `${describeUnstamped(unstamped.media)} media, ${describeUnstamped(unstamped.retrainFrames)} retrain frames`;
333
- if (mode === "nonBlocking") throw new Error(`${scale} event-media row(s) still carry no locationId (${perLane}). A non-blocking cutover would silently orphan them. Run the seal first: pipelineAnalytics.relocateMedia({ mode: "seal", toLocationId: "${this.deps.locations.getDefaultLocation("eventMedia")?.id ?? "eventMedia"}" }).`);
335
+ if (mode === "nonBlocking") throw new Error(`${scale} event-media row(s) still carry no locationId (${perLane}). A non-blocking cutover would silently orphan them. Run the seal first: pipelineAnalytics.relocateMedia({ mode: "seal", toLocationId: "${this.deps.locations.writeTargets("eventMedia")[0]?.id ?? "eventMedia"}" }).`);
334
336
  findings.push({
335
337
  code: "unstampedEventMediaRows",
336
338
  storageClass: "eventMedia",
@@ -469,7 +471,7 @@ var StorageMigrationCoordinator = class {
469
471
  * remaining" action.
470
472
  *
471
473
  * A class appears here only when something is (or might be) left on a
472
- * location that is not its default. An empty result therefore means exactly
474
+ * location outside its write set. An empty result therefore means exactly
473
475
  * "there is nothing to drain", which is what lets the UI offer the action
474
476
  * only when it is true, and what lets {@link drain} refuse rather than start
475
477
  * a job that would move nothing and report `done` — the failure mode D295
@@ -483,11 +485,13 @@ var StorageMigrationCoordinator = class {
483
485
  const out = [];
484
486
  for (const storageClass of STORAGE_CLASSES) {
485
487
  if (!MOVER_CLASSES.includes(storageClass)) continue;
486
- const target = this.deps.locations.getDefaultLocation(storageClass);
488
+ const writeSet = this.deps.locations.writeTargets(storageClass);
489
+ const target = writeSet[0];
487
490
  if (!target) continue;
491
+ const inWriteSet = new Set(writeSet.map((loc) => loc.id));
488
492
  if (laneOf(storageClass) === "media") {
489
493
  for (const source of this.deps.locations.listLocations({ type: storageClass })) {
490
- if (source.id === target.id) continue;
494
+ if (inWriteSet.has(source.id)) continue;
491
495
  const count = await unanswerable(this.deps.participants.analytics.residue({
492
496
  toLocationId: target.id,
493
497
  fromLocationId: source.id,
@@ -505,7 +509,7 @@ var StorageMigrationCoordinator = class {
505
509
  continue;
506
510
  }
507
511
  for (const source of this.deps.locations.listLocations({ type: storageClass })) {
508
- if (source.id === target.id) continue;
512
+ if (inWriteSet.has(source.id)) continue;
509
513
  const census = await unanswerable(this.deps.participants.recorder.residue({
510
514
  fromLocationId: source.id,
511
515
  footageClass: storageClass === "recordingsLow" ? "recordingsLow" : "recordings"
@@ -523,12 +527,12 @@ var StorageMigrationCoordinator = class {
523
527
  return out;
524
528
  }
525
529
  /**
526
- * Run the DRAIN half alone, against classes whose default has already moved.
530
+ * Run the DRAIN half alone, against classes whose write set has already moved.
527
531
  *
528
532
  * ## Why this is a second verb rather than a looser `start`
529
533
  *
530
- * `start` refuses a destination that is already the class's default
531
- * (`"recordingsLow:ssd" is already the "recordingsLow" default`). That refusal
534
+ * `start` refuses a destination that is already the class's only write target
535
+ * (`"recordingsLow:ssd" is already the only write target for "recordingsLow"`). That refusal
532
536
  * is correct and it is load-bearing: there is genuinely nothing left to
533
537
  * repoint, and an operator must never be able to re-repoint a migrated class
534
538
  * by accident. Making `start` idempotent — "an already-repointed class
@@ -537,7 +541,7 @@ var StorageMigrationCoordinator = class {
537
541
  * operator reads ("pauses the writers…") would be a lie half the time.
538
542
  *
539
543
  * `drain` instead cannot repoint AT ALL: it never touches
540
- * `setDefaultLocations`, and its job starts in `draining` with `repointed`
544
+ * `setWriteTargets`, and its job starts in `draining` with `repointed`
541
545
  * already true, so the `repointing` / `refreshing` / `resuming` blocks of
542
546
  * {@link run} are behind it and unreachable. The two verbs are disjoint, and
543
547
  * `start`'s refusal keeps meaning exactly what it meant.
@@ -547,7 +551,7 @@ var StorageMigrationCoordinator = class {
547
551
  * The finished job is the audit of what happened; re-opening it destroys
548
552
  * that. And a drain is needed in cases where no migration job ever existed
549
553
  * (a mover armed by hand, footage stranded on a location an operator added
550
- * and then un-defaulted). One job = one operation, and the work list comes
554
+ * and then disabled again). One job = one operation, and the work list comes
551
555
  * from {@link residue} — the archive — not from what a previous job believed.
552
556
  */
553
557
  async drain(input) {
@@ -570,9 +574,9 @@ var StorageMigrationCoordinator = class {
570
574
  const destinations = {};
571
575
  for (const storageClass of input.classes) {
572
576
  const remaining = residue.filter((entry) => entry.storageClass === storageClass);
573
- if (remaining.length === 0) throw new Error(`"${storageClass}" has nothing left outside its default location — there is nothing to drain. To CHANGE where it writes, use storageMigration.start with a new destination.`);
577
+ if (remaining.length === 0) throw new Error(`"${storageClass}" has nothing left outside its write set — there is nothing to drain. To CHANGE where it writes, use storageMigration.start with a new destination.`);
574
578
  for (const entry of remaining) {
575
- if (this.deps.locations.getLocationById(entry.toLocationId)?.enabled === false) throw new Error(`The "${storageClass}" drain destination "${entry.toLocationId}" is DISABLED — a disabled location takes no writes, and a drain is the largest write there is. Enable it on the Storage screen, or make an enabled location the "${storageClass}" default first.`);
579
+ if (this.deps.locations.getLocationById(entry.toLocationId)?.enabled === false) throw new Error(`The "${storageClass}" drain destination "${entry.toLocationId}" is DISABLED — a disabled location takes no writes, and a drain is the largest write there is. Enable it on the Storage screen first.`);
576
580
  moves.push({
577
581
  storageClass,
578
582
  fromLocationId: entry.fromLocationId,
@@ -690,8 +694,13 @@ var StorageMigrationCoordinator = class {
690
694
  await this.setPhase(job, "repointing");
691
695
  }
692
696
  if (job.phase === "repointing") {
693
- const targets = new Map(job.moves.filter((move) => move.freezeSource !== true).map((move) => [move.storageClass, move.toLocationId]));
694
- if (targets.size > 0) await this.deps.locations.setDefaultLocations(targets);
697
+ const targets = /* @__PURE__ */ new Map();
698
+ for (const move of job.moves) {
699
+ if (move.freezeSource === true) continue;
700
+ targets.set(move.toLocationId, true);
701
+ targets.set(move.fromLocationId, false);
702
+ }
703
+ if (targets.size > 0) await this.deps.locations.setWriteTargets(targets);
695
704
  job.repointed = true;
696
705
  await this.setPhase(job, "refreshing");
697
706
  }
@@ -890,9 +899,9 @@ var StorageMigrationCoordinator = class {
890
899
  for (const move of job.moves) if (move.state !== "done") throw new Error(`${move.storageClass} move did not complete verification`);
891
900
  }
892
901
  /**
893
- * After a from→to copy whose source was NOT the class default: stop new
894
- * writes to `from`. The default is unchanged (that is the whole point of
895
- * naming a non-default source). A location that is already disabled is
902
+ * After a from→to copy whose source was NOT a write target: stop new writes
903
+ * to `from`. The rest of the write set is unchanged (that is the whole point
904
+ * of naming a source outside it). A location that is already disabled is
896
905
  * left alone.
897
906
  */
898
907
  freezeDrainedSources(job) {
@@ -1837,33 +1846,77 @@ var StorageOrchestratorService = class {
1837
1846
  backfilled: backfill.length,
1838
1847
  total: this.locations.size
1839
1848
  } });
1840
- this.reportDisabledDefaults();
1849
+ this.migrateOntoTheWriteSet(rows);
1841
1850
  }
1842
1851
  /**
1843
- * Shout about a persisted row that is BOTH the type default and disabled.
1852
+ * ONE explicit migration onto the enabled write set (D383), run once per
1853
+ * boot against the rows that came off disk. Idempotent: the second boot finds
1854
+ * nothing to do.
1855
+ *
1856
+ * Two steps, in order, and each one is a WRITE — the point is that "absent"
1857
+ * stops existing rather than being re-interpreted on every read forever:
1844
1858
  *
1845
- * `upsertLocation` can now produce this (enabled is the write set; `isDefault`
1846
- * is store residue). Hydrate also writes rows straight into the map, so a
1847
- * hand-edited store or a row written by another build can too. It matters
1848
- * because every bare-type ref still resolves through {@link getDefaultLocation}
1849
- * until Stage 5 of the enabled-write-set design: on such a row
1850
- * `storage.write({ location: 'eventMedia' })` and the migration coordinator's
1851
- * drain destination both point at a disk the operator turned off.
1859
+ * 1. A flagless row (persisted before `enabled` existed) is stamped
1860
+ * `enabled: true`. Absence already meant active, so this preserves
1861
+ * behaviour exactly; it is the operator's "the initial locations are
1862
+ * enabled" spelled into the store.
1863
+ * 2. A type left with NO enabled row — only reachable from a hand-edited
1864
+ * store or a row an older build wrote — gets one enabled back, chosen as
1865
+ * the row the RETIRED default flag named, else the `:default`-slugged
1866
+ * row, else the lexicographically first. Loudly: a class with no write
1867
+ * target records nowhere, and refusing to repair it here would leave the
1868
+ * hub silently unable to write with no operator action pending.
1852
1869
  *
1853
- * Reported, never repaired: flipping an operator's flag back on at boot would
1854
- * be the system overruling the switch instead of obeying it. The coordinator
1855
- * refuses the drain (`storage-migration-coordinator.drain`); the bare-ref read
1856
- * path is deliberately left working, because narrowing a READ is a worse bug
1857
- * than the one this line reports.
1870
+ * An operator's explicit `enabled: false` is never reversed by step 1 — only
1871
+ * step 2 touches it, and only when the alternative is a class that cannot
1872
+ * write at all.
1858
1873
  */
1859
- reportDisabledDefaults() {
1874
+ migrateOntoTheWriteSet(rows) {
1875
+ const legacyDefaultOfType = /* @__PURE__ */ new Map();
1876
+ for (const row of rows) if (row.isDefault === true) legacyDefaultOfType.set(row.type, row.id);
1877
+ const stamped = [];
1860
1878
  for (const loc of this.locations.values()) {
1861
- if (!loc.isDefault || loc.enabled !== false) continue;
1862
- this.logger.error("storage-orchestrator: the type DEFAULT is disabled — every bare-type ref for this type resolves to a location the operator turned off. Enable it, or make another location the default.", { meta: {
1863
- id: loc.id,
1864
- type: loc.type
1865
- } });
1879
+ if (loc.enabled !== void 0) continue;
1880
+ this.persistMigrated({
1881
+ ...loc,
1882
+ enabled: true
1883
+ });
1884
+ stamped.push(loc.id);
1885
+ }
1886
+ const repaired = [];
1887
+ for (const type of new Set([...this.locations.values()].map((l) => l.type))) {
1888
+ const ofType = [...this.locations.values()].filter((l) => l.type === type);
1889
+ if (ofType.some((l) => require_dist.isLocationEnabled(l))) continue;
1890
+ const legacyDefault = legacyDefaultOfType.get(type);
1891
+ const pick = ofType.find((l) => l.id === legacyDefault) ?? ofType.find((l) => l.id === `${type}:default`) ?? ofType.toSorted((a, b) => a.id.localeCompare(b.id))[0];
1892
+ if (!pick) continue;
1893
+ this.persistMigrated({
1894
+ ...pick,
1895
+ enabled: true
1896
+ });
1897
+ repaired.push(pick.id);
1866
1898
  }
1899
+ if (stamped.length > 0 || repaired.length > 0) this.logger.info("storage-orchestrator: migrated locations onto the enabled write set", { meta: {
1900
+ stamped,
1901
+ repaired
1902
+ } });
1903
+ for (const id of repaired) this.logger.warn("storage-orchestrator: a storage type had NO enabled location — re-enabled one so it can write. Every other location of that type stays read-only until you enable it.", { meta: { id } });
1904
+ }
1905
+ /** Apply one migrated row in memory and mirror it to the store. */
1906
+ persistMigrated(next) {
1907
+ const withStamp = {
1908
+ ...next,
1909
+ updatedAt: Date.now()
1910
+ };
1911
+ this.locations.set(withStamp.id, withStamp);
1912
+ const store = this.locationStore;
1913
+ if (!store) return;
1914
+ store.upsert(withStamp).catch((err) => {
1915
+ this.logger.error("storage-orchestrator: write-set migration persist failed", { meta: {
1916
+ id: withStamp.id,
1917
+ error: err instanceof Error ? err.message : String(err)
1918
+ } });
1919
+ });
1867
1920
  }
1868
1921
  /**
1869
1922
  * Inject the declaration-driven cardinality source. Called once by the
@@ -1918,7 +1971,7 @@ var StorageOrchestratorService = class {
1918
1971
  const rows = await this.locationStore.loadAll();
1919
1972
  for (const loc of rows) this.locations.set(loc.id, loc);
1920
1973
  this.logger.info("storage-orchestrator: hydrated locations from store", { meta: { loaded: this.locations.size } });
1921
- this.reportDisabledDefaults();
1974
+ this.migrateOntoTheWriteSet(rows);
1922
1975
  }
1923
1976
  /**
1924
1977
  * Boot backfill (SP1): stamp `nodeId` on every persisted node-local
@@ -1967,40 +2020,51 @@ var StorageOrchestratorService = class {
1967
2020
  const all = [...this.locations.values()];
1968
2021
  return filter?.type ? all.filter((l) => l.type === filter.type) : all;
1969
2022
  }
1970
- getDefaultLocation(type) {
1971
- for (const loc of this.locations.values()) if (loc.type === type && loc.isDefault) return loc;
1972
- return null;
2023
+ /**
2024
+ * The WRITE SET of a type: every enabled location of it, in stable id order.
2025
+ * This replaced `getDefaultLocation` (D383) — a type does not have one write
2026
+ * target any more, it has a pool, and every consumer that must pick one picks
2027
+ * from here by its own rule (the recorder by headroom, a drain by an operator
2028
+ * -named destination).
2029
+ */
2030
+ writeTargets(type) {
2031
+ return [...this.locations.values()].filter((loc) => loc.type === type && require_dist.isLocationEnabled(loc)).toSorted((a, b) => a.id.localeCompare(b.id));
1973
2032
  }
1974
2033
  /**
1975
- * Repoint several logical defaults as one in-memory commit. The migration
1976
- * coordinator persists its `repointing` phase before calling this method, so
1977
- * a process crash during the backing-store writes is recovered by replaying
1978
- * the same idempotent target set before anyone resumes writers.
2034
+ * Change the WRITE SET as one compensated commit: each named location is
2035
+ * enabled (it joins the pool) or disabled (it becomes read-only — still read,
2036
+ * still played back, still age-swept, still drained).
2037
+ *
2038
+ * This is the cutover verb, and its only caller is the migration coordinator,
2039
+ * which persists its `repointing` phase BEFORE calling so a crash mid-write
2040
+ * is recovered by replaying the same idempotent set before writers resume.
2041
+ *
2042
+ * It moves ONLY the locations it is given: a cutover from A to C on a class
2043
+ * that also writes to B leaves B in the pool, because the operator's pool is
2044
+ * the authority and a migration is not a licence to shrink it. The commit is
2045
+ * refused when it would leave a write-serving type with nothing enabled —
2046
+ * the same refusal `upsertLocation` applies, judged on the RESULT so an
2047
+ * enable and a disable in one commit are weighed together.
1979
2048
  */
1980
- async setDefaultLocations(targetByType) {
1981
- if (targetByType.size === 0) return;
2049
+ async setWriteTargets(enabledByLocationId) {
2050
+ if (enabledByLocationId.size === 0) return;
1982
2051
  const now = Date.now();
1983
2052
  const nextById = new Map(this.locations);
1984
2053
  const touched = /* @__PURE__ */ new Map();
1985
- for (const [type, targetId] of targetByType) {
1986
- const target = nextById.get(targetId);
1987
- if (!target) throw new Error(`Storage location "${targetId}" not found`);
1988
- if (target.type !== type) throw new Error(`Storage location "${targetId}" is type "${target.type}", expected "${type}"`);
1989
- for (const [id, location] of nextById) {
1990
- if (location.type !== type) continue;
1991
- const isDefault = id === targetId;
1992
- const enable = isDefault && location.enabled === false;
1993
- if (location.isDefault === isDefault && !enable) continue;
1994
- const updated = {
1995
- ...location,
1996
- isDefault,
1997
- ...enable ? { enabled: true } : {},
1998
- updatedAt: now
1999
- };
2000
- nextById.set(id, updated);
2001
- touched.set(id, updated);
2002
- }
2054
+ for (const [id, enabled] of enabledByLocationId) {
2055
+ const location = nextById.get(id);
2056
+ if (!location) throw new Error(`Storage location "${id}" not found`);
2057
+ if (location.enabled === enabled) continue;
2058
+ const updated = {
2059
+ ...location,
2060
+ enabled,
2061
+ updatedAt: now
2062
+ };
2063
+ nextById.set(id, updated);
2064
+ touched.set(id, updated);
2003
2065
  }
2066
+ if (touched.size === 0) return;
2067
+ refuseTypeWithoutWriteTarget(touched.values(), nextById.values());
2004
2068
  if (this.locationStore) {
2005
2069
  const previous = /* @__PURE__ */ new Map();
2006
2070
  for (const id of touched.keys()) {
@@ -2020,13 +2084,10 @@ var StorageOrchestratorService = class {
2020
2084
  for (const [id, location] of nextById) this.locations.set(id, location);
2021
2085
  }
2022
2086
  /**
2023
- * Insert or update a location. If `isDefault: true`, atomically
2024
- * demotes any other default for the same type — operators always see
2025
- * exactly one default per type.
2087
+ * Insert or update a location.
2026
2088
  *
2027
- * When a persistence store is wired (Task 6), the new record AND any
2028
- * implicitly-demoted siblings are persisted before the in-memory map
2029
- * mutation returns. Persistence errors propagate to the caller.
2089
+ * When a persistence store is wired (Task 6), the record is mirrored to it;
2090
+ * the mirror is fire-and-forget and its errors reach the logger.
2030
2091
  */
2031
2092
  /**
2032
2093
  * Refuse a `(location kind, provider)` pair the kind cannot use.
@@ -2112,30 +2173,17 @@ var StorageOrchestratorService = class {
2112
2173
  updatedAt: now
2113
2174
  };
2114
2175
  refuseLastWriteTargetDisabled(existing, next, this.locations.values());
2115
- const demoted = [];
2116
- if (next.isDefault) {
2117
- for (const [otherId, otherLoc] of this.locations) if (otherLoc.type === next.type && otherLoc.id !== next.id && otherLoc.isDefault) {
2118
- const updated = {
2119
- ...otherLoc,
2120
- isDefault: false,
2121
- updatedAt: now
2122
- };
2123
- this.locations.set(otherId, updated);
2124
- demoted.push(updated);
2125
- }
2126
- }
2127
2176
  this.locations.set(next.id, next);
2128
2177
  this.logger.debug("storage-orchestrator: upsertLocation", { meta: {
2129
2178
  id: next.id,
2130
2179
  type: next.type,
2131
2180
  providerId: next.providerId,
2132
- isDefault: next.isDefault
2181
+ enabled: next.enabled
2133
2182
  } });
2134
2183
  if (this.locationStore) {
2135
2184
  const store = this.locationStore;
2136
2185
  (async () => {
2137
2186
  try {
2138
- for (const d of demoted) await store.upsert(d);
2139
2187
  await store.upsert(next);
2140
2188
  } catch (err) {
2141
2189
  this.logger.error("storage-orchestrator: upsert persistence failed", { meta: {
@@ -2150,8 +2198,7 @@ var StorageOrchestratorService = class {
2150
2198
  /**
2151
2199
  * Remove a location. Refuses the last location of a type and the last
2152
2200
  * enabled location of a type. `isSystem` is informational — a frozen
2153
- * recordings `:default` is deletable once a sibling is live. Deleting
2154
- * the flagged default promotes an enabled sibling. Occupancy still
2201
+ * recordings `:default` is deletable once a sibling is live. Occupancy still
2155
2202
  * refuses unless `force` is set; force never overrides uniqueness /
2156
2203
  * last-enabled.
2157
2204
  */
@@ -2162,8 +2209,6 @@ var StorageOrchestratorService = class {
2162
2209
  if (ofType.length <= 1) throw new Error(`Cannot delete "${id}" — it is the only location for type "${loc.type}"`);
2163
2210
  const enabledOfType = ofType.filter((l) => l.enabled !== false);
2164
2211
  if (loc.enabled !== false && enabledOfType.length <= 1) throw new Error(`Cannot delete "${id}" — it is the only enabled location for type "${loc.type}"`);
2165
- const successor = loc.isDefault === true ? ofType.find((l) => l.id !== id && l.enabled !== false) : void 0;
2166
- if (loc.isDefault === true && successor === void 0) throw new Error(`Cannot delete "${id}" — it is the only enabled location for type "${loc.type}"`);
2167
2212
  const occupancy = await this.probeOccupancy(loc);
2168
2213
  if (occupancy !== "empty") {
2169
2214
  if (options?.force !== true) throw new Error(occupancy === "occupied" ? `Storage location "${id}" still holds data — drain it first (storage migration / relocate), or pass force to delete the record anyway and strand what is on it` : `Storage location "${id}" could not be checked for remaining data (unreachable provider, another node, or an unmounted root) — pass force to delete the record anyway`);
@@ -2173,21 +2218,6 @@ var StorageOrchestratorService = class {
2173
2218
  occupancy
2174
2219
  } });
2175
2220
  }
2176
- if (successor !== void 0 && successor.isDefault !== true) {
2177
- const now = Date.now();
2178
- const promoted = {
2179
- ...successor,
2180
- isDefault: true,
2181
- updatedAt: now
2182
- };
2183
- this.locations.set(successor.id, promoted);
2184
- if (this.locationStore) this.locationStore.upsert(promoted).catch((err) => {
2185
- this.logger.error("storage-orchestrator: default promotion persist failed", { meta: {
2186
- id: successor.id,
2187
- error: err instanceof Error ? err.message : String(err)
2188
- } });
2189
- });
2190
- }
2191
2221
  this.locations.delete(id);
2192
2222
  if (this.locationStore) this.locationStore.delete(id).catch((err) => {
2193
2223
  this.logger.error("storage-orchestrator: delete persistence failed", { meta: {
@@ -2266,9 +2296,17 @@ var StorageOrchestratorService = class {
2266
2296
  if (!loc) throw new Error(`Storage location "${ref}" not found`);
2267
2297
  return loc;
2268
2298
  }
2269
- const def = this.getDefaultLocation(ref);
2270
- if (!def) throw new Error(`No default storage location configured for type "${ref}" — register one or pass a fully-qualified id (\`<type>:<slug>\`)`);
2271
- return def;
2299
+ const ofType = [...this.locations.values()].filter((loc) => loc.type === ref);
2300
+ if (ofType.length === 1) return ofType[0];
2301
+ if (ofType.length === 0) throw new Error(`No storage location configured for type "${ref}" — register one or pass a fully-qualified id (\`<type>:<slug>\`)`);
2302
+ const slugged = ofType.find((loc) => loc.id === `${ref}:default`);
2303
+ if (!slugged) throw new Error(`Storage type "${ref}" has ${String(ofType.length)} locations (${ofType.map((loc) => loc.id).toSorted().join(", ")}) — a bare type ref is ambiguous, pass a fully-qualified id (\`<type>:<slug>\`)`);
2304
+ this.logger.warn("storage-orchestrator: DEPRECATED bare-type ref on a type with several locations — resolved to the legacy `:default`-slugged row. Pass a fully-qualified location id.", { meta: {
2305
+ ref,
2306
+ resolved: slugged.id,
2307
+ candidates: ofType.length
2308
+ } });
2309
+ return slugged;
2272
2310
  }
2273
2311
  /**
2274
2312
  * Find the storage-provider that backs a given location — the entry point
@@ -2372,9 +2410,11 @@ var StorageOrchestratorService = class {
2372
2410
  * declarations on a populated map is a no-op (each declared `id`
2373
2411
  * already resolves to an existing `<id>:default`).
2374
2412
  *
2375
- * Convention: the bare-type ref `'recordings'` resolves via
2376
- * `getDefaultLocation('recordings')` (walks `isDefault === true`); the
2377
- * actual stored `id` is `'recordings:default'` so it satisfies the
2413
+ * The seeded row is born ENABLED — a fresh install must be able to write the
2414
+ * moment it boots, and `enabled` is the only write switch there is (D383).
2415
+ * The `:default` suffix is a NAME kept for idempotency across existing
2416
+ * installs, not a flag: the stored `id` is `'recordings:default'` so it
2417
+ * satisfies the
2378
2418
  * `^[a-z][a-z0-9-]*:[a-z0-9-]+$` regex on `StorageLocationSchema.id`.
2379
2419
  *
2380
2420
  * `basePath` is the storage root (e.g. `<CAMSTACK_DATA>`). Each
@@ -2399,7 +2439,7 @@ var StorageOrchestratorService = class {
2399
2439
  displayName: d.displayName,
2400
2440
  providerId: input.providerId,
2401
2441
  config: { basePath: base },
2402
- isDefault: true,
2442
+ enabled: true,
2403
2443
  isSystem: false
2404
2444
  });
2405
2445
  added++;
@@ -2423,7 +2463,7 @@ function resolveEnabled(input, existing, typeAlreadyHasLocation) {
2423
2463
  const requested = input.enabled ?? existing?.enabled;
2424
2464
  if (requested === void 0) {
2425
2465
  if (existing) return {};
2426
- return { enabled: input.isDefault || !typeAlreadyHasLocation };
2466
+ return { enabled: !typeAlreadyHasLocation };
2427
2467
  }
2428
2468
  return { enabled: requested };
2429
2469
  }
@@ -2432,8 +2472,7 @@ var OPTIONAL_WRITE_TYPES = new Set(["backups"]);
2432
2472
  /**
2433
2473
  * Refuse an UPDATE that would leave a write-serving type with no enabled
2434
2474
  * location. Creates are exempt (an explicit `enabled: false` on the first
2435
- * location is already a named test). `isDefault` is not consulted — enabled
2436
- * is the write set.
2475
+ * location is already a named test). Enabled IS the write set.
2437
2476
  */
2438
2477
  function refuseLastWriteTargetDisabled(existing, next, locations) {
2439
2478
  if (OPTIONAL_WRITE_TYPES.has(next.type)) return;
@@ -2446,6 +2485,23 @@ function refuseLastWriteTargetDisabled(existing, next, locations) {
2446
2485
  }
2447
2486
  throw new Error(`Cannot disable "${next.id}": it is the last enabled location of type "${next.type}". Enable another location of this type first.`);
2448
2487
  }
2488
+ /**
2489
+ * The same refusal, judged on a whole COMMIT rather than one row: a
2490
+ * multi-location change (a cutover enables its destination and disables its
2491
+ * source in one go) must be weighed on its RESULT, or the disable half looks
2492
+ * like the last write target leaving while the enable half is already in the
2493
+ * same map.
2494
+ */
2495
+ function refuseTypeWithoutWriteTarget(touched, result) {
2496
+ const rows = [...result];
2497
+ for (const type of new Set([...touched].map((loc) => loc.type))) {
2498
+ if (OPTIONAL_WRITE_TYPES.has(type)) continue;
2499
+ const ofType = rows.filter((loc) => loc.type === type);
2500
+ if (ofType.length === 0) continue;
2501
+ if (ofType.some((loc) => require_dist.isLocationEnabled(loc))) continue;
2502
+ throw new Error(`Refusing the write-set change: it would leave type "${type}" with no enabled location. Enable one of ${ofType.map((loc) => loc.id).toSorted().join(", ")} first.`);
2503
+ }
2504
+ }
2449
2505
  //#endregion
2450
2506
  //#region src/builtins/storage-orchestrator/storage-pressure-manager.ts
2451
2507
  var DEFAULT_MAX_ROUNDS = 8;
@@ -2687,13 +2743,9 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
2687
2743
  const rows = type !== void 0 ? service.listLocations({ type }) : service.listLocations();
2688
2744
  return Promise.all(rows.map(async (loc) => ({
2689
2745
  ...this.redacted(loc),
2690
- capacity: loc.enabled === false ? null : await this.localCapacityOf(loc)
2746
+ capacity: require_dist.isLocationEnabled(loc) ? await this.localCapacityOf(loc) : null
2691
2747
  })));
2692
2748
  },
2693
- getDefaultLocation: async ({ type }) => {
2694
- const loc = service.getDefaultLocation(type);
2695
- return loc === null ? null : this.redacted(loc);
2696
- },
2697
2749
  listLocationDeclarations: async () => service.listDeclarations(),
2698
2750
  upsertLocation: async (input) => {
2699
2751
  const stored = service.getLocationById(input.id);