camstack 1.2.110 → 1.2.112

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.
@@ -23633,9 +23633,9 @@ var require_zod = __commonJS({
23633
23633
  }
23634
23634
  });
23635
23635
 
23636
- // ../system/dist/dist-KQ5D2wqr.js
23637
- var require_dist_KQ5D2wqr = __commonJS({
23638
- "../system/dist/dist-KQ5D2wqr.js"(exports) {
23636
+ // ../system/dist/dist-jZb-HKI5.js
23637
+ var require_dist_jZb_HKI5 = __commonJS({
23638
+ "../system/dist/dist-jZb-HKI5.js"(exports) {
23639
23639
  "use strict";
23640
23640
  var zod = require_zod();
23641
23641
  var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
@@ -26081,46 +26081,6 @@ var require_dist_KQ5D2wqr = __commonJS({
26081
26081
  function logLevelAtMost(level, threshold) {
26082
26082
  return LOG_LEVEL_RANK[level] <= LOG_LEVEL_RANK[threshold];
26083
26083
  }
26084
- var LogChannelLevelSchema = zod.z.enum([
26085
- "info",
26086
- "warn",
26087
- "error"
26088
- ]);
26089
- var LogChannelDescriptorSchema = zod.z.object({
26090
- /**
26091
- * Dotted `area.thing`, unique across the workspace. `area` is conventionally
26092
- * the addon's short name so an operator reading a channel list can tell who
26093
- * owns it without a second lookup.
26094
- */
26095
- name: zod.z.string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
26096
- /** One sentence: what the operator will SEE after arming it. */
26097
- description: zod.z.string().min(1),
26098
- /** The level its lines are emitted at. Never below `info`. */
26099
- defaultLevel: LogChannelLevelSchema,
26100
- /**
26101
- * Whether this channel can be narrowed to a camera.
26102
- *
26103
- * `true` is a PROMISE with two halves, and both must hold: the gate is
26104
- * consulted with the numeric device id, AND every line the channel admits
26105
- * carries `tags: { deviceId }` with that same numeric id. The second half is
26106
- * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
26107
- * keeps `deviceId` out of the stream labels for cardinality, so the tag in
26108
- * the body is the only way to filter.
26109
- *
26110
- * A channel whose lines carry the device only in `meta` (or not at all) is
26111
- * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
26112
- * the operator narrows to one camera, sees nothing, and concludes the code
26113
- * path was never taken.
26114
- */
26115
- perDevice: zod.z.boolean()
26116
- });
26117
- var LogChannelWindowSchema = zod.z.object({
26118
- channel: zod.z.string().min(1),
26119
- /** Epoch ms the window closes at. */
26120
- armedUntilMs: zod.z.number(),
26121
- /** `null` = every camera. A non-empty list narrows to those numeric ids. */
26122
- deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable()
26123
- });
26124
26084
  var OpsLogDomainSchema = zod.z.enum(["recording", "events"]);
26125
26085
  var OpsLogOpSchema = zod.z.enum([
26126
26086
  "prune",
@@ -26711,21 +26671,20 @@ var require_dist_KQ5D2wqr = __commonJS({
26711
26671
  * flag at upsert time, not here (the schema is provider-agnostic).
26712
26672
  */
26713
26673
  nodeId: zod.z.string().optional(),
26714
- isDefault: zod.z.boolean().default(false),
26715
26674
  isSystem: zod.z.boolean().default(false),
26716
26675
  /**
26717
- * Operator opt-in: whether consumers that BALANCE across several locations
26718
- * of a type may write here. Recordings reads it today; event media and
26719
- * backups are the next consumers, which is why the flag lives on the
26720
- * location rather than in any one addon's store — nothing has to be
26721
- * extended to add the next consumer.
26676
+ * THE write switch, and the only one (D383). `enabled: true` means every
26677
+ * consumer that chooses a write target for this type may write here, and all
26678
+ * enabled locations of a type are used TOGETHER; `false` means read-only —
26679
+ * still read, still played back, still age-swept, still drained, never
26680
+ * written.
26722
26681
  *
26723
- * OPTIONAL, and ABSENT MEANS ACTIVE. Every location persisted before the
26724
- * flag existed reads back with no flag and keeps working exactly as before;
26725
- * that is the whole compat story, and it is why no migration ships with it.
26726
- * A newly CREATED sibling is stamped `false` by the orchestrator (creating a
26727
- * disk must not silently start writing to it); the default of a type is
26728
- * always stamped `true`.
26682
+ * OPTIONAL only for the wire: an upsert that omits it means "leave what is
26683
+ * stored" on an update and "born inert unless it is the first location of its
26684
+ * type" on a create. On a PERSISTED row absence is legacy and it means
26685
+ * enabled — {@link isLocationEnabled} is the one place that says so, and the
26686
+ * orchestrator stamps every flagless row `true` once at hydrate so absence
26687
+ * stops existing rather than being re-derived on every read.
26729
26688
  */
26730
26689
  enabled: zod.z.boolean().optional(),
26731
26690
  /** COMPUTED at read time by the orchestrator (statfs of the backing volume
@@ -26738,6 +26697,10 @@ var require_dist_KQ5D2wqr = __commonJS({
26738
26697
  createdAt: zod.z.number(),
26739
26698
  updatedAt: zod.z.number()
26740
26699
  });
26700
+ var LegacyStorageLocationDefaultSchema = zod.z.object({ isDefault: zod.z.boolean().optional() });
26701
+ function isLocationEnabled(location) {
26702
+ return location.enabled !== false;
26703
+ }
26741
26704
  var StorageLocationRefSchema = zod.z.union([StorageLocationTypeSchema, zod.z.string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/)]);
26742
26705
  var StorageAccessSchema = zod.z.enum(["local-path", "cap-mediated"]);
26743
26706
  var STORAGE_ACCESS_FALLBACK = "local-path";
@@ -26865,6 +26828,46 @@ var require_dist_KQ5D2wqr = __commonJS({
26865
26828
  */
26866
26829
  debug: zod.z.boolean().optional()
26867
26830
  });
26831
+ var LogChannelLevelSchema = zod.z.enum([
26832
+ "info",
26833
+ "warn",
26834
+ "error"
26835
+ ]);
26836
+ var LogChannelDescriptorSchema = zod.z.object({
26837
+ /**
26838
+ * Dotted `area.thing`, unique across the workspace. `area` is conventionally
26839
+ * the addon's short name so an operator reading a channel list can tell who
26840
+ * owns it without a second lookup.
26841
+ */
26842
+ name: zod.z.string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
26843
+ /** One sentence: what the operator will SEE after arming it. */
26844
+ description: zod.z.string().min(1),
26845
+ /** The level its lines are emitted at. Never below `info`. */
26846
+ defaultLevel: LogChannelLevelSchema,
26847
+ /**
26848
+ * Whether this channel can be narrowed to a camera.
26849
+ *
26850
+ * `true` is a PROMISE with two halves, and both must hold: the gate is
26851
+ * consulted with the numeric device id, AND every line the channel admits
26852
+ * carries `tags: { deviceId }` with that same numeric id. The second half is
26853
+ * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
26854
+ * keeps `deviceId` out of the stream labels for cardinality, so the tag in
26855
+ * the body is the only way to filter.
26856
+ *
26857
+ * A channel whose lines carry the device only in `meta` (or not at all) is
26858
+ * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
26859
+ * the operator narrows to one camera, sees nothing, and concludes the code
26860
+ * path was never taken.
26861
+ */
26862
+ perDevice: zod.z.boolean()
26863
+ });
26864
+ var LogChannelWindowSchema = zod.z.object({
26865
+ channel: zod.z.string().min(1),
26866
+ /** Epoch ms the window closes at. */
26867
+ armedUntilMs: zod.z.number(),
26868
+ /** `null` = every camera. A non-empty list narrows to those numeric ids. */
26869
+ deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable()
26870
+ });
26868
26871
  var UNATTRIBUTED_BUCKET_KEY = "__unattributed__";
26869
26872
  var ROOT_BUCKET_KEY = "__root__";
26870
26873
  function bucketFor(row) {
@@ -28498,12 +28501,30 @@ var require_dist_KQ5D2wqr = __commonJS({
28498
28501
  lastSuccessAt: zod.z.number().optional(),
28499
28502
  /** Newest-archive size from `manifests.json`, or undefined. */
28500
28503
  lastSuccessSizeBytes: zod.z.number().optional(),
28501
- /** Per-destination cron expression. Empty = manual-only (no schedule). */
28504
+ /**
28505
+ * Cron cadence(s) of the ENABLED schedules that fan out to this
28506
+ * destination, comma-joined. Absent when no enabled schedule targets it
28507
+ * — a destination nothing is scheduled to write to must not advertise a
28508
+ * cadence (D384). This is never the `backup_destination_policies.cron`
28509
+ * column: that per-location cron has scheduled nothing since 2026-07-28
28510
+ * and reading it made a destination with a DISABLED schedule claim a
28511
+ * nightly run.
28512
+ */
28502
28513
  cron: zod.z.string().optional(),
28503
- /** ms-epoch of next computed firing for this destination's cron, if any. */
28514
+ /** ms-epoch of the next firing across those schedules (earliest), if any. */
28504
28515
  nextRunAt: zod.z.number().optional(),
28505
- /** ms-epoch of last successful scheduled run (mirrors policy.lastRunAt). */
28506
- lastRunAt: zod.z.number().optional()
28516
+ /**
28517
+ * ms-epoch of the last time a run ATTEMPTED to write here — success or
28518
+ * failure. Never a success stamp: pair it with `lastSuccessAt` (the
28519
+ * newest archive that actually landed) and `lastError`.
28520
+ */
28521
+ lastAttemptAt: zod.z.number().optional(),
28522
+ /**
28523
+ * Why the last attempt failed, verbatim. Absent when the last attempt
28524
+ * landed the archive. A destination that has never been written to has
28525
+ * neither this nor `lastAttemptAt`.
28526
+ */
28527
+ lastError: zod.z.string().optional()
28507
28528
  });
28508
28529
  var BackupArchiveEntrySchema = zod.z.object({
28509
28530
  id: zod.z.string(),
@@ -28625,8 +28646,16 @@ var require_dist_KQ5D2wqr = __commonJS({
28625
28646
  retentionCount: zod.z.number().int().min(1).max(1e3),
28626
28647
  /** Optional subset of source locations to include; omitted = all. */
28627
28648
  dataSources: zod.z.array(zod.z.string()).readonly().optional(),
28628
- /** ms-epoch of last successful run. */
28629
- lastRunAt: zod.z.number().optional(),
28649
+ /**
28650
+ * ms-epoch of the last tick that FIRED this schedule. Stamped before the
28651
+ * archive runs (it is the dedupe anchor), so it says "attempted", never
28652
+ * "succeeded" — a run refused by every destination stamps it too.
28653
+ */
28654
+ lastAttemptAt: zod.z.number().optional(),
28655
+ /** ms-epoch of the last run of this schedule that landed at ≥1 destination. */
28656
+ lastSuccessAt: zod.z.number().optional(),
28657
+ /** Why the last fired run failed, verbatim. Absent when it succeeded. */
28658
+ lastError: zod.z.string().optional(),
28630
28659
  /** ms-epoch of next computed firing (read-only, filled on list). */
28631
28660
  nextRunAt: zod.z.number().optional()
28632
28661
  });
@@ -28749,14 +28778,7 @@ var require_dist_KQ5D2wqr = __commonJS({
28749
28778
  locationId: zod.z.string(),
28750
28779
  enabled: zod.z.boolean(),
28751
28780
  retentionCount: zod.z.number().int().min(1).max(1e3),
28752
- label: zod.z.string().optional(),
28753
- /**
28754
- * Per-destination cron expression. Empty string clears the
28755
- * schedule (manual-only). Validated server-side via croner;
28756
- * malformed expressions reject the upsert with an actionable
28757
- * message.
28758
- */
28759
- cron: zod.z.string().optional()
28781
+ label: zod.z.string().optional()
28760
28782
  }), zod.z.void(), {
28761
28783
  kind: "mutation",
28762
28784
  auth: "admin"
@@ -40651,7 +40673,6 @@ var require_dist_KQ5D2wqr = __commonJS({
40651
40673
  }), zod.z.instanceof(Uint8Array)),
40652
40674
  endDownload: method(zod.z.object({ downloadId: zod.z.string() }), zod.z.void(), { kind: "mutation" }),
40653
40675
  listLocations: method(zod.z.object({ type: StorageLocationTypeSchema.optional() }), zod.z.array(StorageLocationSchema).readonly()),
40654
- getDefaultLocation: method(zod.z.object({ type: StorageLocationTypeSchema }), StorageLocationSchema.nullable()),
40655
40676
  listLocationDeclarations: method(zod.z.void(), zod.z.array(StorageLocationDeclarationSchema).readonly()),
40656
40677
  upsertLocation: method(StorageLocationSchema.omit({
40657
40678
  createdAt: true,
@@ -50687,6 +50708,26 @@ var require_dist_KQ5D2wqr = __commonJS({
50687
50708
  "ffmpeg.threadCount": 0,
50688
50709
  "auth.tokenExpiry": "30d"
50689
50710
  };
50711
+ var AccessoryKind = {
50712
+ Siren: DeviceRole.Siren,
50713
+ Floodlight: DeviceRole.Floodlight,
50714
+ Spotlight: DeviceRole.Spotlight,
50715
+ PirSensor: DeviceRole.PirSensor,
50716
+ Chime: DeviceRole.Chime,
50717
+ Autotrack: DeviceRole.Autotrack,
50718
+ Nightvision: DeviceRole.Nightvision,
50719
+ PrivacyMask: DeviceRole.PrivacyMask
50720
+ };
50721
+ AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
50722
+ function deriveBatteryPresence(input) {
50723
+ const status = input.status;
50724
+ if (!status) return "sleeping";
50725
+ if (status.sleeping !== true) return "awake";
50726
+ const lastContactAt = status.lastContactAt;
50727
+ if (typeof lastContactAt !== "number" || lastContactAt <= 0) return "sleeping";
50728
+ const budget = input.unreachableAfterMs ?? 216e5;
50729
+ return input.nowMs - lastContactAt > budget ? "unreachable" : "sleeping";
50730
+ }
50690
50731
  var CONTAINER_CHILD_PRIORITY = [
50691
50732
  "media-player",
50692
50733
  "alarm-panel",
@@ -50743,26 +50784,6 @@ var require_dist_KQ5D2wqr = __commonJS({
50743
50784
  const namesake = (c) => containerName !== void 0 && containerName.length > 0 && c.name !== void 0 && c.name.toLowerCase() === containerName.toLowerCase() ? 0 : 1;
50744
50785
  return [...children].toSorted((a, b) => roleRank(a.role) - roleRank(b.role) || namesake(a) - namesake(b) || rank(a.type) - rank(b.type))[0] ?? null;
50745
50786
  }
50746
- var AccessoryKind = {
50747
- Siren: DeviceRole.Siren,
50748
- Floodlight: DeviceRole.Floodlight,
50749
- Spotlight: DeviceRole.Spotlight,
50750
- PirSensor: DeviceRole.PirSensor,
50751
- Chime: DeviceRole.Chime,
50752
- Autotrack: DeviceRole.Autotrack,
50753
- Nightvision: DeviceRole.Nightvision,
50754
- PrivacyMask: DeviceRole.PrivacyMask
50755
- };
50756
- AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
50757
- function deriveBatteryPresence(input) {
50758
- const status = input.status;
50759
- if (!status) return "sleeping";
50760
- if (status.sleeping !== true) return "awake";
50761
- const lastContactAt = status.lastContactAt;
50762
- if (typeof lastContactAt !== "number" || lastContactAt <= 0) return "sleeping";
50763
- const budget = input.unreachableAfterMs ?? 216e5;
50764
- return input.nowMs - lastContactAt > budget ? "unreachable" : "sleeping";
50765
- }
50766
50787
  var DECLARED_INTEGRATION_FIXED_KEY = "fixed";
50767
50788
  function declarationOwnerNodeId(localNodeId) {
50768
50789
  const raw = localNodeId ?? "hub";
@@ -56202,12 +56223,6 @@ var require_dist_KQ5D2wqr = __commonJS({
56202
56223
  addonId: null,
56203
56224
  access: "view"
56204
56225
  },
56205
- "storage.getDefaultLocation": {
56206
- capName: "storage",
56207
- capScope: "system",
56208
- addonId: null,
56209
- access: "view"
56210
- },
56211
56226
  "storage.list": {
56212
56227
  capName: "storage",
56213
56228
  capScope: "system",
@@ -60093,6 +60108,12 @@ var require_dist_KQ5D2wqr = __commonJS({
60093
60108
  return LOG_LEVEL_RANK;
60094
60109
  }
60095
60110
  });
60111
+ Object.defineProperty(exports, "LegacyStorageLocationDefaultSchema", {
60112
+ enumerable: true,
60113
+ get: function() {
60114
+ return LegacyStorageLocationDefaultSchema;
60115
+ }
60116
+ });
60096
60117
  Object.defineProperty(exports, "METHOD_ACCESS_MAP", {
60097
60118
  enumerable: true,
60098
60119
  get: function() {
@@ -60423,6 +60444,12 @@ var require_dist_KQ5D2wqr = __commonJS({
60423
60444
  return isDeviceConfigCap;
60424
60445
  }
60425
60446
  });
60447
+ Object.defineProperty(exports, "isLocationEnabled", {
60448
+ enumerable: true,
60449
+ get: function() {
60450
+ return isLocationEnabled;
60451
+ }
60452
+ });
60426
60453
  Object.defineProperty(exports, "isObjectInput", {
60427
60454
  enumerable: true,
60428
60455
  get: function() {
@@ -60675,7 +60702,7 @@ var require_alerts_addon = __commonJS({
60675
60702
  [Symbol.toStringTag]: { value: "Module" }
60676
60703
  });
60677
60704
  require_chunk_Cek0wNdY();
60678
- var require_dist10 = require_dist_KQ5D2wqr();
60705
+ var require_dist10 = require_dist_jZb_HKI5();
60679
60706
  function selectExpired(alerts, cutoffMs) {
60680
60707
  return alerts.filter((a) => a.createdAt < cutoffMs).sort((a, b) => a.createdAt - b.createdAt).map((a) => a.id);
60681
60708
  }
@@ -61494,7 +61521,7 @@ var require_console_logging = __commonJS({
61494
61521
  [Symbol.toStringTag]: { value: "Module" }
61495
61522
  });
61496
61523
  require_chunk_Cek0wNdY();
61497
- var require_dist10 = require_dist_KQ5D2wqr();
61524
+ var require_dist10 = require_dist_jZb_HKI5();
61498
61525
  var require_formatter = require_formatter_DqAKDlvN();
61499
61526
  var LEVEL_RANK = {
61500
61527
  debug: 0,
@@ -61588,7 +61615,7 @@ var require_core_blocks_addon = __commonJS({
61588
61615
  "use strict";
61589
61616
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
61590
61617
  var require_chunk = require_chunk_Cek0wNdY();
61591
- var require_dist10 = require_dist_KQ5D2wqr();
61618
+ var require_dist10 = require_dist_jZb_HKI5();
61592
61619
  var node_crypto = __require("crypto");
61593
61620
  var node_fs_promises = __require("fs/promises");
61594
61621
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -62485,11 +62512,11 @@ var require_core_blocks = __commonJS({
62485
62512
  }
62486
62513
  });
62487
62514
 
62488
- // ../system/dist/retired-settings-keys-2lWk7OeU.js
62489
- var require_retired_settings_keys_2lWk7OeU = __commonJS({
62490
- "../system/dist/retired-settings-keys-2lWk7OeU.js"(exports) {
62515
+ // ../system/dist/retired-settings-keys-Dxv9S_lO.js
62516
+ var require_retired_settings_keys_Dxv9S_lO = __commonJS({
62517
+ "../system/dist/retired-settings-keys-Dxv9S_lO.js"(exports) {
62491
62518
  "use strict";
62492
- var require_dist10 = require_dist_KQ5D2wqr();
62519
+ var require_dist10 = require_dist_jZb_HKI5();
62493
62520
  function settingsStoreIsAuthoritativeHere(env) {
62494
62521
  const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
62495
62522
  return raw === "" || raw === "hub";
@@ -64703,8 +64730,8 @@ var require_device_manager_addon = __commonJS({
64703
64730
  [Symbol.toStringTag]: { value: "Module" }
64704
64731
  });
64705
64732
  require_chunk_Cek0wNdY();
64706
- var require_dist10 = require_dist_KQ5D2wqr();
64707
- var require_retired_settings_keys = require_retired_settings_keys_2lWk7OeU();
64733
+ var require_dist10 = require_dist_jZb_HKI5();
64734
+ var require_retired_settings_keys = require_retired_settings_keys_Dxv9S_lO();
64708
64735
  var node_crypto = __require("crypto");
64709
64736
  var _camstack_types_node = require_node();
64710
64737
  var JOB_HISTORY = 20;
@@ -70329,7 +70356,7 @@ var require_hub_forwarder = __commonJS({
70329
70356
  [Symbol.toStringTag]: { value: "Module" }
70330
70357
  });
70331
70358
  require_chunk_Cek0wNdY();
70332
- var require_dist10 = require_dist_KQ5D2wqr();
70359
+ var require_dist10 = require_dist_jZb_HKI5();
70333
70360
  var require_formatter = require_formatter_DqAKDlvN();
70334
70361
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
70335
70362
  var HubForwarderDestination = class {
@@ -70466,7 +70493,7 @@ var require_liveness_monitor_addon = __commonJS({
70466
70493
  "use strict";
70467
70494
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
70468
70495
  require_chunk_Cek0wNdY();
70469
- var require_dist10 = require_dist_KQ5D2wqr();
70496
+ var require_dist10 = require_dist_jZb_HKI5();
70470
70497
  var NO_DEVICES = "liveness:no-devices";
70471
70498
  var ALL_OFFLINE = "liveness:all-devices-offline";
70472
70499
  var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
@@ -70662,7 +70689,7 @@ var require_local_auth_addon = __commonJS({
70662
70689
  [Symbol.toStringTag]: { value: "Module" }
70663
70690
  });
70664
70691
  var require_chunk = require_chunk_Cek0wNdY();
70665
- var require_dist10 = require_dist_KQ5D2wqr();
70692
+ var require_dist10 = require_dist_jZb_HKI5();
70666
70693
  var node_crypto = __require("crypto");
70667
70694
  node_crypto = require_chunk.__toESM(node_crypto);
70668
70695
  var crypto$1 = __require("crypto");
@@ -78475,7 +78502,7 @@ var require_loki_logging = __commonJS({
78475
78502
  [Symbol.toStringTag]: { value: "Module" }
78476
78503
  });
78477
78504
  require_chunk_Cek0wNdY();
78478
- var require_dist10 = require_dist_KQ5D2wqr();
78505
+ var require_dist10 = require_dist_jZb_HKI5();
78479
78506
  function sanitizeLabelName(raw) {
78480
78507
  const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
78481
78508
  return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
@@ -79040,7 +79067,7 @@ var require_native_metrics_addon = __commonJS({
79040
79067
  [Symbol.toStringTag]: { value: "Module" }
79041
79068
  });
79042
79069
  var require_chunk = require_chunk_Cek0wNdY();
79043
- var require_dist10 = require_dist_KQ5D2wqr();
79070
+ var require_dist10 = require_dist_jZb_HKI5();
79044
79071
  var node_fs_promises = __require("fs/promises");
79045
79072
  var node_child_process = __require("child_process");
79046
79073
  var node_util = __require("util");
@@ -81624,7 +81651,7 @@ var require_filesystem_storage_addon = __commonJS({
81624
81651
  [Symbol.toStringTag]: { value: "Module" }
81625
81652
  });
81626
81653
  var require_chunk = require_chunk_Cek0wNdY();
81627
- var require_dist10 = require_dist_KQ5D2wqr();
81654
+ var require_dist10 = require_dist_jZb_HKI5();
81628
81655
  var node_crypto = __require("crypto");
81629
81656
  var node_fs_promises = __require("fs/promises");
81630
81657
  var node_path = __require("path");
@@ -82740,8 +82767,8 @@ var require_sqlite_settings_addon = __commonJS({
82740
82767
  [Symbol.toStringTag]: { value: "Module" }
82741
82768
  });
82742
82769
  var require_chunk = require_chunk_Cek0wNdY();
82743
- var require_dist10 = require_dist_KQ5D2wqr();
82744
- var require_retired_settings_keys = require_retired_settings_keys_2lWk7OeU();
82770
+ var require_dist10 = require_dist_jZb_HKI5();
82771
+ var require_retired_settings_keys = require_retired_settings_keys_Dxv9S_lO();
82745
82772
  var node_crypto = __require("crypto");
82746
82773
  var node_fs = __require("fs");
82747
82774
  var node_module = __require("module");
@@ -85231,7 +85258,7 @@ var require_storage_orchestrator_addon = __commonJS({
85231
85258
  [Symbol.toStringTag]: { value: "Module" }
85232
85259
  });
85233
85260
  var require_chunk = require_chunk_Cek0wNdY();
85234
- var require_dist10 = require_dist_KQ5D2wqr();
85261
+ var require_dist10 = require_dist_jZb_HKI5();
85235
85262
  var require_hub_cap_forward = require_hub_cap_forward_DmHNjbB0();
85236
85263
  var zod = require_zod();
85237
85264
  var node_crypto = __require("crypto");
@@ -85378,7 +85405,7 @@ var require_storage_orchestrator_addon = __commonJS({
85378
85405
  display_name: loc.displayName,
85379
85406
  provider_id: loc.providerId,
85380
85407
  config: storedConfig,
85381
- is_default: loc.isDefault ? 1 : 0,
85408
+ is_default: 0,
85382
85409
  is_system: loc.isSystem ? 1 : 0,
85383
85410
  created_at: loc.createdAt,
85384
85411
  updated_at: loc.updatedAt
@@ -85407,7 +85434,7 @@ var require_storage_orchestrator_addon = __commonJS({
85407
85434
  config,
85408
85435
  ...nodeId !== void 0 ? { nodeId } : {},
85409
85436
  ...enabled !== void 0 ? { enabled } : {},
85410
- isDefault: row["is_default"] === 1 || row["is_default"] === true,
85437
+ ...require_dist10.LegacyStorageLocationDefaultSchema.parse({ isDefault: row["is_default"] === 1 || row["is_default"] === true }),
85411
85438
  isSystem: row["is_system"] === 1 || row["is_system"] === true,
85412
85439
  createdAt: Number(row["created_at"] ?? 0),
85413
85440
  updatedAt: Number(row["updated_at"] ?? 0)
@@ -85456,16 +85483,18 @@ var require_storage_orchestrator_addon = __commonJS({
85456
85483
  const targetId = input.destinations[storageClass];
85457
85484
  if (targetId === void 0) continue;
85458
85485
  const namedSourceId = input.sources?.[storageClass];
85459
- const defaultLoc = this.deps.locations.getDefaultLocation(storageClass);
85460
- const source = namedSourceId ? this.deps.locations.getLocationById(namedSourceId) : defaultLoc;
85461
- if (!source) throw new Error(namedSourceId ? `Storage location "${namedSourceId}" not found` : `No default storage location for "${storageClass}"`);
85486
+ const writeSet = this.deps.locations.writeTargets(storageClass);
85487
+ const implicitSources = writeSet.filter((loc) => loc.id !== targetId);
85488
+ if (!namedSourceId && implicitSources.length > 1) throw new Error(`"${storageClass}" writes to ${String(implicitSources.length)} locations (${implicitSources.map((loc) => loc.id).join(", ")}) \u2014 name the one to cut over explicitly with \`sources\`.`);
85489
+ const source = namedSourceId ? this.deps.locations.getLocationById(namedSourceId) : implicitSources[0];
85490
+ if (!source) throw new Error(namedSourceId ? `Storage location "${namedSourceId}" not found` : `Storage location "${targetId}" is already the only write target for "${storageClass}" \u2014 there is nothing to cut over. Use storageMigration.drain to move what is left behind.`);
85462
85491
  if (source.type !== storageClass) throw new Error(`Storage location "${source.id}" is type "${source.type}", expected "${storageClass}"`);
85463
85492
  const target = this.deps.locations.getLocationById(targetId);
85464
85493
  if (!target) throw new Error(`Storage location "${targetId}" not found`);
85465
85494
  if (target.type !== storageClass) throw new Error(`Storage location "${targetId}" is type "${target.type}", expected "${storageClass}"`);
85466
- 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`);
85467
- const freezeSource = defaultLoc === null || source.id !== defaultLoc.id;
85468
- if (!MOVER_CLASSES.includes(storageClass)) throw new Error(`No mover owns "${storageClass}" \u2014 the migration can repoint its default but cannot move its bytes. Move it by hand, then repoint the default with upsertLocation.`);
85495
+ 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}"`);
85496
+ const freezeSource = !writeSet.some((loc) => loc.id === source.id);
85497
+ if (!MOVER_CLASSES.includes(storageClass)) throw new Error(`No mover owns "${storageClass}" \u2014 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.`);
85469
85498
  moves.push({
85470
85499
  storageClass,
85471
85500
  fromLocationId: source.id,
@@ -85482,7 +85511,7 @@ var require_storage_orchestrator_addon = __commonJS({
85482
85511
  if (moves.some((move) => move.storageClass === "eventMedia")) {
85483
85512
  const unstamped = await this.deps.participants.analytics.countUnstamped();
85484
85513
  if (unstamped === null) {
85485
- 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.");
85514
+ 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.");
85486
85515
  findings.push({
85487
85516
  code: "unstampedEventMediaRows",
85488
85517
  storageClass: "eventMedia",
@@ -85491,7 +85520,7 @@ var require_storage_orchestrator_addon = __commonJS({
85491
85520
  } else if (unstamped.anyPresent) {
85492
85521
  const scale = unstamped.total === null ? "an unknown number of" : unstamped.total.toString();
85493
85522
  const perLane = `${describeUnstamped(unstamped.media)} media, ${describeUnstamped(unstamped.retrainFrames)} retrain frames`;
85494
- 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"}" }).`);
85523
+ 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"}" }).`);
85495
85524
  findings.push({
85496
85525
  code: "unstampedEventMediaRows",
85497
85526
  storageClass: "eventMedia",
@@ -85630,7 +85659,7 @@ var require_storage_orchestrator_addon = __commonJS({
85630
85659
  * remaining" action.
85631
85660
  *
85632
85661
  * A class appears here only when something is (or might be) left on a
85633
- * location that is not its default. An empty result therefore means exactly
85662
+ * location outside its write set. An empty result therefore means exactly
85634
85663
  * "there is nothing to drain", which is what lets the UI offer the action
85635
85664
  * only when it is true, and what lets {@link drain} refuse rather than start
85636
85665
  * a job that would move nothing and report `done` — the failure mode D295
@@ -85644,11 +85673,13 @@ var require_storage_orchestrator_addon = __commonJS({
85644
85673
  const out = [];
85645
85674
  for (const storageClass of STORAGE_CLASSES) {
85646
85675
  if (!MOVER_CLASSES.includes(storageClass)) continue;
85647
- const target = this.deps.locations.getDefaultLocation(storageClass);
85676
+ const writeSet = this.deps.locations.writeTargets(storageClass);
85677
+ const target = writeSet[0];
85648
85678
  if (!target) continue;
85679
+ const inWriteSet = new Set(writeSet.map((loc) => loc.id));
85649
85680
  if (laneOf(storageClass) === "media") {
85650
85681
  for (const source of this.deps.locations.listLocations({ type: storageClass })) {
85651
- if (source.id === target.id) continue;
85682
+ if (inWriteSet.has(source.id)) continue;
85652
85683
  const count = await unanswerable(this.deps.participants.analytics.residue({
85653
85684
  toLocationId: target.id,
85654
85685
  fromLocationId: source.id,
@@ -85666,7 +85697,7 @@ var require_storage_orchestrator_addon = __commonJS({
85666
85697
  continue;
85667
85698
  }
85668
85699
  for (const source of this.deps.locations.listLocations({ type: storageClass })) {
85669
- if (source.id === target.id) continue;
85700
+ if (inWriteSet.has(source.id)) continue;
85670
85701
  const census = await unanswerable(this.deps.participants.recorder.residue({
85671
85702
  fromLocationId: source.id,
85672
85703
  footageClass: storageClass === "recordingsLow" ? "recordingsLow" : "recordings"
@@ -85684,12 +85715,12 @@ var require_storage_orchestrator_addon = __commonJS({
85684
85715
  return out;
85685
85716
  }
85686
85717
  /**
85687
- * Run the DRAIN half alone, against classes whose default has already moved.
85718
+ * Run the DRAIN half alone, against classes whose write set has already moved.
85688
85719
  *
85689
85720
  * ## Why this is a second verb rather than a looser `start`
85690
85721
  *
85691
- * `start` refuses a destination that is already the class's default
85692
- * (`"recordingsLow:ssd" is already the "recordingsLow" default`). That refusal
85722
+ * `start` refuses a destination that is already the class's only write target
85723
+ * (`"recordingsLow:ssd" is already the only write target for "recordingsLow"`). That refusal
85693
85724
  * is correct and it is load-bearing: there is genuinely nothing left to
85694
85725
  * repoint, and an operator must never be able to re-repoint a migrated class
85695
85726
  * by accident. Making `start` idempotent — "an already-repointed class
@@ -85698,7 +85729,7 @@ var require_storage_orchestrator_addon = __commonJS({
85698
85729
  * operator reads ("pauses the writers…") would be a lie half the time.
85699
85730
  *
85700
85731
  * `drain` instead cannot repoint AT ALL: it never touches
85701
- * `setDefaultLocations`, and its job starts in `draining` with `repointed`
85732
+ * `setWriteTargets`, and its job starts in `draining` with `repointed`
85702
85733
  * already true, so the `repointing` / `refreshing` / `resuming` blocks of
85703
85734
  * {@link run} are behind it and unreachable. The two verbs are disjoint, and
85704
85735
  * `start`'s refusal keeps meaning exactly what it meant.
@@ -85708,7 +85739,7 @@ var require_storage_orchestrator_addon = __commonJS({
85708
85739
  * The finished job is the audit of what happened; re-opening it destroys
85709
85740
  * that. And a drain is needed in cases where no migration job ever existed
85710
85741
  * (a mover armed by hand, footage stranded on a location an operator added
85711
- * and then un-defaulted). One job = one operation, and the work list comes
85742
+ * and then disabled again). One job = one operation, and the work list comes
85712
85743
  * from {@link residue} — the archive — not from what a previous job believed.
85713
85744
  */
85714
85745
  async drain(input) {
@@ -85731,9 +85762,9 @@ var require_storage_orchestrator_addon = __commonJS({
85731
85762
  const destinations = {};
85732
85763
  for (const storageClass of input.classes) {
85733
85764
  const remaining = residue.filter((entry) => entry.storageClass === storageClass);
85734
- if (remaining.length === 0) throw new Error(`"${storageClass}" has nothing left outside its default location \u2014 there is nothing to drain. To CHANGE where it writes, use storageMigration.start with a new destination.`);
85765
+ if (remaining.length === 0) throw new Error(`"${storageClass}" has nothing left outside its write set \u2014 there is nothing to drain. To CHANGE where it writes, use storageMigration.start with a new destination.`);
85735
85766
  for (const entry of remaining) {
85736
- if (this.deps.locations.getLocationById(entry.toLocationId)?.enabled === false) throw new Error(`The "${storageClass}" drain destination "${entry.toLocationId}" is DISABLED \u2014 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.`);
85767
+ if (this.deps.locations.getLocationById(entry.toLocationId)?.enabled === false) throw new Error(`The "${storageClass}" drain destination "${entry.toLocationId}" is DISABLED \u2014 a disabled location takes no writes, and a drain is the largest write there is. Enable it on the Storage screen first.`);
85737
85768
  moves.push({
85738
85769
  storageClass,
85739
85770
  fromLocationId: entry.fromLocationId,
@@ -85851,8 +85882,13 @@ var require_storage_orchestrator_addon = __commonJS({
85851
85882
  await this.setPhase(job, "repointing");
85852
85883
  }
85853
85884
  if (job.phase === "repointing") {
85854
- const targets = new Map(job.moves.filter((move) => move.freezeSource !== true).map((move) => [move.storageClass, move.toLocationId]));
85855
- if (targets.size > 0) await this.deps.locations.setDefaultLocations(targets);
85885
+ const targets = /* @__PURE__ */ new Map();
85886
+ for (const move of job.moves) {
85887
+ if (move.freezeSource === true) continue;
85888
+ targets.set(move.toLocationId, true);
85889
+ targets.set(move.fromLocationId, false);
85890
+ }
85891
+ if (targets.size > 0) await this.deps.locations.setWriteTargets(targets);
85856
85892
  job.repointed = true;
85857
85893
  await this.setPhase(job, "refreshing");
85858
85894
  }
@@ -86052,9 +86088,9 @@ var require_storage_orchestrator_addon = __commonJS({
86052
86088
  for (const move of job.moves) if (move.state !== "done") throw new Error(`${move.storageClass} move did not complete verification`);
86053
86089
  }
86054
86090
  /**
86055
- * After a from→to copy whose source was NOT the class default: stop new
86056
- * writes to `from`. The default is unchanged (that is the whole point of
86057
- * naming a non-default source). A location that is already disabled is
86091
+ * After a from→to copy whose source was NOT a write target: stop new writes
86092
+ * to `from`. The rest of the write set is unchanged (that is the whole point
86093
+ * of naming a source outside it). A location that is already disabled is
86058
86094
  * left alone.
86059
86095
  */
86060
86096
  freezeDrainedSources(job) {
@@ -86652,33 +86688,77 @@ var require_storage_orchestrator_addon = __commonJS({
86652
86688
  backfilled: backfill.length,
86653
86689
  total: this.locations.size
86654
86690
  } });
86655
- this.reportDisabledDefaults();
86691
+ this.migrateOntoTheWriteSet(rows);
86656
86692
  }
86657
86693
  /**
86658
- * Shout about a persisted row that is BOTH the type default and disabled.
86694
+ * ONE explicit migration onto the enabled write set (D383), run once per
86695
+ * boot against the rows that came off disk. Idempotent: the second boot finds
86696
+ * nothing to do.
86659
86697
  *
86660
- * `upsertLocation` can now produce this (enabled is the write set; `isDefault`
86661
- * is store residue). Hydrate also writes rows straight into the map, so a
86662
- * hand-edited store or a row written by another build can too. It matters
86663
- * because every bare-type ref still resolves through {@link getDefaultLocation}
86664
- * until Stage 5 of the enabled-write-set design: on such a row
86665
- * `storage.write({ location: 'eventMedia' })` and the migration coordinator's
86666
- * drain destination both point at a disk the operator turned off.
86698
+ * Two steps, in order, and each one is a WRITE — the point is that "absent"
86699
+ * stops existing rather than being re-interpreted on every read forever:
86667
86700
  *
86668
- * Reported, never repaired: flipping an operator's flag back on at boot would
86669
- * be the system overruling the switch instead of obeying it. The coordinator
86670
- * refuses the drain (`storage-migration-coordinator.drain`); the bare-ref read
86671
- * path is deliberately left working, because narrowing a READ is a worse bug
86672
- * than the one this line reports.
86673
- */
86674
- reportDisabledDefaults() {
86701
+ * 1. A flagless row (persisted before `enabled` existed) is stamped
86702
+ * `enabled: true`. Absence already meant active, so this preserves
86703
+ * behaviour exactly; it is the operator's "the initial locations are
86704
+ * enabled" spelled into the store.
86705
+ * 2. A type left with NO enabled row — only reachable from a hand-edited
86706
+ * store or a row an older build wrote — gets one enabled back, chosen as
86707
+ * the row the RETIRED default flag named, else the `:default`-slugged
86708
+ * row, else the lexicographically first. Loudly: a class with no write
86709
+ * target records nowhere, and refusing to repair it here would leave the
86710
+ * hub silently unable to write with no operator action pending.
86711
+ *
86712
+ * An operator's explicit `enabled: false` is never reversed by step 1 — only
86713
+ * step 2 touches it, and only when the alternative is a class that cannot
86714
+ * write at all.
86715
+ */
86716
+ migrateOntoTheWriteSet(rows) {
86717
+ const legacyDefaultOfType = /* @__PURE__ */ new Map();
86718
+ for (const row of rows) if (row.isDefault === true) legacyDefaultOfType.set(row.type, row.id);
86719
+ const stamped = [];
86675
86720
  for (const loc of this.locations.values()) {
86676
- if (!loc.isDefault || loc.enabled !== false) continue;
86677
- this.logger.error("storage-orchestrator: the type DEFAULT is disabled \u2014 every bare-type ref for this type resolves to a location the operator turned off. Enable it, or make another location the default.", { meta: {
86678
- id: loc.id,
86679
- type: loc.type
86680
- } });
86721
+ if (loc.enabled !== void 0) continue;
86722
+ this.persistMigrated({
86723
+ ...loc,
86724
+ enabled: true
86725
+ });
86726
+ stamped.push(loc.id);
86727
+ }
86728
+ const repaired = [];
86729
+ for (const type of new Set([...this.locations.values()].map((l) => l.type))) {
86730
+ const ofType = [...this.locations.values()].filter((l) => l.type === type);
86731
+ if (ofType.some((l) => require_dist10.isLocationEnabled(l))) continue;
86732
+ const legacyDefault = legacyDefaultOfType.get(type);
86733
+ 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];
86734
+ if (!pick) continue;
86735
+ this.persistMigrated({
86736
+ ...pick,
86737
+ enabled: true
86738
+ });
86739
+ repaired.push(pick.id);
86681
86740
  }
86741
+ if (stamped.length > 0 || repaired.length > 0) this.logger.info("storage-orchestrator: migrated locations onto the enabled write set", { meta: {
86742
+ stamped,
86743
+ repaired
86744
+ } });
86745
+ for (const id of repaired) this.logger.warn("storage-orchestrator: a storage type had NO enabled location \u2014 re-enabled one so it can write. Every other location of that type stays read-only until you enable it.", { meta: { id } });
86746
+ }
86747
+ /** Apply one migrated row in memory and mirror it to the store. */
86748
+ persistMigrated(next) {
86749
+ const withStamp = {
86750
+ ...next,
86751
+ updatedAt: Date.now()
86752
+ };
86753
+ this.locations.set(withStamp.id, withStamp);
86754
+ const store = this.locationStore;
86755
+ if (!store) return;
86756
+ store.upsert(withStamp).catch((err) => {
86757
+ this.logger.error("storage-orchestrator: write-set migration persist failed", { meta: {
86758
+ id: withStamp.id,
86759
+ error: err instanceof Error ? err.message : String(err)
86760
+ } });
86761
+ });
86682
86762
  }
86683
86763
  /**
86684
86764
  * Inject the declaration-driven cardinality source. Called once by the
@@ -86733,7 +86813,7 @@ var require_storage_orchestrator_addon = __commonJS({
86733
86813
  const rows = await this.locationStore.loadAll();
86734
86814
  for (const loc of rows) this.locations.set(loc.id, loc);
86735
86815
  this.logger.info("storage-orchestrator: hydrated locations from store", { meta: { loaded: this.locations.size } });
86736
- this.reportDisabledDefaults();
86816
+ this.migrateOntoTheWriteSet(rows);
86737
86817
  }
86738
86818
  /**
86739
86819
  * Boot backfill (SP1): stamp `nodeId` on every persisted node-local
@@ -86782,40 +86862,51 @@ var require_storage_orchestrator_addon = __commonJS({
86782
86862
  const all = [...this.locations.values()];
86783
86863
  return filter?.type ? all.filter((l) => l.type === filter.type) : all;
86784
86864
  }
86785
- getDefaultLocation(type) {
86786
- for (const loc of this.locations.values()) if (loc.type === type && loc.isDefault) return loc;
86787
- return null;
86788
- }
86789
86865
  /**
86790
- * Repoint several logical defaults as one in-memory commit. The migration
86791
- * coordinator persists its `repointing` phase before calling this method, so
86792
- * a process crash during the backing-store writes is recovered by replaying
86793
- * the same idempotent target set before anyone resumes writers.
86866
+ * The WRITE SET of a type: every enabled location of it, in stable id order.
86867
+ * This replaced `getDefaultLocation` (D383) — a type does not have one write
86868
+ * target any more, it has a pool, and every consumer that must pick one picks
86869
+ * from here by its own rule (the recorder by headroom, a drain by an operator
86870
+ * -named destination).
86794
86871
  */
86795
- async setDefaultLocations(targetByType) {
86796
- if (targetByType.size === 0) return;
86872
+ writeTargets(type) {
86873
+ return [...this.locations.values()].filter((loc) => loc.type === type && require_dist10.isLocationEnabled(loc)).toSorted((a, b) => a.id.localeCompare(b.id));
86874
+ }
86875
+ /**
86876
+ * Change the WRITE SET as one compensated commit: each named location is
86877
+ * enabled (it joins the pool) or disabled (it becomes read-only — still read,
86878
+ * still played back, still age-swept, still drained).
86879
+ *
86880
+ * This is the cutover verb, and its only caller is the migration coordinator,
86881
+ * which persists its `repointing` phase BEFORE calling so a crash mid-write
86882
+ * is recovered by replaying the same idempotent set before writers resume.
86883
+ *
86884
+ * It moves ONLY the locations it is given: a cutover from A to C on a class
86885
+ * that also writes to B leaves B in the pool, because the operator's pool is
86886
+ * the authority and a migration is not a licence to shrink it. The commit is
86887
+ * refused when it would leave a write-serving type with nothing enabled —
86888
+ * the same refusal `upsertLocation` applies, judged on the RESULT so an
86889
+ * enable and a disable in one commit are weighed together.
86890
+ */
86891
+ async setWriteTargets(enabledByLocationId) {
86892
+ if (enabledByLocationId.size === 0) return;
86797
86893
  const now = Date.now();
86798
86894
  const nextById = new Map(this.locations);
86799
86895
  const touched = /* @__PURE__ */ new Map();
86800
- for (const [type, targetId] of targetByType) {
86801
- const target = nextById.get(targetId);
86802
- if (!target) throw new Error(`Storage location "${targetId}" not found`);
86803
- if (target.type !== type) throw new Error(`Storage location "${targetId}" is type "${target.type}", expected "${type}"`);
86804
- for (const [id, location] of nextById) {
86805
- if (location.type !== type) continue;
86806
- const isDefault = id === targetId;
86807
- const enable = isDefault && location.enabled === false;
86808
- if (location.isDefault === isDefault && !enable) continue;
86809
- const updated = {
86810
- ...location,
86811
- isDefault,
86812
- ...enable ? { enabled: true } : {},
86813
- updatedAt: now
86814
- };
86815
- nextById.set(id, updated);
86816
- touched.set(id, updated);
86817
- }
86896
+ for (const [id, enabled] of enabledByLocationId) {
86897
+ const location = nextById.get(id);
86898
+ if (!location) throw new Error(`Storage location "${id}" not found`);
86899
+ if (location.enabled === enabled) continue;
86900
+ const updated = {
86901
+ ...location,
86902
+ enabled,
86903
+ updatedAt: now
86904
+ };
86905
+ nextById.set(id, updated);
86906
+ touched.set(id, updated);
86818
86907
  }
86908
+ if (touched.size === 0) return;
86909
+ refuseTypeWithoutWriteTarget(touched.values(), nextById.values());
86819
86910
  if (this.locationStore) {
86820
86911
  const previous = /* @__PURE__ */ new Map();
86821
86912
  for (const id of touched.keys()) {
@@ -86836,13 +86927,10 @@ var require_storage_orchestrator_addon = __commonJS({
86836
86927
  for (const [id, location] of nextById) this.locations.set(id, location);
86837
86928
  }
86838
86929
  /**
86839
- * Insert or update a location. If `isDefault: true`, atomically
86840
- * demotes any other default for the same type — operators always see
86841
- * exactly one default per type.
86930
+ * Insert or update a location.
86842
86931
  *
86843
- * When a persistence store is wired (Task 6), the new record AND any
86844
- * implicitly-demoted siblings are persisted before the in-memory map
86845
- * mutation returns. Persistence errors propagate to the caller.
86932
+ * When a persistence store is wired (Task 6), the record is mirrored to it;
86933
+ * the mirror is fire-and-forget and its errors reach the logger.
86846
86934
  */
86847
86935
  /**
86848
86936
  * Refuse a `(location kind, provider)` pair the kind cannot use.
@@ -86928,30 +87016,17 @@ var require_storage_orchestrator_addon = __commonJS({
86928
87016
  updatedAt: now
86929
87017
  };
86930
87018
  refuseLastWriteTargetDisabled(existing, next, this.locations.values());
86931
- const demoted = [];
86932
- if (next.isDefault) {
86933
- for (const [otherId, otherLoc] of this.locations) if (otherLoc.type === next.type && otherLoc.id !== next.id && otherLoc.isDefault) {
86934
- const updated = {
86935
- ...otherLoc,
86936
- isDefault: false,
86937
- updatedAt: now
86938
- };
86939
- this.locations.set(otherId, updated);
86940
- demoted.push(updated);
86941
- }
86942
- }
86943
87019
  this.locations.set(next.id, next);
86944
87020
  this.logger.debug("storage-orchestrator: upsertLocation", { meta: {
86945
87021
  id: next.id,
86946
87022
  type: next.type,
86947
87023
  providerId: next.providerId,
86948
- isDefault: next.isDefault
87024
+ enabled: next.enabled
86949
87025
  } });
86950
87026
  if (this.locationStore) {
86951
87027
  const store = this.locationStore;
86952
87028
  (async () => {
86953
87029
  try {
86954
- for (const d of demoted) await store.upsert(d);
86955
87030
  await store.upsert(next);
86956
87031
  } catch (err) {
86957
87032
  this.logger.error("storage-orchestrator: upsert persistence failed", { meta: {
@@ -86966,8 +87041,7 @@ var require_storage_orchestrator_addon = __commonJS({
86966
87041
  /**
86967
87042
  * Remove a location. Refuses the last location of a type and the last
86968
87043
  * enabled location of a type. `isSystem` is informational — a frozen
86969
- * recordings `:default` is deletable once a sibling is live. Deleting
86970
- * the flagged default promotes an enabled sibling. Occupancy still
87044
+ * recordings `:default` is deletable once a sibling is live. Occupancy still
86971
87045
  * refuses unless `force` is set; force never overrides uniqueness /
86972
87046
  * last-enabled.
86973
87047
  */
@@ -86978,8 +87052,6 @@ var require_storage_orchestrator_addon = __commonJS({
86978
87052
  if (ofType.length <= 1) throw new Error(`Cannot delete "${id}" \u2014 it is the only location for type "${loc.type}"`);
86979
87053
  const enabledOfType = ofType.filter((l) => l.enabled !== false);
86980
87054
  if (loc.enabled !== false && enabledOfType.length <= 1) throw new Error(`Cannot delete "${id}" \u2014 it is the only enabled location for type "${loc.type}"`);
86981
- const successor = loc.isDefault === true ? ofType.find((l) => l.id !== id && l.enabled !== false) : void 0;
86982
- if (loc.isDefault === true && successor === void 0) throw new Error(`Cannot delete "${id}" \u2014 it is the only enabled location for type "${loc.type}"`);
86983
87055
  const occupancy = await this.probeOccupancy(loc);
86984
87056
  if (occupancy !== "empty") {
86985
87057
  if (options?.force !== true) throw new Error(occupancy === "occupied" ? `Storage location "${id}" still holds data \u2014 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) \u2014 pass force to delete the record anyway`);
@@ -86989,21 +87061,6 @@ var require_storage_orchestrator_addon = __commonJS({
86989
87061
  occupancy
86990
87062
  } });
86991
87063
  }
86992
- if (successor !== void 0 && successor.isDefault !== true) {
86993
- const now = Date.now();
86994
- const promoted = {
86995
- ...successor,
86996
- isDefault: true,
86997
- updatedAt: now
86998
- };
86999
- this.locations.set(successor.id, promoted);
87000
- if (this.locationStore) this.locationStore.upsert(promoted).catch((err) => {
87001
- this.logger.error("storage-orchestrator: default promotion persist failed", { meta: {
87002
- id: successor.id,
87003
- error: err instanceof Error ? err.message : String(err)
87004
- } });
87005
- });
87006
- }
87007
87064
  this.locations.delete(id);
87008
87065
  if (this.locationStore) this.locationStore.delete(id).catch((err) => {
87009
87066
  this.logger.error("storage-orchestrator: delete persistence failed", { meta: {
@@ -87082,9 +87139,17 @@ var require_storage_orchestrator_addon = __commonJS({
87082
87139
  if (!loc) throw new Error(`Storage location "${ref}" not found`);
87083
87140
  return loc;
87084
87141
  }
87085
- const def = this.getDefaultLocation(ref);
87086
- if (!def) throw new Error(`No default storage location configured for type "${ref}" \u2014 register one or pass a fully-qualified id (\`<type>:<slug>\`)`);
87087
- return def;
87142
+ const ofType = [...this.locations.values()].filter((loc) => loc.type === ref);
87143
+ if (ofType.length === 1) return ofType[0];
87144
+ if (ofType.length === 0) throw new Error(`No storage location configured for type "${ref}" \u2014 register one or pass a fully-qualified id (\`<type>:<slug>\`)`);
87145
+ const slugged = ofType.find((loc) => loc.id === `${ref}:default`);
87146
+ if (!slugged) throw new Error(`Storage type "${ref}" has ${String(ofType.length)} locations (${ofType.map((loc) => loc.id).toSorted().join(", ")}) \u2014 a bare type ref is ambiguous, pass a fully-qualified id (\`<type>:<slug>\`)`);
87147
+ this.logger.warn("storage-orchestrator: DEPRECATED bare-type ref on a type with several locations \u2014 resolved to the legacy `:default`-slugged row. Pass a fully-qualified location id.", { meta: {
87148
+ ref,
87149
+ resolved: slugged.id,
87150
+ candidates: ofType.length
87151
+ } });
87152
+ return slugged;
87088
87153
  }
87089
87154
  /**
87090
87155
  * Find the storage-provider that backs a given location — the entry point
@@ -87188,9 +87253,11 @@ var require_storage_orchestrator_addon = __commonJS({
87188
87253
  * declarations on a populated map is a no-op (each declared `id`
87189
87254
  * already resolves to an existing `<id>:default`).
87190
87255
  *
87191
- * Convention: the bare-type ref `'recordings'` resolves via
87192
- * `getDefaultLocation('recordings')` (walks `isDefault === true`); the
87193
- * actual stored `id` is `'recordings:default'` so it satisfies the
87256
+ * The seeded row is born ENABLED — a fresh install must be able to write the
87257
+ * moment it boots, and `enabled` is the only write switch there is (D383).
87258
+ * The `:default` suffix is a NAME kept for idempotency across existing
87259
+ * installs, not a flag: the stored `id` is `'recordings:default'` so it
87260
+ * satisfies the
87194
87261
  * `^[a-z][a-z0-9-]*:[a-z0-9-]+$` regex on `StorageLocationSchema.id`.
87195
87262
  *
87196
87263
  * `basePath` is the storage root (e.g. `<CAMSTACK_DATA>`). Each
@@ -87215,7 +87282,7 @@ var require_storage_orchestrator_addon = __commonJS({
87215
87282
  displayName: d.displayName,
87216
87283
  providerId: input.providerId,
87217
87284
  config: { basePath: base },
87218
- isDefault: true,
87285
+ enabled: true,
87219
87286
  isSystem: false
87220
87287
  });
87221
87288
  added++;
@@ -87235,7 +87302,7 @@ var require_storage_orchestrator_addon = __commonJS({
87235
87302
  const requested = input.enabled ?? existing?.enabled;
87236
87303
  if (requested === void 0) {
87237
87304
  if (existing) return {};
87238
- return { enabled: input.isDefault || !typeAlreadyHasLocation };
87305
+ return { enabled: !typeAlreadyHasLocation };
87239
87306
  }
87240
87307
  return { enabled: requested };
87241
87308
  }
@@ -87251,6 +87318,16 @@ var require_storage_orchestrator_addon = __commonJS({
87251
87318
  }
87252
87319
  throw new Error(`Cannot disable "${next.id}": it is the last enabled location of type "${next.type}". Enable another location of this type first.`);
87253
87320
  }
87321
+ function refuseTypeWithoutWriteTarget(touched, result) {
87322
+ const rows = [...result];
87323
+ for (const type of new Set([...touched].map((loc) => loc.type))) {
87324
+ if (OPTIONAL_WRITE_TYPES.has(type)) continue;
87325
+ const ofType = rows.filter((loc) => loc.type === type);
87326
+ if (ofType.length === 0) continue;
87327
+ if (ofType.some((loc) => require_dist10.isLocationEnabled(loc))) continue;
87328
+ 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.`);
87329
+ }
87330
+ }
87254
87331
  var DEFAULT_MAX_ROUNDS = 8;
87255
87332
  var DEFAULT_HYSTERESIS_PERCENT = 1;
87256
87333
  var StoragePressureManager = class {
@@ -87455,13 +87532,9 @@ var require_storage_orchestrator_addon = __commonJS({
87455
87532
  const rows = type !== void 0 ? service.listLocations({ type }) : service.listLocations();
87456
87533
  return Promise.all(rows.map(async (loc) => ({
87457
87534
  ...this.redacted(loc),
87458
- capacity: loc.enabled === false ? null : await this.localCapacityOf(loc)
87535
+ capacity: require_dist10.isLocationEnabled(loc) ? await this.localCapacityOf(loc) : null
87459
87536
  })));
87460
87537
  },
87461
- getDefaultLocation: async ({ type }) => {
87462
- const loc = service.getDefaultLocation(type);
87463
- return loc === null ? null : this.redacted(loc);
87464
- },
87465
87538
  listLocationDeclarations: async () => service.listDeclarations(),
87466
87539
  upsertLocation: async (input) => {
87467
87540
  const stored = service.getLocationById(input.id);
@@ -88182,7 +88255,7 @@ var require_system_config_addon = __commonJS({
88182
88255
  [Symbol.toStringTag]: { value: "Module" }
88183
88256
  });
88184
88257
  require_chunk_Cek0wNdY();
88185
- var require_dist10 = require_dist_KQ5D2wqr();
88258
+ var require_dist10 = require_dist_jZb_HKI5();
88186
88259
  var SECTION_TITLES = {
88187
88260
  server: "Server",
88188
88261
  auth: "Authentication"
@@ -106243,7 +106316,7 @@ var require_winston_logging = __commonJS({
106243
106316
  [Symbol.toStringTag]: { value: "Module" }
106244
106317
  });
106245
106318
  var require_chunk = require_chunk_Cek0wNdY();
106246
- var require_dist10 = require_dist_KQ5D2wqr();
106319
+ var require_dist10 = require_dist_jZb_HKI5();
106247
106320
  var require_formatter = require_formatter_DqAKDlvN();
106248
106321
  var node_path = __require("path");
106249
106322
  node_path = require_chunk.__toESM(node_path);
@@ -112075,47 +112148,17 @@ var require_addon = __commonJS({
112075
112148
  },
112076
112149
  "setDeviceConfig": { "config": { "__nested__": { "bands": [] } } }
112077
112150
  },
112078
- "storage": { "upsertLocation": {
112079
- "isDefault": false,
112080
- "isSystem": false
112081
- } },
112151
+ "storage": { "upsertLocation": { "isSystem": false } },
112082
112152
  "storage-provider": {
112083
- "beginDownload": { "location": { "__nested__": {
112084
- "isDefault": false,
112085
- "isSystem": false
112086
- } } },
112087
- "beginUpload": { "location": { "__nested__": {
112088
- "isDefault": false,
112089
- "isSystem": false
112090
- } } },
112091
- "delete": { "location": { "__nested__": {
112092
- "isDefault": false,
112093
- "isSystem": false
112094
- } } },
112095
- "exists": { "location": { "__nested__": {
112096
- "isDefault": false,
112097
- "isSystem": false
112098
- } } },
112099
- "getAvailableSpace": { "location": { "__nested__": {
112100
- "isDefault": false,
112101
- "isSystem": false
112102
- } } },
112103
- "list": { "location": { "__nested__": {
112104
- "isDefault": false,
112105
- "isSystem": false
112106
- } } },
112107
- "read": { "location": { "__nested__": {
112108
- "isDefault": false,
112109
- "isSystem": false
112110
- } } },
112111
- "resolve": { "location": { "__nested__": {
112112
- "isDefault": false,
112113
- "isSystem": false
112114
- } } },
112115
- "write": { "location": { "__nested__": {
112116
- "isDefault": false,
112117
- "isSystem": false
112118
- } } }
112153
+ "beginDownload": { "location": { "__nested__": { "isSystem": false } } },
112154
+ "beginUpload": { "location": { "__nested__": { "isSystem": false } } },
112155
+ "delete": { "location": { "__nested__": { "isSystem": false } } },
112156
+ "exists": { "location": { "__nested__": { "isSystem": false } } },
112157
+ "getAvailableSpace": { "location": { "__nested__": { "isSystem": false } } },
112158
+ "list": { "location": { "__nested__": { "isSystem": false } } },
112159
+ "read": { "location": { "__nested__": { "isSystem": false } } },
112160
+ "resolve": { "location": { "__nested__": { "isSystem": false } } },
112161
+ "write": { "location": { "__nested__": { "isSystem": false } } }
112119
112162
  },
112120
112163
  "stream-broker": {
112121
112164
  "produceEventMedia": {
@@ -130881,7 +130924,7 @@ var require_dist3 = __commonJS({
130881
130924
  "use strict";
130882
130925
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
130883
130926
  var require_chunk = require_chunk_Cek0wNdY();
130884
- var require_dist10 = require_dist_KQ5D2wqr();
130927
+ var require_dist10 = require_dist_jZb_HKI5();
130885
130928
  var require_builtins_alerts_alerts_addon = require_alerts_addon();
130886
130929
  require_alerts();
130887
130930
  var require_formatter = require_formatter_DqAKDlvN();
@@ -212888,253 +212931,6 @@ var require_dist4 = __commonJS({
212888
212931
  function logLevelAtMost(level, threshold) {
212889
212932
  return LOG_LEVEL_RANK[level] <= LOG_LEVEL_RANK[threshold];
212890
212933
  }
212891
- var LogChannelLevelSchema = zod.z.enum([
212892
- "info",
212893
- "warn",
212894
- "error"
212895
- ]);
212896
- var LogChannelDescriptorSchema = zod.z.object({
212897
- /**
212898
- * Dotted `area.thing`, unique across the workspace. `area` is conventionally
212899
- * the addon's short name so an operator reading a channel list can tell who
212900
- * owns it without a second lookup.
212901
- */
212902
- name: zod.z.string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
212903
- /** One sentence: what the operator will SEE after arming it. */
212904
- description: zod.z.string().min(1),
212905
- /** The level its lines are emitted at. Never below `info`. */
212906
- defaultLevel: LogChannelLevelSchema,
212907
- /**
212908
- * Whether this channel can be narrowed to a camera.
212909
- *
212910
- * `true` is a PROMISE with two halves, and both must hold: the gate is
212911
- * consulted with the numeric device id, AND every line the channel admits
212912
- * carries `tags: { deviceId }` with that same numeric id. The second half is
212913
- * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
212914
- * keeps `deviceId` out of the stream labels for cardinality, so the tag in
212915
- * the body is the only way to filter.
212916
- *
212917
- * A channel whose lines carry the device only in `meta` (or not at all) is
212918
- * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
212919
- * the operator narrows to one camera, sees nothing, and concludes the code
212920
- * path was never taken.
212921
- */
212922
- perDevice: zod.z.boolean()
212923
- });
212924
- var LogChannelWindowSchema = zod.z.object({
212925
- channel: zod.z.string().min(1),
212926
- /** Epoch ms the window closes at. */
212927
- armedUntilMs: zod.z.number(),
212928
- /** `null` = every camera. A non-empty list narrows to those numeric ids. */
212929
- deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable()
212930
- });
212931
- var LogChannelGate = class {
212932
- descriptor;
212933
- /**
212934
- * HOT PATH GUARD. A plain data FIELD, and it must stay one.
212935
- *
212936
- * `log-channel.spec.ts` asserts the property descriptor has no getter and
212937
- * booby-traps the device set, so turning this into an accessor — or reading
212938
- * anything before it — fails the spec instead of taxing every line the
212939
- * process emits.
212940
- */
212941
- on = false;
212942
- /** `null` while armed for every camera. Never read while `on` is false. */
212943
- devices = null;
212944
- level;
212945
- closesAtMs = 0;
212946
- constructor(descriptor) {
212947
- this.descriptor = descriptor;
212948
- this.level = descriptor.defaultLevel;
212949
- }
212950
- /** Epoch ms this channel disarms itself at. 0 when disarmed. */
212951
- get armedUntilMs() {
212952
- return this.on ? this.closesAtMs : 0;
212953
- }
212954
- /**
212955
- * Does this channel want a line about `deviceId`?
212956
- *
212957
- * Call it only behind `gate.on &&`. On its own it is still correct — the
212958
- * guard is repeated inside — but the point of the prefix is that a disarmed
212959
- * channel must not pay the call at all.
212960
- */
212961
- wants(deviceId) {
212962
- if (!this.on) return false;
212963
- return this.devices === null || this.devices.has(deviceId);
212964
- }
212965
- /**
212966
- * Emit one line on this channel, at the channel's declared level.
212967
- *
212968
- * The channel name is added as `tags.logChannel` so LogQL can select the
212969
- * channel without matching on the message text, and whatever `tags` the
212970
- * caller passed — `deviceId` above all — is preserved.
212971
- */
212972
- log(logger, message, extras) {
212973
- if (!this.on) return;
212974
- const tags = {
212975
- ...extras.tags,
212976
- logChannel: this.descriptor.name
212977
- };
212978
- const line = {
212979
- ...extras,
212980
- tags
212981
- };
212982
- if (this.level === "error") logger.error(message, line);
212983
- else if (this.level === "warn") logger.warn(message, line);
212984
- else logger.info(message, line);
212985
- }
212986
- /**
212987
- * Arm (or RE-arm, restarting) this channel. Off the hot path only.
212988
- *
212989
- * An empty `deviceIds` list is treated as "every camera" rather than "no
212990
- * camera": a window that matches nothing is indistinguishable from a
212991
- * disarmed one, and the operator who asked for it would wait for lines that
212992
- * can never come.
212993
- */
212994
- arm(window2) {
212995
- const ids = window2.deviceIds;
212996
- this.devices = ids === null || ids.length === 0 ? null : new Set(ids);
212997
- this.closesAtMs = window2.armedUntilMs;
212998
- this.on = true;
212999
- }
213000
- /** Disarm. Off the hot path only. */
213001
- disarm() {
213002
- this.on = false;
213003
- this.devices = null;
213004
- this.closesAtMs = 0;
213005
- }
213006
- };
213007
- var LogChannelRegistry = class {
213008
- gates = /* @__PURE__ */ new Map();
213009
- /**
213010
- * Declare a channel and get its gate.
213011
- *
213012
- * A duplicate name throws. Two declarations of one name is a programming
213013
- * error, not a merge: the operator would arm one and the other would stay
213014
- * dark, which is the dead-knob shape (D62) with an extra step.
213015
- */
213016
- declare(descriptor) {
213017
- const parsed = LogChannelDescriptorSchema.parse(descriptor);
213018
- if (this.gates.get(parsed.name) !== void 0) throw new Error(`log channel "${parsed.name}" is already declared in this process \u2014 two declarations of one name is a programming error, not a merge`);
213019
- const gate = new LogChannelGate(parsed);
213020
- this.gates.set(parsed.name, gate);
213021
- return gate;
213022
- }
213023
- /** The declarations, sorted by name so a list is stable to read and diff. */
213024
- list() {
213025
- return [...this.gates.values()].map((gate) => gate.descriptor).sort((a, b) => a.name.localeCompare(b.name));
213026
- }
213027
- /** The gate for a declared channel, or `undefined`. */
213028
- gate(name) {
213029
- return this.gates.get(name);
213030
- }
213031
- /**
213032
- * Apply the FULL set of armed windows. Off the hot path.
213033
- *
213034
- * Full, not incremental, and that is the whole design: the document is the
213035
- * authority, so a channel the document does not name is disarmed here. An
213036
- * incremental apply would let a disarm get lost in transit and leave a
213037
- * channel running that nobody can see is running.
213038
- *
213039
- * A window already past its deadline is ignored rather than armed — a
213040
- * restore that re-armed an expired window would make a forgotten diagnostic
213041
- * immortal across restarts.
213042
- *
213043
- * Returns the names it could not place, so the caller can log them: a
213044
- * channel named in the document that this process does not declare is
213045
- * either a typo or an addon that has not booted yet, and both deserve a
213046
- * line rather than silence.
213047
- */
213048
- apply(windows, nowMs) {
213049
- const wanted = /* @__PURE__ */ new Map();
213050
- const unknown = [];
213051
- for (const window2 of windows) {
213052
- if (window2.armedUntilMs <= nowMs) continue;
213053
- if (!this.gates.has(window2.channel)) {
213054
- unknown.push(window2.channel);
213055
- continue;
213056
- }
213057
- wanted.set(window2.channel, window2);
213058
- }
213059
- for (const [name, gate] of this.gates) {
213060
- const window2 = wanted.get(name);
213061
- if (window2 === void 0) gate.disarm();
213062
- else gate.arm(window2);
213063
- }
213064
- return unknown;
213065
- }
213066
- /**
213067
- * Disarm whatever has run out. Called on a timer, NEVER from a log path — a
213068
- * diagnostic that adds a `Date.now()` to the path it is measuring measures
213069
- * itself.
213070
- *
213071
- * Returns the names it closed, so the caller can write the one line that
213072
- * says a window ended and stops "it went quiet" from reading as "the branch
213073
- * was not taken".
213074
- */
213075
- tick(nowMs) {
213076
- const closed = [];
213077
- for (const [name, gate] of this.gates) if (gate.on && gate.armedUntilMs <= nowMs) {
213078
- gate.disarm();
213079
- closed.push(name);
213080
- }
213081
- return closed;
213082
- }
213083
- /** The channels armed right now, as the document would describe them. */
213084
- armed() {
213085
- const out = [];
213086
- for (const [name, gate] of this.gates) if (gate.on) out.push({
213087
- channel: name,
213088
- armedUntilMs: gate.armedUntilMs,
213089
- deviceIds: null
213090
- });
213091
- return out;
213092
- }
213093
- };
213094
- var instance = null;
213095
- function getLogChannelRegistry() {
213096
- instance ??= new LogChannelRegistry();
213097
- return instance;
213098
- }
213099
- function declareLogChannel(descriptor) {
213100
- return getLogChannelRegistry().declare(descriptor);
213101
- }
213102
- function __resetLogChannelRegistryForTests() {
213103
- instance = null;
213104
- }
213105
- var LOG_CHANNEL_TICK_MS = 5e3;
213106
- function createLogChannelsProvider(logger, options = {}) {
213107
- const registry = getLogChannelRegistry();
213108
- const now = options.now ?? Date.now;
213109
- const tickMs = options.tickMs ?? 5e3;
213110
- const timer = setInterval(() => {
213111
- const closed = registry.tick(now());
213112
- for (const name of closed) logger.info("log channel window closed", {
213113
- tags: { logChannel: name },
213114
- meta: { channel: name }
213115
- });
213116
- }, tickMs);
213117
- timer.unref?.();
213118
- return {
213119
- list: () => registry.list(),
213120
- apply: (input) => {
213121
- const unknown = registry.apply(input.windows, now());
213122
- const armed = registry.armed();
213123
- logger.info("log channels applied", { meta: {
213124
- armed: armed.map((window2) => window2.channel),
213125
- unknown,
213126
- declared: registry.list().length
213127
- } });
213128
- return {
213129
- armed: armed.length,
213130
- unknown
213131
- };
213132
- },
213133
- stop: () => {
213134
- clearInterval(timer);
213135
- }
213136
- };
213137
- }
213138
212934
  var OpsLogDomainSchema = zod.z.enum(["recording", "events"]);
213139
212935
  var OpsLogOpSchema = zod.z.enum([
213140
212936
  "prune",
@@ -213840,21 +213636,20 @@ var require_dist4 = __commonJS({
213840
213636
  * flag at upsert time, not here (the schema is provider-agnostic).
213841
213637
  */
213842
213638
  nodeId: zod.z.string().optional(),
213843
- isDefault: zod.z.boolean().default(false),
213844
213639
  isSystem: zod.z.boolean().default(false),
213845
213640
  /**
213846
- * Operator opt-in: whether consumers that BALANCE across several locations
213847
- * of a type may write here. Recordings reads it today; event media and
213848
- * backups are the next consumers, which is why the flag lives on the
213849
- * location rather than in any one addon's store — nothing has to be
213850
- * extended to add the next consumer.
213641
+ * THE write switch, and the only one (D383). `enabled: true` means every
213642
+ * consumer that chooses a write target for this type may write here, and all
213643
+ * enabled locations of a type are used TOGETHER; `false` means read-only —
213644
+ * still read, still played back, still age-swept, still drained, never
213645
+ * written.
213851
213646
  *
213852
- * OPTIONAL, and ABSENT MEANS ACTIVE. Every location persisted before the
213853
- * flag existed reads back with no flag and keeps working exactly as before;
213854
- * that is the whole compat story, and it is why no migration ships with it.
213855
- * A newly CREATED sibling is stamped `false` by the orchestrator (creating a
213856
- * disk must not silently start writing to it); the default of a type is
213857
- * always stamped `true`.
213647
+ * OPTIONAL only for the wire: an upsert that omits it means "leave what is
213648
+ * stored" on an update and "born inert unless it is the first location of its
213649
+ * type" on a create. On a PERSISTED row absence is legacy and it means
213650
+ * enabled — {@link isLocationEnabled} is the one place that says so, and the
213651
+ * orchestrator stamps every flagless row `true` once at hydrate so absence
213652
+ * stops existing rather than being re-derived on every read.
213858
213653
  */
213859
213654
  enabled: zod.z.boolean().optional(),
213860
213655
  /** COMPUTED at read time by the orchestrator (statfs of the backing volume
@@ -213867,6 +213662,10 @@ var require_dist4 = __commonJS({
213867
213662
  createdAt: zod.z.number(),
213868
213663
  updatedAt: zod.z.number()
213869
213664
  });
213665
+ var LegacyStorageLocationDefaultSchema = zod.z.object({ isDefault: zod.z.boolean().optional() });
213666
+ function isLocationEnabled(location) {
213667
+ return location.enabled !== false;
213668
+ }
213870
213669
  var StorageLocationRefSchema = zod.z.union([StorageLocationTypeSchema, zod.z.string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/)]);
213871
213670
  var StorageAccessSchema = zod.z.enum(["local-path", "cap-mediated"]);
213872
213671
  var STORAGE_ACCESS_FALLBACK = "local-path";
@@ -214164,52 +213963,343 @@ var require_dist4 = __commonJS({
214164
213963
  function findTimezone(id) {
214165
213964
  return TIMEZONES.find((tz) => tz.id === id);
214166
213965
  }
214167
- function pickPreferredRtspEntry(entries2, pref, deviceId, options = {}) {
214168
- if (entries2.length === 0) return null;
214169
- const prefix = `${deviceId}/`;
214170
- const toPicked = (e) => ({
214171
- brokerId: e.brokerId,
214172
- profileId: e.brokerId.startsWith(prefix) ? e.brokerId.slice(prefix.length) : e.brokerId,
214173
- url: e.url,
214174
- mutedUrl: e.mutedUrl,
214175
- enabled: e.enabled,
214176
- ...e.codec !== void 0 ? { codec: e.codec } : {},
214177
- ...e.resolution !== void 0 ? { resolution: e.resolution } : {}
214178
- });
214179
- if (pref !== "auto") {
214180
- const match = entries2.find((e) => e.enabled && e.url.length > 0 && (e.profile === pref || e.brokerId === `${prefix}${pref}`));
214181
- if (match) return toPicked(match);
213966
+ var LogChannelLevelSchema = zod.z.enum([
213967
+ "info",
213968
+ "warn",
213969
+ "error"
213970
+ ]);
213971
+ var LogChannelDescriptorSchema = zod.z.object({
213972
+ /**
213973
+ * Dotted `area.thing`, unique across the workspace. `area` is conventionally
213974
+ * the addon's short name so an operator reading a channel list can tell who
213975
+ * owns it without a second lookup.
213976
+ */
213977
+ name: zod.z.string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
213978
+ /** One sentence: what the operator will SEE after arming it. */
213979
+ description: zod.z.string().min(1),
213980
+ /** The level its lines are emitted at. Never below `info`. */
213981
+ defaultLevel: LogChannelLevelSchema,
213982
+ /**
213983
+ * Whether this channel can be narrowed to a camera.
213984
+ *
213985
+ * `true` is a PROMISE with two halves, and both must hold: the gate is
213986
+ * consulted with the numeric device id, AND every line the channel admits
213987
+ * carries `tags: { deviceId }` with that same numeric id. The second half is
213988
+ * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
213989
+ * keeps `deviceId` out of the stream labels for cardinality, so the tag in
213990
+ * the body is the only way to filter.
213991
+ *
213992
+ * A channel whose lines carry the device only in `meta` (or not at all) is
213993
+ * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
213994
+ * the operator narrows to one camera, sees nothing, and concludes the code
213995
+ * path was never taken.
213996
+ */
213997
+ perDevice: zod.z.boolean()
213998
+ });
213999
+ var LogChannelWindowSchema = zod.z.object({
214000
+ channel: zod.z.string().min(1),
214001
+ /** Epoch ms the window closes at. */
214002
+ armedUntilMs: zod.z.number(),
214003
+ /** `null` = every camera. A non-empty list narrows to those numeric ids. */
214004
+ deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable()
214005
+ });
214006
+ var LogChannelGate = class {
214007
+ descriptor;
214008
+ /**
214009
+ * HOT PATH GUARD. A plain data FIELD, and it must stay one.
214010
+ *
214011
+ * `log-channel.spec.ts` asserts the property descriptor has no getter and
214012
+ * booby-traps the device set, so turning this into an accessor — or reading
214013
+ * anything before it — fails the spec instead of taxing every line the
214014
+ * process emits.
214015
+ */
214016
+ on = false;
214017
+ /** `null` while armed for every camera. Never read while `on` is false. */
214018
+ devices = null;
214019
+ level;
214020
+ closesAtMs = 0;
214021
+ constructor(descriptor) {
214022
+ this.descriptor = descriptor;
214023
+ this.level = descriptor.defaultLevel;
214182
214024
  }
214183
- const eligible = entries2.filter((e) => e.enabled && e.url.length > 0);
214184
- if (eligible.length === 0) return null;
214185
- const target = options.targetResolution;
214186
- if (target) {
214187
- const withRes = eligible.filter((e) => e.resolution !== void 0);
214188
- if (withRes.length > 0) {
214189
- const picked = pickClosestResolution(withRes, target);
214190
- if (picked) return toPicked(picked);
214025
+ /** Epoch ms this channel disarms itself at. 0 when disarmed. */
214026
+ get armedUntilMs() {
214027
+ return this.on ? this.closesAtMs : 0;
214028
+ }
214029
+ /**
214030
+ * Does this channel want a line about `deviceId`?
214031
+ *
214032
+ * Call it only behind `gate.on &&`. On its own it is still correct — the
214033
+ * guard is repeated inside — but the point of the prefix is that a disarmed
214034
+ * channel must not pay the call at all.
214035
+ */
214036
+ wants(deviceId) {
214037
+ if (!this.on) return false;
214038
+ return this.devices === null || this.devices.has(deviceId);
214039
+ }
214040
+ /**
214041
+ * Emit one line on this channel, at the channel's declared level.
214042
+ *
214043
+ * The channel name is added as `tags.logChannel` so LogQL can select the
214044
+ * channel without matching on the message text, and whatever `tags` the
214045
+ * caller passed — `deviceId` above all — is preserved.
214046
+ */
214047
+ log(logger, message, extras) {
214048
+ if (!this.on) return;
214049
+ const tags = {
214050
+ ...extras.tags,
214051
+ logChannel: this.descriptor.name
214052
+ };
214053
+ const line = {
214054
+ ...extras,
214055
+ tags
214056
+ };
214057
+ if (this.level === "error") logger.error(message, line);
214058
+ else if (this.level === "warn") logger.warn(message, line);
214059
+ else logger.info(message, line);
214060
+ }
214061
+ /**
214062
+ * Arm (or RE-arm, restarting) this channel. Off the hot path only.
214063
+ *
214064
+ * An empty `deviceIds` list is treated as "every camera" rather than "no
214065
+ * camera": a window that matches nothing is indistinguishable from a
214066
+ * disarmed one, and the operator who asked for it would wait for lines that
214067
+ * can never come.
214068
+ */
214069
+ arm(window2) {
214070
+ const ids = window2.deviceIds;
214071
+ this.devices = ids === null || ids.length === 0 ? null : new Set(ids);
214072
+ this.closesAtMs = window2.armedUntilMs;
214073
+ this.on = true;
214074
+ }
214075
+ /** Disarm. Off the hot path only. */
214076
+ disarm() {
214077
+ this.on = false;
214078
+ this.devices = null;
214079
+ this.closesAtMs = 0;
214080
+ }
214081
+ };
214082
+ var LogChannelRegistry = class {
214083
+ gates = /* @__PURE__ */ new Map();
214084
+ /**
214085
+ * Declare a channel and get its gate.
214086
+ *
214087
+ * A duplicate name throws. Two declarations of one name is a programming
214088
+ * error, not a merge: the operator would arm one and the other would stay
214089
+ * dark, which is the dead-knob shape (D62) with an extra step.
214090
+ */
214091
+ declare(descriptor) {
214092
+ const parsed = LogChannelDescriptorSchema.parse(descriptor);
214093
+ if (this.gates.get(parsed.name) !== void 0) throw new Error(`log channel "${parsed.name}" is already declared in this process \u2014 two declarations of one name is a programming error, not a merge`);
214094
+ const gate = new LogChannelGate(parsed);
214095
+ this.gates.set(parsed.name, gate);
214096
+ return gate;
214097
+ }
214098
+ /** The declarations, sorted by name so a list is stable to read and diff. */
214099
+ list() {
214100
+ return [...this.gates.values()].map((gate) => gate.descriptor).sort((a, b) => a.name.localeCompare(b.name));
214101
+ }
214102
+ /** The gate for a declared channel, or `undefined`. */
214103
+ gate(name) {
214104
+ return this.gates.get(name);
214105
+ }
214106
+ /**
214107
+ * Apply the FULL set of armed windows. Off the hot path.
214108
+ *
214109
+ * Full, not incremental, and that is the whole design: the document is the
214110
+ * authority, so a channel the document does not name is disarmed here. An
214111
+ * incremental apply would let a disarm get lost in transit and leave a
214112
+ * channel running that nobody can see is running.
214113
+ *
214114
+ * A window already past its deadline is ignored rather than armed — a
214115
+ * restore that re-armed an expired window would make a forgotten diagnostic
214116
+ * immortal across restarts.
214117
+ *
214118
+ * Returns the names it could not place, so the caller can log them: a
214119
+ * channel named in the document that this process does not declare is
214120
+ * either a typo or an addon that has not booted yet, and both deserve a
214121
+ * line rather than silence.
214122
+ */
214123
+ apply(windows, nowMs) {
214124
+ const wanted = /* @__PURE__ */ new Map();
214125
+ const unknown = [];
214126
+ for (const window2 of windows) {
214127
+ if (window2.armedUntilMs <= nowMs) continue;
214128
+ if (!this.gates.has(window2.channel)) {
214129
+ unknown.push(window2.channel);
214130
+ continue;
214131
+ }
214132
+ wanted.set(window2.channel, window2);
214133
+ }
214134
+ for (const [name, gate] of this.gates) {
214135
+ const window2 = wanted.get(name);
214136
+ if (window2 === void 0) gate.disarm();
214137
+ else gate.arm(window2);
214191
214138
  }
214139
+ return unknown;
214192
214140
  }
214193
- return toPicked(eligible[0]);
214141
+ /**
214142
+ * Disarm whatever has run out. Called on a timer, NEVER from a log path — a
214143
+ * diagnostic that adds a `Date.now()` to the path it is measuring measures
214144
+ * itself.
214145
+ *
214146
+ * Returns the names it closed, so the caller can write the one line that
214147
+ * says a window ended and stops "it went quiet" from reading as "the branch
214148
+ * was not taken".
214149
+ */
214150
+ tick(nowMs) {
214151
+ const closed = [];
214152
+ for (const [name, gate] of this.gates) if (gate.on && gate.armedUntilMs <= nowMs) {
214153
+ gate.disarm();
214154
+ closed.push(name);
214155
+ }
214156
+ return closed;
214157
+ }
214158
+ /** The channels armed right now, as the document would describe them. */
214159
+ armed() {
214160
+ const out = [];
214161
+ for (const [name, gate] of this.gates) if (gate.on) out.push({
214162
+ channel: name,
214163
+ armedUntilMs: gate.armedUntilMs,
214164
+ deviceIds: null
214165
+ });
214166
+ return out;
214167
+ }
214168
+ };
214169
+ var instance = null;
214170
+ function getLogChannelRegistry() {
214171
+ instance ??= new LogChannelRegistry();
214172
+ return instance;
214194
214173
  }
214195
- function pickClosestResolution(entries2, target) {
214196
- const targetPixels = target.width * target.height;
214197
- let bestAbove;
214198
- let bestBelow;
214199
- for (const e of entries2) {
214200
- if (e.resolution === void 0) continue;
214201
- const pixels = e.resolution.width * e.resolution.height;
214202
- if (pixels >= targetPixels) {
214203
- if (!bestAbove || pixels < bestAbove.pixels) bestAbove = {
214204
- entry: e,
214205
- pixels
214174
+ function declareLogChannel(descriptor) {
214175
+ return getLogChannelRegistry().declare(descriptor);
214176
+ }
214177
+ function __resetLogChannelRegistryForTests() {
214178
+ instance = null;
214179
+ }
214180
+ var LOG_CHANNEL_TICK_MS = 5e3;
214181
+ function createLogChannelsProvider(logger, options = {}) {
214182
+ const registry = getLogChannelRegistry();
214183
+ const now = options.now ?? Date.now;
214184
+ const tickMs = options.tickMs ?? 5e3;
214185
+ const timer = setInterval(() => {
214186
+ const closed = registry.tick(now());
214187
+ for (const name of closed) logger.info("log channel window closed", {
214188
+ tags: { logChannel: name },
214189
+ meta: { channel: name }
214190
+ });
214191
+ }, tickMs);
214192
+ timer.unref?.();
214193
+ return {
214194
+ list: () => registry.list(),
214195
+ apply: (input) => {
214196
+ const unknown = registry.apply(input.windows, now());
214197
+ const armed = registry.armed();
214198
+ logger.info("log channels applied", { meta: {
214199
+ armed: armed.map((window2) => window2.channel),
214200
+ unknown,
214201
+ declared: registry.list().length
214202
+ } });
214203
+ return {
214204
+ armed: armed.length,
214205
+ unknown
214206
214206
  };
214207
- } else if (!bestBelow || pixels > bestBelow.pixels) bestBelow = {
214208
- entry: e,
214209
- pixels
214210
- };
214207
+ },
214208
+ stop: () => {
214209
+ clearInterval(timer);
214210
+ }
214211
+ };
214212
+ }
214213
+ var MAX_REASONS_PER_KEY = 16;
214214
+ var MAX_KEYS = 1024;
214215
+ var OVERFLOW_REASON = "other";
214216
+ function counterKey(deviceId, family, variant) {
214217
+ return variant === void 0 ? `${deviceId}\0${family}` : `${deviceId}\0${family}\0${variant}`;
214218
+ }
214219
+ var FailureCounters = class {
214220
+ maxKeys;
214221
+ maxReasons;
214222
+ counters = /* @__PURE__ */ new Map();
214223
+ refused = 0;
214224
+ constructor(maxKeys = MAX_KEYS, maxReasons = 16) {
214225
+ this.maxKeys = maxKeys;
214226
+ this.maxReasons = maxReasons;
214211
214227
  }
214212
- return bestAbove?.entry ?? bestBelow?.entry;
214228
+ /**
214229
+ * Counters refused because {@link MAX_KEYS} was already held.
214230
+ *
214231
+ * Cumulative for the life of the instance: a bound that bit is a fact about
214232
+ * the deployment, and a surface that hid it would under-report a fleet
214233
+ * precisely when the fleet got large enough to matter.
214234
+ */
214235
+ get keysRefused() {
214236
+ return this.refused;
214237
+ }
214238
+ /** Counters currently held. */
214239
+ get size() {
214240
+ return this.counters.size;
214241
+ }
214242
+ /**
214243
+ * Fold one observation in.
214244
+ *
214245
+ * A non-positive or non-integer `deviceId` is REFUSED rather than bucketed:
214246
+ * see the module docblock — an entry that cannot name its camera is worse
214247
+ * than no entry.
214248
+ */
214249
+ note(observation, nowMs) {
214250
+ if (!Number.isInteger(observation.deviceId) || observation.deviceId <= 0) return;
214251
+ const key = counterKey(observation.deviceId, observation.family, observation.variant);
214252
+ let counter = this.counters.get(key);
214253
+ if (counter === void 0) {
214254
+ if (this.counters.size >= this.maxKeys) {
214255
+ this.refused += 1;
214256
+ return;
214257
+ }
214258
+ counter = {
214259
+ deviceId: observation.deviceId,
214260
+ family: observation.family,
214261
+ variant: observation.variant,
214262
+ sinceMs: nowMs,
214263
+ attempts: 0,
214264
+ succeeded: 0,
214265
+ reasons: /* @__PURE__ */ new Map()
214266
+ };
214267
+ this.counters.set(key, counter);
214268
+ }
214269
+ counter.attempts += 1;
214270
+ if (observation.reason === void 0) {
214271
+ counter.succeeded += 1;
214272
+ return;
214273
+ }
214274
+ const reason = counter.reasons.has(observation.reason) || counter.reasons.size < this.maxReasons ? observation.reason : OVERFLOW_REASON;
214275
+ counter.reasons.set(reason, (counter.reasons.get(reason) ?? 0) + 1);
214276
+ }
214277
+ /** Read every counter. Never mutates — see the module docblock. */
214278
+ snapshot(nowMs) {
214279
+ const out = [];
214280
+ for (const counter of this.counters.values()) out.push({
214281
+ deviceId: counter.deviceId,
214282
+ family: counter.family,
214283
+ ...counter.variant !== void 0 ? { variant: counter.variant } : {},
214284
+ sinceMs: counter.sinceMs,
214285
+ atMs: nowMs,
214286
+ attempts: counter.attempts,
214287
+ succeeded: counter.succeeded,
214288
+ reasons: [...counter.reasons.entries()].map(([reason, count]) => ({
214289
+ reason,
214290
+ count
214291
+ })).toSorted((a, b) => b.count - a.count)
214292
+ });
214293
+ return out;
214294
+ }
214295
+ /** Drop everything (host disposal). */
214296
+ clear() {
214297
+ this.counters.clear();
214298
+ }
214299
+ };
214300
+ function failureRate(sample) {
214301
+ if (sample.attempts <= 0) return null;
214302
+ return (sample.attempts - sample.succeeded) / sample.attempts;
214213
214303
  }
214214
214304
  var UNATTRIBUTED_BUCKET_KEY = "__unattributed__";
214215
214305
  var ROOT_BUCKET_KEY = "__root__";
@@ -214321,96 +214411,52 @@ var require_dist4 = __commonJS({
214321
214411
  }
214322
214412
  return [...buckets.values()].toSorted((a, b) => a.atMs - b.atMs);
214323
214413
  }
214324
- var MAX_REASONS_PER_KEY = 16;
214325
- var MAX_KEYS = 1024;
214326
- var OVERFLOW_REASON = "other";
214327
- function counterKey(deviceId, family, variant) {
214328
- return variant === void 0 ? `${deviceId}\0${family}` : `${deviceId}\0${family}\0${variant}`;
214329
- }
214330
- var FailureCounters = class {
214331
- maxKeys;
214332
- maxReasons;
214333
- counters = /* @__PURE__ */ new Map();
214334
- refused = 0;
214335
- constructor(maxKeys = MAX_KEYS, maxReasons = 16) {
214336
- this.maxKeys = maxKeys;
214337
- this.maxReasons = maxReasons;
214338
- }
214339
- /**
214340
- * Counters refused because {@link MAX_KEYS} was already held.
214341
- *
214342
- * Cumulative for the life of the instance: a bound that bit is a fact about
214343
- * the deployment, and a surface that hid it would under-report a fleet
214344
- * precisely when the fleet got large enough to matter.
214345
- */
214346
- get keysRefused() {
214347
- return this.refused;
214348
- }
214349
- /** Counters currently held. */
214350
- get size() {
214351
- return this.counters.size;
214414
+ function pickPreferredRtspEntry(entries2, pref, deviceId, options = {}) {
214415
+ if (entries2.length === 0) return null;
214416
+ const prefix = `${deviceId}/`;
214417
+ const toPicked = (e) => ({
214418
+ brokerId: e.brokerId,
214419
+ profileId: e.brokerId.startsWith(prefix) ? e.brokerId.slice(prefix.length) : e.brokerId,
214420
+ url: e.url,
214421
+ mutedUrl: e.mutedUrl,
214422
+ enabled: e.enabled,
214423
+ ...e.codec !== void 0 ? { codec: e.codec } : {},
214424
+ ...e.resolution !== void 0 ? { resolution: e.resolution } : {}
214425
+ });
214426
+ if (pref !== "auto") {
214427
+ const match = entries2.find((e) => e.enabled && e.url.length > 0 && (e.profile === pref || e.brokerId === `${prefix}${pref}`));
214428
+ if (match) return toPicked(match);
214352
214429
  }
214353
- /**
214354
- * Fold one observation in.
214355
- *
214356
- * A non-positive or non-integer `deviceId` is REFUSED rather than bucketed:
214357
- * see the module docblock — an entry that cannot name its camera is worse
214358
- * than no entry.
214359
- */
214360
- note(observation, nowMs) {
214361
- if (!Number.isInteger(observation.deviceId) || observation.deviceId <= 0) return;
214362
- const key = counterKey(observation.deviceId, observation.family, observation.variant);
214363
- let counter = this.counters.get(key);
214364
- if (counter === void 0) {
214365
- if (this.counters.size >= this.maxKeys) {
214366
- this.refused += 1;
214367
- return;
214368
- }
214369
- counter = {
214370
- deviceId: observation.deviceId,
214371
- family: observation.family,
214372
- variant: observation.variant,
214373
- sinceMs: nowMs,
214374
- attempts: 0,
214375
- succeeded: 0,
214376
- reasons: /* @__PURE__ */ new Map()
214377
- };
214378
- this.counters.set(key, counter);
214379
- }
214380
- counter.attempts += 1;
214381
- if (observation.reason === void 0) {
214382
- counter.succeeded += 1;
214383
- return;
214430
+ const eligible = entries2.filter((e) => e.enabled && e.url.length > 0);
214431
+ if (eligible.length === 0) return null;
214432
+ const target = options.targetResolution;
214433
+ if (target) {
214434
+ const withRes = eligible.filter((e) => e.resolution !== void 0);
214435
+ if (withRes.length > 0) {
214436
+ const picked = pickClosestResolution(withRes, target);
214437
+ if (picked) return toPicked(picked);
214384
214438
  }
214385
- const reason = counter.reasons.has(observation.reason) || counter.reasons.size < this.maxReasons ? observation.reason : OVERFLOW_REASON;
214386
- counter.reasons.set(reason, (counter.reasons.get(reason) ?? 0) + 1);
214387
- }
214388
- /** Read every counter. Never mutates — see the module docblock. */
214389
- snapshot(nowMs) {
214390
- const out = [];
214391
- for (const counter of this.counters.values()) out.push({
214392
- deviceId: counter.deviceId,
214393
- family: counter.family,
214394
- ...counter.variant !== void 0 ? { variant: counter.variant } : {},
214395
- sinceMs: counter.sinceMs,
214396
- atMs: nowMs,
214397
- attempts: counter.attempts,
214398
- succeeded: counter.succeeded,
214399
- reasons: [...counter.reasons.entries()].map(([reason, count]) => ({
214400
- reason,
214401
- count
214402
- })).toSorted((a, b) => b.count - a.count)
214403
- });
214404
- return out;
214405
214439
  }
214406
- /** Drop everything (host disposal). */
214407
- clear() {
214408
- this.counters.clear();
214440
+ return toPicked(eligible[0]);
214441
+ }
214442
+ function pickClosestResolution(entries2, target) {
214443
+ const targetPixels = target.width * target.height;
214444
+ let bestAbove;
214445
+ let bestBelow;
214446
+ for (const e of entries2) {
214447
+ if (e.resolution === void 0) continue;
214448
+ const pixels = e.resolution.width * e.resolution.height;
214449
+ if (pixels >= targetPixels) {
214450
+ if (!bestAbove || pixels < bestAbove.pixels) bestAbove = {
214451
+ entry: e,
214452
+ pixels
214453
+ };
214454
+ } else if (!bestBelow || pixels > bestBelow.pixels) bestBelow = {
214455
+ entry: e,
214456
+ pixels
214457
+ };
214409
214458
  }
214410
- };
214411
- function failureRate(sample) {
214412
- if (sample.attempts <= 0) return null;
214413
- return (sample.attempts - sample.succeeded) / sample.attempts;
214459
+ return bestAbove?.entry ?? bestBelow?.entry;
214414
214460
  }
214415
214461
  var FORMAT_KEYS = [
214416
214462
  "onnx",
@@ -216679,12 +216725,30 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
216679
216725
  lastSuccessAt: zod.z.number().optional(),
216680
216726
  /** Newest-archive size from `manifests.json`, or undefined. */
216681
216727
  lastSuccessSizeBytes: zod.z.number().optional(),
216682
- /** Per-destination cron expression. Empty = manual-only (no schedule). */
216728
+ /**
216729
+ * Cron cadence(s) of the ENABLED schedules that fan out to this
216730
+ * destination, comma-joined. Absent when no enabled schedule targets it
216731
+ * — a destination nothing is scheduled to write to must not advertise a
216732
+ * cadence (D384). This is never the `backup_destination_policies.cron`
216733
+ * column: that per-location cron has scheduled nothing since 2026-07-28
216734
+ * and reading it made a destination with a DISABLED schedule claim a
216735
+ * nightly run.
216736
+ */
216683
216737
  cron: zod.z.string().optional(),
216684
- /** ms-epoch of next computed firing for this destination's cron, if any. */
216738
+ /** ms-epoch of the next firing across those schedules (earliest), if any. */
216685
216739
  nextRunAt: zod.z.number().optional(),
216686
- /** ms-epoch of last successful scheduled run (mirrors policy.lastRunAt). */
216687
- lastRunAt: zod.z.number().optional()
216740
+ /**
216741
+ * ms-epoch of the last time a run ATTEMPTED to write here — success or
216742
+ * failure. Never a success stamp: pair it with `lastSuccessAt` (the
216743
+ * newest archive that actually landed) and `lastError`.
216744
+ */
216745
+ lastAttemptAt: zod.z.number().optional(),
216746
+ /**
216747
+ * Why the last attempt failed, verbatim. Absent when the last attempt
216748
+ * landed the archive. A destination that has never been written to has
216749
+ * neither this nor `lastAttemptAt`.
216750
+ */
216751
+ lastError: zod.z.string().optional()
216688
216752
  });
216689
216753
  var BackupArchiveEntrySchema = zod.z.object({
216690
216754
  id: zod.z.string(),
@@ -216806,8 +216870,16 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
216806
216870
  retentionCount: zod.z.number().int().min(1).max(1e3),
216807
216871
  /** Optional subset of source locations to include; omitted = all. */
216808
216872
  dataSources: zod.z.array(zod.z.string()).readonly().optional(),
216809
- /** ms-epoch of last successful run. */
216810
- lastRunAt: zod.z.number().optional(),
216873
+ /**
216874
+ * ms-epoch of the last tick that FIRED this schedule. Stamped before the
216875
+ * archive runs (it is the dedupe anchor), so it says "attempted", never
216876
+ * "succeeded" — a run refused by every destination stamps it too.
216877
+ */
216878
+ lastAttemptAt: zod.z.number().optional(),
216879
+ /** ms-epoch of the last run of this schedule that landed at ≥1 destination. */
216880
+ lastSuccessAt: zod.z.number().optional(),
216881
+ /** Why the last fired run failed, verbatim. Absent when it succeeded. */
216882
+ lastError: zod.z.string().optional(),
216811
216883
  /** ms-epoch of next computed firing (read-only, filled on list). */
216812
216884
  nextRunAt: zod.z.number().optional()
216813
216885
  });
@@ -216930,14 +217002,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
216930
217002
  locationId: zod.z.string(),
216931
217003
  enabled: zod.z.boolean(),
216932
217004
  retentionCount: zod.z.number().int().min(1).max(1e3),
216933
- label: zod.z.string().optional(),
216934
- /**
216935
- * Per-destination cron expression. Empty string clears the
216936
- * schedule (manual-only). Validated server-side via croner;
216937
- * malformed expressions reject the upsert with an actionable
216938
- * message.
216939
- */
216940
- cron: zod.z.string().optional()
217005
+ label: zod.z.string().optional()
216941
217006
  }), zod.z.void(), {
216942
217007
  kind: "mutation",
216943
217008
  auth: "admin"
@@ -229922,7 +229987,6 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
229922
229987
  }), zod.z.instanceof(Uint8Array)),
229923
229988
  endDownload: require_sleep.method(zod.z.object({ downloadId: zod.z.string() }), zod.z.void(), { kind: "mutation" }),
229924
229989
  listLocations: require_sleep.method(zod.z.object({ type: StorageLocationTypeSchema.optional() }), zod.z.array(StorageLocationSchema).readonly()),
229925
- getDefaultLocation: require_sleep.method(zod.z.object({ type: StorageLocationTypeSchema }), StorageLocationSchema.nullable()),
229926
229990
  listLocationDeclarations: require_sleep.method(zod.z.void(), zod.z.array(StorageLocationDeclarationSchema).readonly()),
229927
229991
  upsertLocation: require_sleep.method(StorageLocationSchema.omit({
229928
229992
  createdAt: true,
@@ -240195,62 +240259,6 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
240195
240259
  "ffmpeg.threadCount": 0,
240196
240260
  "auth.tokenExpiry": "30d"
240197
240261
  };
240198
- var CONTAINER_CHILD_PRIORITY = [
240199
- "media-player",
240200
- "alarm-panel",
240201
- "thermostat",
240202
- "climate",
240203
- "humidifier",
240204
- "water-heater",
240205
- "lock",
240206
- "cover",
240207
- "valve",
240208
- "fan",
240209
- "vacuum",
240210
- "lawn-mower",
240211
- "light",
240212
- "switch",
240213
- "siren",
240214
- "button",
240215
- "control",
240216
- "notifier",
240217
- "script",
240218
- "automation",
240219
- "update",
240220
- "presence",
240221
- "weather",
240222
- "image",
240223
- "sensor"
240224
- ];
240225
- var CONTAINER_CHILD_ROLE_PRIORITY = [
240226
- "contact-sensor",
240227
- "motion-sensor",
240228
- "occupancy-sensor",
240229
- "smoke-sensor",
240230
- "co-sensor",
240231
- "gas-sensor",
240232
- "leak-sensor",
240233
- "vibration-sensor",
240234
- "tamper-sensor",
240235
- "sound-sensor"
240236
- ];
240237
- function roleRank(role) {
240238
- if (role === void 0) return CONTAINER_CHILD_ROLE_PRIORITY.length;
240239
- const i = CONTAINER_CHILD_ROLE_PRIORITY.indexOf(role);
240240
- return i === -1 ? CONTAINER_CHILD_ROLE_PRIORITY.length : i;
240241
- }
240242
- function rank(type) {
240243
- const i = CONTAINER_CHILD_PRIORITY.indexOf(type);
240244
- return i === -1 ? CONTAINER_CHILD_PRIORITY.length : i;
240245
- }
240246
- function resolveContainerPrimaryChild(children, overrideEntityId, containerName) {
240247
- if (overrideEntityId !== void 0 && overrideEntityId !== null) {
240248
- const picked = children.find((c) => c.stableId === overrideEntityId) ?? children.find((c) => c.entityId !== void 0 && c.entityId === overrideEntityId);
240249
- if (picked !== void 0) return picked;
240250
- }
240251
- const namesake = (c) => containerName !== void 0 && containerName.length > 0 && c.name !== void 0 && c.name.toLowerCase() === containerName.toLowerCase() ? 0 : 1;
240252
- return [...children].toSorted((a, b) => roleRank(a.role) - roleRank(b.role) || namesake(a) - namesake(b) || rank(a.type) - rank(b.type))[0] ?? null;
240253
- }
240254
240262
  var AccessoryKind = {
240255
240263
  Siren: require_sleep.DeviceRole.Siren,
240256
240264
  Floodlight: require_sleep.DeviceRole.Floodlight,
@@ -241701,6 +241709,62 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
241701
241709
  function isBatteryPresenceFault(presence) {
241702
241710
  return presence === "unreachable";
241703
241711
  }
241712
+ var CONTAINER_CHILD_PRIORITY = [
241713
+ "media-player",
241714
+ "alarm-panel",
241715
+ "thermostat",
241716
+ "climate",
241717
+ "humidifier",
241718
+ "water-heater",
241719
+ "lock",
241720
+ "cover",
241721
+ "valve",
241722
+ "fan",
241723
+ "vacuum",
241724
+ "lawn-mower",
241725
+ "light",
241726
+ "switch",
241727
+ "siren",
241728
+ "button",
241729
+ "control",
241730
+ "notifier",
241731
+ "script",
241732
+ "automation",
241733
+ "update",
241734
+ "presence",
241735
+ "weather",
241736
+ "image",
241737
+ "sensor"
241738
+ ];
241739
+ var CONTAINER_CHILD_ROLE_PRIORITY = [
241740
+ "contact-sensor",
241741
+ "motion-sensor",
241742
+ "occupancy-sensor",
241743
+ "smoke-sensor",
241744
+ "co-sensor",
241745
+ "gas-sensor",
241746
+ "leak-sensor",
241747
+ "vibration-sensor",
241748
+ "tamper-sensor",
241749
+ "sound-sensor"
241750
+ ];
241751
+ function roleRank(role) {
241752
+ if (role === void 0) return CONTAINER_CHILD_ROLE_PRIORITY.length;
241753
+ const i = CONTAINER_CHILD_ROLE_PRIORITY.indexOf(role);
241754
+ return i === -1 ? CONTAINER_CHILD_ROLE_PRIORITY.length : i;
241755
+ }
241756
+ function rank(type) {
241757
+ const i = CONTAINER_CHILD_PRIORITY.indexOf(type);
241758
+ return i === -1 ? CONTAINER_CHILD_PRIORITY.length : i;
241759
+ }
241760
+ function resolveContainerPrimaryChild(children, overrideEntityId, containerName) {
241761
+ if (overrideEntityId !== void 0 && overrideEntityId !== null) {
241762
+ const picked = children.find((c) => c.stableId === overrideEntityId) ?? children.find((c) => c.entityId !== void 0 && c.entityId === overrideEntityId);
241763
+ if (picked !== void 0) return picked;
241764
+ }
241765
+ const namesake = (c) => containerName !== void 0 && containerName.length > 0 && c.name !== void 0 && c.name.toLowerCase() === containerName.toLowerCase() ? 0 : 1;
241766
+ return [...children].toSorted((a, b) => roleRank(a.role) - roleRank(b.role) || namesake(a) - namesake(b) || rank(a.type) - rank(b.type))[0] ?? null;
241767
+ }
241704
241768
  var DECLARED_INTEGRATION_FIXED_KEY = "fixed";
241705
241769
  function declarationOwnerNodeId(localNodeId) {
241706
241770
  const raw = localNodeId ?? "hub";
@@ -248984,12 +249048,6 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
248984
249048
  addonId: null,
248985
249049
  access: "view"
248986
249050
  },
248987
- "storage.getDefaultLocation": {
248988
- capName: "storage",
248989
- capScope: "system",
248990
- addonId: null,
248991
- access: "view"
248992
- },
248993
249051
  "storage.list": {
248994
249052
  capName: "storage",
248995
249053
  capScope: "system",
@@ -253365,7 +253423,6 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
253365
253423
  readChunk: (input) => dispatch("storage", "readChunk", "query", input),
253366
253424
  endDownload: (input) => dispatch("storage", "endDownload", "mutation", input),
253367
253425
  listLocations: (input) => dispatch("storage", "listLocations", "query", input),
253368
- getDefaultLocation: (input) => dispatch("storage", "getDefaultLocation", "query", input),
253369
253426
  listLocationDeclarations: (input) => dispatch("storage", "listLocationDeclarations", "query", input),
253370
253427
  upsertLocation: (input) => dispatch("storage", "upsertLocation", "mutation", input),
253371
253428
  deleteLocation: (input) => dispatch("storage", "deleteLocation", "mutation", input),
@@ -256543,6 +256600,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
256543
256600
  exports.LedgerWalkReportSchema = LedgerWalkReportSchema;
256544
256601
  exports.LedgerWalkSkipCountsSchema = LedgerWalkSkipCountsSchema;
256545
256602
  exports.LedgerWalkSkipReasonSchema = LedgerWalkSkipReasonSchema;
256603
+ exports.LegacyStorageLocationDefaultSchema = LegacyStorageLocationDefaultSchema;
256546
256604
  exports.LinkedDeviceSchema = LinkedDeviceSchema;
256547
256605
  exports.LinkedDevicesModeSchema = LinkedDevicesModeSchema;
256548
256606
  exports.LlmDefaultSchema = LlmDefaultSchema;
@@ -257395,6 +257453,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
257395
257453
  exports.isEvent = require_sleep.isEvent;
257396
257454
  exports.isFirstLevelMacroClass = isFirstLevelMacroClass;
257397
257455
  exports.isIsolatedBuiltin = isIsolatedBuiltin;
257456
+ exports.isLocationEnabled = isLocationEnabled;
257398
257457
  exports.isNode = isNode;
257399
257458
  exports.isObjectInput = isObjectInput;
257400
257459
  exports.isOccupancyRule = isOccupancyRule;