camstack 1.2.111 → 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-CcV3k-hD.js
23637
- var require_dist_CcV3k_hD = __commonJS({
23638
- "../system/dist/dist-CcV3k-hD.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_CcV3k_hD = __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_CcV3k_hD = __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_CcV3k_hD = __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_CcV3k_hD = __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) {
@@ -40670,7 +40673,6 @@ var require_dist_CcV3k_hD = __commonJS({
40670
40673
  }), zod.z.instanceof(Uint8Array)),
40671
40674
  endDownload: method(zod.z.object({ downloadId: zod.z.string() }), zod.z.void(), { kind: "mutation" }),
40672
40675
  listLocations: method(zod.z.object({ type: StorageLocationTypeSchema.optional() }), zod.z.array(StorageLocationSchema).readonly()),
40673
- getDefaultLocation: method(zod.z.object({ type: StorageLocationTypeSchema }), StorageLocationSchema.nullable()),
40674
40676
  listLocationDeclarations: method(zod.z.void(), zod.z.array(StorageLocationDeclarationSchema).readonly()),
40675
40677
  upsertLocation: method(StorageLocationSchema.omit({
40676
40678
  createdAt: true,
@@ -50706,6 +50708,26 @@ var require_dist_CcV3k_hD = __commonJS({
50706
50708
  "ffmpeg.threadCount": 0,
50707
50709
  "auth.tokenExpiry": "30d"
50708
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
+ }
50709
50731
  var CONTAINER_CHILD_PRIORITY = [
50710
50732
  "media-player",
50711
50733
  "alarm-panel",
@@ -50762,26 +50784,6 @@ var require_dist_CcV3k_hD = __commonJS({
50762
50784
  const namesake = (c) => containerName !== void 0 && containerName.length > 0 && c.name !== void 0 && c.name.toLowerCase() === containerName.toLowerCase() ? 0 : 1;
50763
50785
  return [...children].toSorted((a, b) => roleRank(a.role) - roleRank(b.role) || namesake(a) - namesake(b) || rank(a.type) - rank(b.type))[0] ?? null;
50764
50786
  }
50765
- var AccessoryKind = {
50766
- Siren: DeviceRole.Siren,
50767
- Floodlight: DeviceRole.Floodlight,
50768
- Spotlight: DeviceRole.Spotlight,
50769
- PirSensor: DeviceRole.PirSensor,
50770
- Chime: DeviceRole.Chime,
50771
- Autotrack: DeviceRole.Autotrack,
50772
- Nightvision: DeviceRole.Nightvision,
50773
- PrivacyMask: DeviceRole.PrivacyMask
50774
- };
50775
- AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
50776
- function deriveBatteryPresence(input) {
50777
- const status = input.status;
50778
- if (!status) return "sleeping";
50779
- if (status.sleeping !== true) return "awake";
50780
- const lastContactAt = status.lastContactAt;
50781
- if (typeof lastContactAt !== "number" || lastContactAt <= 0) return "sleeping";
50782
- const budget = input.unreachableAfterMs ?? 216e5;
50783
- return input.nowMs - lastContactAt > budget ? "unreachable" : "sleeping";
50784
- }
50785
50787
  var DECLARED_INTEGRATION_FIXED_KEY = "fixed";
50786
50788
  function declarationOwnerNodeId(localNodeId) {
50787
50789
  const raw = localNodeId ?? "hub";
@@ -56221,12 +56223,6 @@ var require_dist_CcV3k_hD = __commonJS({
56221
56223
  addonId: null,
56222
56224
  access: "view"
56223
56225
  },
56224
- "storage.getDefaultLocation": {
56225
- capName: "storage",
56226
- capScope: "system",
56227
- addonId: null,
56228
- access: "view"
56229
- },
56230
56226
  "storage.list": {
56231
56227
  capName: "storage",
56232
56228
  capScope: "system",
@@ -60112,6 +60108,12 @@ var require_dist_CcV3k_hD = __commonJS({
60112
60108
  return LOG_LEVEL_RANK;
60113
60109
  }
60114
60110
  });
60111
+ Object.defineProperty(exports, "LegacyStorageLocationDefaultSchema", {
60112
+ enumerable: true,
60113
+ get: function() {
60114
+ return LegacyStorageLocationDefaultSchema;
60115
+ }
60116
+ });
60115
60117
  Object.defineProperty(exports, "METHOD_ACCESS_MAP", {
60116
60118
  enumerable: true,
60117
60119
  get: function() {
@@ -60442,6 +60444,12 @@ var require_dist_CcV3k_hD = __commonJS({
60442
60444
  return isDeviceConfigCap;
60443
60445
  }
60444
60446
  });
60447
+ Object.defineProperty(exports, "isLocationEnabled", {
60448
+ enumerable: true,
60449
+ get: function() {
60450
+ return isLocationEnabled;
60451
+ }
60452
+ });
60445
60453
  Object.defineProperty(exports, "isObjectInput", {
60446
60454
  enumerable: true,
60447
60455
  get: function() {
@@ -60694,7 +60702,7 @@ var require_alerts_addon = __commonJS({
60694
60702
  [Symbol.toStringTag]: { value: "Module" }
60695
60703
  });
60696
60704
  require_chunk_Cek0wNdY();
60697
- var require_dist10 = require_dist_CcV3k_hD();
60705
+ var require_dist10 = require_dist_jZb_HKI5();
60698
60706
  function selectExpired(alerts, cutoffMs) {
60699
60707
  return alerts.filter((a) => a.createdAt < cutoffMs).sort((a, b) => a.createdAt - b.createdAt).map((a) => a.id);
60700
60708
  }
@@ -61513,7 +61521,7 @@ var require_console_logging = __commonJS({
61513
61521
  [Symbol.toStringTag]: { value: "Module" }
61514
61522
  });
61515
61523
  require_chunk_Cek0wNdY();
61516
- var require_dist10 = require_dist_CcV3k_hD();
61524
+ var require_dist10 = require_dist_jZb_HKI5();
61517
61525
  var require_formatter = require_formatter_DqAKDlvN();
61518
61526
  var LEVEL_RANK = {
61519
61527
  debug: 0,
@@ -61607,7 +61615,7 @@ var require_core_blocks_addon = __commonJS({
61607
61615
  "use strict";
61608
61616
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
61609
61617
  var require_chunk = require_chunk_Cek0wNdY();
61610
- var require_dist10 = require_dist_CcV3k_hD();
61618
+ var require_dist10 = require_dist_jZb_HKI5();
61611
61619
  var node_crypto = __require("crypto");
61612
61620
  var node_fs_promises = __require("fs/promises");
61613
61621
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -62504,11 +62512,11 @@ var require_core_blocks = __commonJS({
62504
62512
  }
62505
62513
  });
62506
62514
 
62507
- // ../system/dist/retired-settings-keys-Bd8fuwO1.js
62508
- var require_retired_settings_keys_Bd8fuwO1 = __commonJS({
62509
- "../system/dist/retired-settings-keys-Bd8fuwO1.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) {
62510
62518
  "use strict";
62511
- var require_dist10 = require_dist_CcV3k_hD();
62519
+ var require_dist10 = require_dist_jZb_HKI5();
62512
62520
  function settingsStoreIsAuthoritativeHere(env) {
62513
62521
  const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
62514
62522
  return raw === "" || raw === "hub";
@@ -64722,8 +64730,8 @@ var require_device_manager_addon = __commonJS({
64722
64730
  [Symbol.toStringTag]: { value: "Module" }
64723
64731
  });
64724
64732
  require_chunk_Cek0wNdY();
64725
- var require_dist10 = require_dist_CcV3k_hD();
64726
- var require_retired_settings_keys = require_retired_settings_keys_Bd8fuwO1();
64733
+ var require_dist10 = require_dist_jZb_HKI5();
64734
+ var require_retired_settings_keys = require_retired_settings_keys_Dxv9S_lO();
64727
64735
  var node_crypto = __require("crypto");
64728
64736
  var _camstack_types_node = require_node();
64729
64737
  var JOB_HISTORY = 20;
@@ -70348,7 +70356,7 @@ var require_hub_forwarder = __commonJS({
70348
70356
  [Symbol.toStringTag]: { value: "Module" }
70349
70357
  });
70350
70358
  require_chunk_Cek0wNdY();
70351
- var require_dist10 = require_dist_CcV3k_hD();
70359
+ var require_dist10 = require_dist_jZb_HKI5();
70352
70360
  var require_formatter = require_formatter_DqAKDlvN();
70353
70361
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
70354
70362
  var HubForwarderDestination = class {
@@ -70485,7 +70493,7 @@ var require_liveness_monitor_addon = __commonJS({
70485
70493
  "use strict";
70486
70494
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
70487
70495
  require_chunk_Cek0wNdY();
70488
- var require_dist10 = require_dist_CcV3k_hD();
70496
+ var require_dist10 = require_dist_jZb_HKI5();
70489
70497
  var NO_DEVICES = "liveness:no-devices";
70490
70498
  var ALL_OFFLINE = "liveness:all-devices-offline";
70491
70499
  var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
@@ -70681,7 +70689,7 @@ var require_local_auth_addon = __commonJS({
70681
70689
  [Symbol.toStringTag]: { value: "Module" }
70682
70690
  });
70683
70691
  var require_chunk = require_chunk_Cek0wNdY();
70684
- var require_dist10 = require_dist_CcV3k_hD();
70692
+ var require_dist10 = require_dist_jZb_HKI5();
70685
70693
  var node_crypto = __require("crypto");
70686
70694
  node_crypto = require_chunk.__toESM(node_crypto);
70687
70695
  var crypto$1 = __require("crypto");
@@ -78494,7 +78502,7 @@ var require_loki_logging = __commonJS({
78494
78502
  [Symbol.toStringTag]: { value: "Module" }
78495
78503
  });
78496
78504
  require_chunk_Cek0wNdY();
78497
- var require_dist10 = require_dist_CcV3k_hD();
78505
+ var require_dist10 = require_dist_jZb_HKI5();
78498
78506
  function sanitizeLabelName(raw) {
78499
78507
  const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
78500
78508
  return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
@@ -79059,7 +79067,7 @@ var require_native_metrics_addon = __commonJS({
79059
79067
  [Symbol.toStringTag]: { value: "Module" }
79060
79068
  });
79061
79069
  var require_chunk = require_chunk_Cek0wNdY();
79062
- var require_dist10 = require_dist_CcV3k_hD();
79070
+ var require_dist10 = require_dist_jZb_HKI5();
79063
79071
  var node_fs_promises = __require("fs/promises");
79064
79072
  var node_child_process = __require("child_process");
79065
79073
  var node_util = __require("util");
@@ -81643,7 +81651,7 @@ var require_filesystem_storage_addon = __commonJS({
81643
81651
  [Symbol.toStringTag]: { value: "Module" }
81644
81652
  });
81645
81653
  var require_chunk = require_chunk_Cek0wNdY();
81646
- var require_dist10 = require_dist_CcV3k_hD();
81654
+ var require_dist10 = require_dist_jZb_HKI5();
81647
81655
  var node_crypto = __require("crypto");
81648
81656
  var node_fs_promises = __require("fs/promises");
81649
81657
  var node_path = __require("path");
@@ -82759,8 +82767,8 @@ var require_sqlite_settings_addon = __commonJS({
82759
82767
  [Symbol.toStringTag]: { value: "Module" }
82760
82768
  });
82761
82769
  var require_chunk = require_chunk_Cek0wNdY();
82762
- var require_dist10 = require_dist_CcV3k_hD();
82763
- var require_retired_settings_keys = require_retired_settings_keys_Bd8fuwO1();
82770
+ var require_dist10 = require_dist_jZb_HKI5();
82771
+ var require_retired_settings_keys = require_retired_settings_keys_Dxv9S_lO();
82764
82772
  var node_crypto = __require("crypto");
82765
82773
  var node_fs = __require("fs");
82766
82774
  var node_module = __require("module");
@@ -85250,7 +85258,7 @@ var require_storage_orchestrator_addon = __commonJS({
85250
85258
  [Symbol.toStringTag]: { value: "Module" }
85251
85259
  });
85252
85260
  var require_chunk = require_chunk_Cek0wNdY();
85253
- var require_dist10 = require_dist_CcV3k_hD();
85261
+ var require_dist10 = require_dist_jZb_HKI5();
85254
85262
  var require_hub_cap_forward = require_hub_cap_forward_DmHNjbB0();
85255
85263
  var zod = require_zod();
85256
85264
  var node_crypto = __require("crypto");
@@ -85397,7 +85405,7 @@ var require_storage_orchestrator_addon = __commonJS({
85397
85405
  display_name: loc.displayName,
85398
85406
  provider_id: loc.providerId,
85399
85407
  config: storedConfig,
85400
- is_default: loc.isDefault ? 1 : 0,
85408
+ is_default: 0,
85401
85409
  is_system: loc.isSystem ? 1 : 0,
85402
85410
  created_at: loc.createdAt,
85403
85411
  updated_at: loc.updatedAt
@@ -85426,7 +85434,7 @@ var require_storage_orchestrator_addon = __commonJS({
85426
85434
  config,
85427
85435
  ...nodeId !== void 0 ? { nodeId } : {},
85428
85436
  ...enabled !== void 0 ? { enabled } : {},
85429
- isDefault: row["is_default"] === 1 || row["is_default"] === true,
85437
+ ...require_dist10.LegacyStorageLocationDefaultSchema.parse({ isDefault: row["is_default"] === 1 || row["is_default"] === true }),
85430
85438
  isSystem: row["is_system"] === 1 || row["is_system"] === true,
85431
85439
  createdAt: Number(row["created_at"] ?? 0),
85432
85440
  updatedAt: Number(row["updated_at"] ?? 0)
@@ -85475,16 +85483,18 @@ var require_storage_orchestrator_addon = __commonJS({
85475
85483
  const targetId = input.destinations[storageClass];
85476
85484
  if (targetId === void 0) continue;
85477
85485
  const namedSourceId = input.sources?.[storageClass];
85478
- const defaultLoc = this.deps.locations.getDefaultLocation(storageClass);
85479
- const source = namedSourceId ? this.deps.locations.getLocationById(namedSourceId) : defaultLoc;
85480
- 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.`);
85481
85491
  if (source.type !== storageClass) throw new Error(`Storage location "${source.id}" is type "${source.type}", expected "${storageClass}"`);
85482
85492
  const target = this.deps.locations.getLocationById(targetId);
85483
85493
  if (!target) throw new Error(`Storage location "${targetId}" not found`);
85484
85494
  if (target.type !== storageClass) throw new Error(`Storage location "${targetId}" is type "${target.type}", expected "${storageClass}"`);
85485
- 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`);
85486
- const freezeSource = defaultLoc === null || source.id !== defaultLoc.id;
85487
- 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.`);
85488
85498
  moves.push({
85489
85499
  storageClass,
85490
85500
  fromLocationId: source.id,
@@ -85501,7 +85511,7 @@ var require_storage_orchestrator_addon = __commonJS({
85501
85511
  if (moves.some((move) => move.storageClass === "eventMedia")) {
85502
85512
  const unstamped = await this.deps.participants.analytics.countUnstamped();
85503
85513
  if (unstamped === null) {
85504
- 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.");
85505
85515
  findings.push({
85506
85516
  code: "unstampedEventMediaRows",
85507
85517
  storageClass: "eventMedia",
@@ -85510,7 +85520,7 @@ var require_storage_orchestrator_addon = __commonJS({
85510
85520
  } else if (unstamped.anyPresent) {
85511
85521
  const scale = unstamped.total === null ? "an unknown number of" : unstamped.total.toString();
85512
85522
  const perLane = `${describeUnstamped(unstamped.media)} media, ${describeUnstamped(unstamped.retrainFrames)} retrain frames`;
85513
- 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"}" }).`);
85514
85524
  findings.push({
85515
85525
  code: "unstampedEventMediaRows",
85516
85526
  storageClass: "eventMedia",
@@ -85649,7 +85659,7 @@ var require_storage_orchestrator_addon = __commonJS({
85649
85659
  * remaining" action.
85650
85660
  *
85651
85661
  * A class appears here only when something is (or might be) left on a
85652
- * location that is not its default. An empty result therefore means exactly
85662
+ * location outside its write set. An empty result therefore means exactly
85653
85663
  * "there is nothing to drain", which is what lets the UI offer the action
85654
85664
  * only when it is true, and what lets {@link drain} refuse rather than start
85655
85665
  * a job that would move nothing and report `done` — the failure mode D295
@@ -85663,11 +85673,13 @@ var require_storage_orchestrator_addon = __commonJS({
85663
85673
  const out = [];
85664
85674
  for (const storageClass of STORAGE_CLASSES) {
85665
85675
  if (!MOVER_CLASSES.includes(storageClass)) continue;
85666
- const target = this.deps.locations.getDefaultLocation(storageClass);
85676
+ const writeSet = this.deps.locations.writeTargets(storageClass);
85677
+ const target = writeSet[0];
85667
85678
  if (!target) continue;
85679
+ const inWriteSet = new Set(writeSet.map((loc) => loc.id));
85668
85680
  if (laneOf(storageClass) === "media") {
85669
85681
  for (const source of this.deps.locations.listLocations({ type: storageClass })) {
85670
- if (source.id === target.id) continue;
85682
+ if (inWriteSet.has(source.id)) continue;
85671
85683
  const count = await unanswerable(this.deps.participants.analytics.residue({
85672
85684
  toLocationId: target.id,
85673
85685
  fromLocationId: source.id,
@@ -85685,7 +85697,7 @@ var require_storage_orchestrator_addon = __commonJS({
85685
85697
  continue;
85686
85698
  }
85687
85699
  for (const source of this.deps.locations.listLocations({ type: storageClass })) {
85688
- if (source.id === target.id) continue;
85700
+ if (inWriteSet.has(source.id)) continue;
85689
85701
  const census = await unanswerable(this.deps.participants.recorder.residue({
85690
85702
  fromLocationId: source.id,
85691
85703
  footageClass: storageClass === "recordingsLow" ? "recordingsLow" : "recordings"
@@ -85703,12 +85715,12 @@ var require_storage_orchestrator_addon = __commonJS({
85703
85715
  return out;
85704
85716
  }
85705
85717
  /**
85706
- * 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.
85707
85719
  *
85708
85720
  * ## Why this is a second verb rather than a looser `start`
85709
85721
  *
85710
- * `start` refuses a destination that is already the class's default
85711
- * (`"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
85712
85724
  * is correct and it is load-bearing: there is genuinely nothing left to
85713
85725
  * repoint, and an operator must never be able to re-repoint a migrated class
85714
85726
  * by accident. Making `start` idempotent — "an already-repointed class
@@ -85717,7 +85729,7 @@ var require_storage_orchestrator_addon = __commonJS({
85717
85729
  * operator reads ("pauses the writers…") would be a lie half the time.
85718
85730
  *
85719
85731
  * `drain` instead cannot repoint AT ALL: it never touches
85720
- * `setDefaultLocations`, and its job starts in `draining` with `repointed`
85732
+ * `setWriteTargets`, and its job starts in `draining` with `repointed`
85721
85733
  * already true, so the `repointing` / `refreshing` / `resuming` blocks of
85722
85734
  * {@link run} are behind it and unreachable. The two verbs are disjoint, and
85723
85735
  * `start`'s refusal keeps meaning exactly what it meant.
@@ -85727,7 +85739,7 @@ var require_storage_orchestrator_addon = __commonJS({
85727
85739
  * The finished job is the audit of what happened; re-opening it destroys
85728
85740
  * that. And a drain is needed in cases where no migration job ever existed
85729
85741
  * (a mover armed by hand, footage stranded on a location an operator added
85730
- * 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
85731
85743
  * from {@link residue} — the archive — not from what a previous job believed.
85732
85744
  */
85733
85745
  async drain(input) {
@@ -85750,9 +85762,9 @@ var require_storage_orchestrator_addon = __commonJS({
85750
85762
  const destinations = {};
85751
85763
  for (const storageClass of input.classes) {
85752
85764
  const remaining = residue.filter((entry) => entry.storageClass === storageClass);
85753
- 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.`);
85754
85766
  for (const entry of remaining) {
85755
- 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.`);
85756
85768
  moves.push({
85757
85769
  storageClass,
85758
85770
  fromLocationId: entry.fromLocationId,
@@ -85870,8 +85882,13 @@ var require_storage_orchestrator_addon = __commonJS({
85870
85882
  await this.setPhase(job, "repointing");
85871
85883
  }
85872
85884
  if (job.phase === "repointing") {
85873
- const targets = new Map(job.moves.filter((move) => move.freezeSource !== true).map((move) => [move.storageClass, move.toLocationId]));
85874
- 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);
85875
85892
  job.repointed = true;
85876
85893
  await this.setPhase(job, "refreshing");
85877
85894
  }
@@ -86071,9 +86088,9 @@ var require_storage_orchestrator_addon = __commonJS({
86071
86088
  for (const move of job.moves) if (move.state !== "done") throw new Error(`${move.storageClass} move did not complete verification`);
86072
86089
  }
86073
86090
  /**
86074
- * After a from→to copy whose source was NOT the class default: stop new
86075
- * writes to `from`. The default is unchanged (that is the whole point of
86076
- * 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
86077
86094
  * left alone.
86078
86095
  */
86079
86096
  freezeDrainedSources(job) {
@@ -86671,33 +86688,77 @@ var require_storage_orchestrator_addon = __commonJS({
86671
86688
  backfilled: backfill.length,
86672
86689
  total: this.locations.size
86673
86690
  } });
86674
- this.reportDisabledDefaults();
86691
+ this.migrateOntoTheWriteSet(rows);
86675
86692
  }
86676
86693
  /**
86677
- * 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.
86678
86697
  *
86679
- * `upsertLocation` can now produce this (enabled is the write set; `isDefault`
86680
- * is store residue). Hydrate also writes rows straight into the map, so a
86681
- * hand-edited store or a row written by another build can too. It matters
86682
- * because every bare-type ref still resolves through {@link getDefaultLocation}
86683
- * until Stage 5 of the enabled-write-set design: on such a row
86684
- * `storage.write({ location: 'eventMedia' })` and the migration coordinator's
86685
- * 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:
86686
86700
  *
86687
- * Reported, never repaired: flipping an operator's flag back on at boot would
86688
- * be the system overruling the switch instead of obeying it. The coordinator
86689
- * refuses the drain (`storage-migration-coordinator.drain`); the bare-ref read
86690
- * path is deliberately left working, because narrowing a READ is a worse bug
86691
- * than the one this line reports.
86692
- */
86693
- 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 = [];
86694
86720
  for (const loc of this.locations.values()) {
86695
- if (!loc.isDefault || loc.enabled !== false) continue;
86696
- 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: {
86697
- id: loc.id,
86698
- type: loc.type
86699
- } });
86721
+ if (loc.enabled !== void 0) continue;
86722
+ this.persistMigrated({
86723
+ ...loc,
86724
+ enabled: true
86725
+ });
86726
+ stamped.push(loc.id);
86700
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);
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
+ });
86701
86762
  }
86702
86763
  /**
86703
86764
  * Inject the declaration-driven cardinality source. Called once by the
@@ -86752,7 +86813,7 @@ var require_storage_orchestrator_addon = __commonJS({
86752
86813
  const rows = await this.locationStore.loadAll();
86753
86814
  for (const loc of rows) this.locations.set(loc.id, loc);
86754
86815
  this.logger.info("storage-orchestrator: hydrated locations from store", { meta: { loaded: this.locations.size } });
86755
- this.reportDisabledDefaults();
86816
+ this.migrateOntoTheWriteSet(rows);
86756
86817
  }
86757
86818
  /**
86758
86819
  * Boot backfill (SP1): stamp `nodeId` on every persisted node-local
@@ -86801,40 +86862,51 @@ var require_storage_orchestrator_addon = __commonJS({
86801
86862
  const all = [...this.locations.values()];
86802
86863
  return filter?.type ? all.filter((l) => l.type === filter.type) : all;
86803
86864
  }
86804
- getDefaultLocation(type) {
86805
- for (const loc of this.locations.values()) if (loc.type === type && loc.isDefault) return loc;
86806
- return null;
86807
- }
86808
86865
  /**
86809
- * Repoint several logical defaults as one in-memory commit. The migration
86810
- * coordinator persists its `repointing` phase before calling this method, so
86811
- * a process crash during the backing-store writes is recovered by replaying
86812
- * 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).
86813
86871
  */
86814
- async setDefaultLocations(targetByType) {
86815
- 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;
86816
86893
  const now = Date.now();
86817
86894
  const nextById = new Map(this.locations);
86818
86895
  const touched = /* @__PURE__ */ new Map();
86819
- for (const [type, targetId] of targetByType) {
86820
- const target = nextById.get(targetId);
86821
- if (!target) throw new Error(`Storage location "${targetId}" not found`);
86822
- if (target.type !== type) throw new Error(`Storage location "${targetId}" is type "${target.type}", expected "${type}"`);
86823
- for (const [id, location] of nextById) {
86824
- if (location.type !== type) continue;
86825
- const isDefault = id === targetId;
86826
- const enable = isDefault && location.enabled === false;
86827
- if (location.isDefault === isDefault && !enable) continue;
86828
- const updated = {
86829
- ...location,
86830
- isDefault,
86831
- ...enable ? { enabled: true } : {},
86832
- updatedAt: now
86833
- };
86834
- nextById.set(id, updated);
86835
- touched.set(id, updated);
86836
- }
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);
86837
86907
  }
86908
+ if (touched.size === 0) return;
86909
+ refuseTypeWithoutWriteTarget(touched.values(), nextById.values());
86838
86910
  if (this.locationStore) {
86839
86911
  const previous = /* @__PURE__ */ new Map();
86840
86912
  for (const id of touched.keys()) {
@@ -86855,13 +86927,10 @@ var require_storage_orchestrator_addon = __commonJS({
86855
86927
  for (const [id, location] of nextById) this.locations.set(id, location);
86856
86928
  }
86857
86929
  /**
86858
- * Insert or update a location. If `isDefault: true`, atomically
86859
- * demotes any other default for the same type — operators always see
86860
- * exactly one default per type.
86930
+ * Insert or update a location.
86861
86931
  *
86862
- * When a persistence store is wired (Task 6), the new record AND any
86863
- * implicitly-demoted siblings are persisted before the in-memory map
86864
- * 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.
86865
86934
  */
86866
86935
  /**
86867
86936
  * Refuse a `(location kind, provider)` pair the kind cannot use.
@@ -86947,30 +87016,17 @@ var require_storage_orchestrator_addon = __commonJS({
86947
87016
  updatedAt: now
86948
87017
  };
86949
87018
  refuseLastWriteTargetDisabled(existing, next, this.locations.values());
86950
- const demoted = [];
86951
- if (next.isDefault) {
86952
- for (const [otherId, otherLoc] of this.locations) if (otherLoc.type === next.type && otherLoc.id !== next.id && otherLoc.isDefault) {
86953
- const updated = {
86954
- ...otherLoc,
86955
- isDefault: false,
86956
- updatedAt: now
86957
- };
86958
- this.locations.set(otherId, updated);
86959
- demoted.push(updated);
86960
- }
86961
- }
86962
87019
  this.locations.set(next.id, next);
86963
87020
  this.logger.debug("storage-orchestrator: upsertLocation", { meta: {
86964
87021
  id: next.id,
86965
87022
  type: next.type,
86966
87023
  providerId: next.providerId,
86967
- isDefault: next.isDefault
87024
+ enabled: next.enabled
86968
87025
  } });
86969
87026
  if (this.locationStore) {
86970
87027
  const store = this.locationStore;
86971
87028
  (async () => {
86972
87029
  try {
86973
- for (const d of demoted) await store.upsert(d);
86974
87030
  await store.upsert(next);
86975
87031
  } catch (err) {
86976
87032
  this.logger.error("storage-orchestrator: upsert persistence failed", { meta: {
@@ -86985,8 +87041,7 @@ var require_storage_orchestrator_addon = __commonJS({
86985
87041
  /**
86986
87042
  * Remove a location. Refuses the last location of a type and the last
86987
87043
  * enabled location of a type. `isSystem` is informational — a frozen
86988
- * recordings `:default` is deletable once a sibling is live. Deleting
86989
- * the flagged default promotes an enabled sibling. Occupancy still
87044
+ * recordings `:default` is deletable once a sibling is live. Occupancy still
86990
87045
  * refuses unless `force` is set; force never overrides uniqueness /
86991
87046
  * last-enabled.
86992
87047
  */
@@ -86997,8 +87052,6 @@ var require_storage_orchestrator_addon = __commonJS({
86997
87052
  if (ofType.length <= 1) throw new Error(`Cannot delete "${id}" \u2014 it is the only location for type "${loc.type}"`);
86998
87053
  const enabledOfType = ofType.filter((l) => l.enabled !== false);
86999
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}"`);
87000
- const successor = loc.isDefault === true ? ofType.find((l) => l.id !== id && l.enabled !== false) : void 0;
87001
- if (loc.isDefault === true && successor === void 0) throw new Error(`Cannot delete "${id}" \u2014 it is the only enabled location for type "${loc.type}"`);
87002
87055
  const occupancy = await this.probeOccupancy(loc);
87003
87056
  if (occupancy !== "empty") {
87004
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`);
@@ -87008,21 +87061,6 @@ var require_storage_orchestrator_addon = __commonJS({
87008
87061
  occupancy
87009
87062
  } });
87010
87063
  }
87011
- if (successor !== void 0 && successor.isDefault !== true) {
87012
- const now = Date.now();
87013
- const promoted = {
87014
- ...successor,
87015
- isDefault: true,
87016
- updatedAt: now
87017
- };
87018
- this.locations.set(successor.id, promoted);
87019
- if (this.locationStore) this.locationStore.upsert(promoted).catch((err) => {
87020
- this.logger.error("storage-orchestrator: default promotion persist failed", { meta: {
87021
- id: successor.id,
87022
- error: err instanceof Error ? err.message : String(err)
87023
- } });
87024
- });
87025
- }
87026
87064
  this.locations.delete(id);
87027
87065
  if (this.locationStore) this.locationStore.delete(id).catch((err) => {
87028
87066
  this.logger.error("storage-orchestrator: delete persistence failed", { meta: {
@@ -87101,9 +87139,17 @@ var require_storage_orchestrator_addon = __commonJS({
87101
87139
  if (!loc) throw new Error(`Storage location "${ref}" not found`);
87102
87140
  return loc;
87103
87141
  }
87104
- const def = this.getDefaultLocation(ref);
87105
- if (!def) throw new Error(`No default storage location configured for type "${ref}" \u2014 register one or pass a fully-qualified id (\`<type>:<slug>\`)`);
87106
- 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;
87107
87153
  }
87108
87154
  /**
87109
87155
  * Find the storage-provider that backs a given location — the entry point
@@ -87207,9 +87253,11 @@ var require_storage_orchestrator_addon = __commonJS({
87207
87253
  * declarations on a populated map is a no-op (each declared `id`
87208
87254
  * already resolves to an existing `<id>:default`).
87209
87255
  *
87210
- * Convention: the bare-type ref `'recordings'` resolves via
87211
- * `getDefaultLocation('recordings')` (walks `isDefault === true`); the
87212
- * 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
87213
87261
  * `^[a-z][a-z0-9-]*:[a-z0-9-]+$` regex on `StorageLocationSchema.id`.
87214
87262
  *
87215
87263
  * `basePath` is the storage root (e.g. `<CAMSTACK_DATA>`). Each
@@ -87234,7 +87282,7 @@ var require_storage_orchestrator_addon = __commonJS({
87234
87282
  displayName: d.displayName,
87235
87283
  providerId: input.providerId,
87236
87284
  config: { basePath: base },
87237
- isDefault: true,
87285
+ enabled: true,
87238
87286
  isSystem: false
87239
87287
  });
87240
87288
  added++;
@@ -87254,7 +87302,7 @@ var require_storage_orchestrator_addon = __commonJS({
87254
87302
  const requested = input.enabled ?? existing?.enabled;
87255
87303
  if (requested === void 0) {
87256
87304
  if (existing) return {};
87257
- return { enabled: input.isDefault || !typeAlreadyHasLocation };
87305
+ return { enabled: !typeAlreadyHasLocation };
87258
87306
  }
87259
87307
  return { enabled: requested };
87260
87308
  }
@@ -87270,6 +87318,16 @@ var require_storage_orchestrator_addon = __commonJS({
87270
87318
  }
87271
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.`);
87272
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
+ }
87273
87331
  var DEFAULT_MAX_ROUNDS = 8;
87274
87332
  var DEFAULT_HYSTERESIS_PERCENT = 1;
87275
87333
  var StoragePressureManager = class {
@@ -87474,13 +87532,9 @@ var require_storage_orchestrator_addon = __commonJS({
87474
87532
  const rows = type !== void 0 ? service.listLocations({ type }) : service.listLocations();
87475
87533
  return Promise.all(rows.map(async (loc) => ({
87476
87534
  ...this.redacted(loc),
87477
- capacity: loc.enabled === false ? null : await this.localCapacityOf(loc)
87535
+ capacity: require_dist10.isLocationEnabled(loc) ? await this.localCapacityOf(loc) : null
87478
87536
  })));
87479
87537
  },
87480
- getDefaultLocation: async ({ type }) => {
87481
- const loc = service.getDefaultLocation(type);
87482
- return loc === null ? null : this.redacted(loc);
87483
- },
87484
87538
  listLocationDeclarations: async () => service.listDeclarations(),
87485
87539
  upsertLocation: async (input) => {
87486
87540
  const stored = service.getLocationById(input.id);
@@ -88201,7 +88255,7 @@ var require_system_config_addon = __commonJS({
88201
88255
  [Symbol.toStringTag]: { value: "Module" }
88202
88256
  });
88203
88257
  require_chunk_Cek0wNdY();
88204
- var require_dist10 = require_dist_CcV3k_hD();
88258
+ var require_dist10 = require_dist_jZb_HKI5();
88205
88259
  var SECTION_TITLES = {
88206
88260
  server: "Server",
88207
88261
  auth: "Authentication"
@@ -106262,7 +106316,7 @@ var require_winston_logging = __commonJS({
106262
106316
  [Symbol.toStringTag]: { value: "Module" }
106263
106317
  });
106264
106318
  var require_chunk = require_chunk_Cek0wNdY();
106265
- var require_dist10 = require_dist_CcV3k_hD();
106319
+ var require_dist10 = require_dist_jZb_HKI5();
106266
106320
  var require_formatter = require_formatter_DqAKDlvN();
106267
106321
  var node_path = __require("path");
106268
106322
  node_path = require_chunk.__toESM(node_path);
@@ -112094,47 +112148,17 @@ var require_addon = __commonJS({
112094
112148
  },
112095
112149
  "setDeviceConfig": { "config": { "__nested__": { "bands": [] } } }
112096
112150
  },
112097
- "storage": { "upsertLocation": {
112098
- "isDefault": false,
112099
- "isSystem": false
112100
- } },
112151
+ "storage": { "upsertLocation": { "isSystem": false } },
112101
112152
  "storage-provider": {
112102
- "beginDownload": { "location": { "__nested__": {
112103
- "isDefault": false,
112104
- "isSystem": false
112105
- } } },
112106
- "beginUpload": { "location": { "__nested__": {
112107
- "isDefault": false,
112108
- "isSystem": false
112109
- } } },
112110
- "delete": { "location": { "__nested__": {
112111
- "isDefault": false,
112112
- "isSystem": false
112113
- } } },
112114
- "exists": { "location": { "__nested__": {
112115
- "isDefault": false,
112116
- "isSystem": false
112117
- } } },
112118
- "getAvailableSpace": { "location": { "__nested__": {
112119
- "isDefault": false,
112120
- "isSystem": false
112121
- } } },
112122
- "list": { "location": { "__nested__": {
112123
- "isDefault": false,
112124
- "isSystem": false
112125
- } } },
112126
- "read": { "location": { "__nested__": {
112127
- "isDefault": false,
112128
- "isSystem": false
112129
- } } },
112130
- "resolve": { "location": { "__nested__": {
112131
- "isDefault": false,
112132
- "isSystem": false
112133
- } } },
112134
- "write": { "location": { "__nested__": {
112135
- "isDefault": false,
112136
- "isSystem": false
112137
- } } }
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 } } }
112138
112162
  },
112139
112163
  "stream-broker": {
112140
112164
  "produceEventMedia": {
@@ -130900,7 +130924,7 @@ var require_dist3 = __commonJS({
130900
130924
  "use strict";
130901
130925
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
130902
130926
  var require_chunk = require_chunk_Cek0wNdY();
130903
- var require_dist10 = require_dist_CcV3k_hD();
130927
+ var require_dist10 = require_dist_jZb_HKI5();
130904
130928
  var require_builtins_alerts_alerts_addon = require_alerts_addon();
130905
130929
  require_alerts();
130906
130930
  var require_formatter = require_formatter_DqAKDlvN();
@@ -212907,253 +212931,6 @@ var require_dist4 = __commonJS({
212907
212931
  function logLevelAtMost(level, threshold) {
212908
212932
  return LOG_LEVEL_RANK[level] <= LOG_LEVEL_RANK[threshold];
212909
212933
  }
212910
- var LogChannelLevelSchema = zod.z.enum([
212911
- "info",
212912
- "warn",
212913
- "error"
212914
- ]);
212915
- var LogChannelDescriptorSchema = zod.z.object({
212916
- /**
212917
- * Dotted `area.thing`, unique across the workspace. `area` is conventionally
212918
- * the addon's short name so an operator reading a channel list can tell who
212919
- * owns it without a second lookup.
212920
- */
212921
- name: zod.z.string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
212922
- /** One sentence: what the operator will SEE after arming it. */
212923
- description: zod.z.string().min(1),
212924
- /** The level its lines are emitted at. Never below `info`. */
212925
- defaultLevel: LogChannelLevelSchema,
212926
- /**
212927
- * Whether this channel can be narrowed to a camera.
212928
- *
212929
- * `true` is a PROMISE with two halves, and both must hold: the gate is
212930
- * consulted with the numeric device id, AND every line the channel admits
212931
- * carries `tags: { deviceId }` with that same numeric id. The second half is
212932
- * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
212933
- * keeps `deviceId` out of the stream labels for cardinality, so the tag in
212934
- * the body is the only way to filter.
212935
- *
212936
- * A channel whose lines carry the device only in `meta` (or not at all) is
212937
- * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
212938
- * the operator narrows to one camera, sees nothing, and concludes the code
212939
- * path was never taken.
212940
- */
212941
- perDevice: zod.z.boolean()
212942
- });
212943
- var LogChannelWindowSchema = zod.z.object({
212944
- channel: zod.z.string().min(1),
212945
- /** Epoch ms the window closes at. */
212946
- armedUntilMs: zod.z.number(),
212947
- /** `null` = every camera. A non-empty list narrows to those numeric ids. */
212948
- deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable()
212949
- });
212950
- var LogChannelGate = class {
212951
- descriptor;
212952
- /**
212953
- * HOT PATH GUARD. A plain data FIELD, and it must stay one.
212954
- *
212955
- * `log-channel.spec.ts` asserts the property descriptor has no getter and
212956
- * booby-traps the device set, so turning this into an accessor — or reading
212957
- * anything before it — fails the spec instead of taxing every line the
212958
- * process emits.
212959
- */
212960
- on = false;
212961
- /** `null` while armed for every camera. Never read while `on` is false. */
212962
- devices = null;
212963
- level;
212964
- closesAtMs = 0;
212965
- constructor(descriptor) {
212966
- this.descriptor = descriptor;
212967
- this.level = descriptor.defaultLevel;
212968
- }
212969
- /** Epoch ms this channel disarms itself at. 0 when disarmed. */
212970
- get armedUntilMs() {
212971
- return this.on ? this.closesAtMs : 0;
212972
- }
212973
- /**
212974
- * Does this channel want a line about `deviceId`?
212975
- *
212976
- * Call it only behind `gate.on &&`. On its own it is still correct — the
212977
- * guard is repeated inside — but the point of the prefix is that a disarmed
212978
- * channel must not pay the call at all.
212979
- */
212980
- wants(deviceId) {
212981
- if (!this.on) return false;
212982
- return this.devices === null || this.devices.has(deviceId);
212983
- }
212984
- /**
212985
- * Emit one line on this channel, at the channel's declared level.
212986
- *
212987
- * The channel name is added as `tags.logChannel` so LogQL can select the
212988
- * channel without matching on the message text, and whatever `tags` the
212989
- * caller passed — `deviceId` above all — is preserved.
212990
- */
212991
- log(logger, message, extras) {
212992
- if (!this.on) return;
212993
- const tags = {
212994
- ...extras.tags,
212995
- logChannel: this.descriptor.name
212996
- };
212997
- const line = {
212998
- ...extras,
212999
- tags
213000
- };
213001
- if (this.level === "error") logger.error(message, line);
213002
- else if (this.level === "warn") logger.warn(message, line);
213003
- else logger.info(message, line);
213004
- }
213005
- /**
213006
- * Arm (or RE-arm, restarting) this channel. Off the hot path only.
213007
- *
213008
- * An empty `deviceIds` list is treated as "every camera" rather than "no
213009
- * camera": a window that matches nothing is indistinguishable from a
213010
- * disarmed one, and the operator who asked for it would wait for lines that
213011
- * can never come.
213012
- */
213013
- arm(window2) {
213014
- const ids = window2.deviceIds;
213015
- this.devices = ids === null || ids.length === 0 ? null : new Set(ids);
213016
- this.closesAtMs = window2.armedUntilMs;
213017
- this.on = true;
213018
- }
213019
- /** Disarm. Off the hot path only. */
213020
- disarm() {
213021
- this.on = false;
213022
- this.devices = null;
213023
- this.closesAtMs = 0;
213024
- }
213025
- };
213026
- var LogChannelRegistry = class {
213027
- gates = /* @__PURE__ */ new Map();
213028
- /**
213029
- * Declare a channel and get its gate.
213030
- *
213031
- * A duplicate name throws. Two declarations of one name is a programming
213032
- * error, not a merge: the operator would arm one and the other would stay
213033
- * dark, which is the dead-knob shape (D62) with an extra step.
213034
- */
213035
- declare(descriptor) {
213036
- const parsed = LogChannelDescriptorSchema.parse(descriptor);
213037
- 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`);
213038
- const gate = new LogChannelGate(parsed);
213039
- this.gates.set(parsed.name, gate);
213040
- return gate;
213041
- }
213042
- /** The declarations, sorted by name so a list is stable to read and diff. */
213043
- list() {
213044
- return [...this.gates.values()].map((gate) => gate.descriptor).sort((a, b) => a.name.localeCompare(b.name));
213045
- }
213046
- /** The gate for a declared channel, or `undefined`. */
213047
- gate(name) {
213048
- return this.gates.get(name);
213049
- }
213050
- /**
213051
- * Apply the FULL set of armed windows. Off the hot path.
213052
- *
213053
- * Full, not incremental, and that is the whole design: the document is the
213054
- * authority, so a channel the document does not name is disarmed here. An
213055
- * incremental apply would let a disarm get lost in transit and leave a
213056
- * channel running that nobody can see is running.
213057
- *
213058
- * A window already past its deadline is ignored rather than armed — a
213059
- * restore that re-armed an expired window would make a forgotten diagnostic
213060
- * immortal across restarts.
213061
- *
213062
- * Returns the names it could not place, so the caller can log them: a
213063
- * channel named in the document that this process does not declare is
213064
- * either a typo or an addon that has not booted yet, and both deserve a
213065
- * line rather than silence.
213066
- */
213067
- apply(windows, nowMs) {
213068
- const wanted = /* @__PURE__ */ new Map();
213069
- const unknown = [];
213070
- for (const window2 of windows) {
213071
- if (window2.armedUntilMs <= nowMs) continue;
213072
- if (!this.gates.has(window2.channel)) {
213073
- unknown.push(window2.channel);
213074
- continue;
213075
- }
213076
- wanted.set(window2.channel, window2);
213077
- }
213078
- for (const [name, gate] of this.gates) {
213079
- const window2 = wanted.get(name);
213080
- if (window2 === void 0) gate.disarm();
213081
- else gate.arm(window2);
213082
- }
213083
- return unknown;
213084
- }
213085
- /**
213086
- * Disarm whatever has run out. Called on a timer, NEVER from a log path — a
213087
- * diagnostic that adds a `Date.now()` to the path it is measuring measures
213088
- * itself.
213089
- *
213090
- * Returns the names it closed, so the caller can write the one line that
213091
- * says a window ended and stops "it went quiet" from reading as "the branch
213092
- * was not taken".
213093
- */
213094
- tick(nowMs) {
213095
- const closed = [];
213096
- for (const [name, gate] of this.gates) if (gate.on && gate.armedUntilMs <= nowMs) {
213097
- gate.disarm();
213098
- closed.push(name);
213099
- }
213100
- return closed;
213101
- }
213102
- /** The channels armed right now, as the document would describe them. */
213103
- armed() {
213104
- const out = [];
213105
- for (const [name, gate] of this.gates) if (gate.on) out.push({
213106
- channel: name,
213107
- armedUntilMs: gate.armedUntilMs,
213108
- deviceIds: null
213109
- });
213110
- return out;
213111
- }
213112
- };
213113
- var instance = null;
213114
- function getLogChannelRegistry() {
213115
- instance ??= new LogChannelRegistry();
213116
- return instance;
213117
- }
213118
- function declareLogChannel(descriptor) {
213119
- return getLogChannelRegistry().declare(descriptor);
213120
- }
213121
- function __resetLogChannelRegistryForTests() {
213122
- instance = null;
213123
- }
213124
- var LOG_CHANNEL_TICK_MS = 5e3;
213125
- function createLogChannelsProvider(logger, options = {}) {
213126
- const registry = getLogChannelRegistry();
213127
- const now = options.now ?? Date.now;
213128
- const tickMs = options.tickMs ?? 5e3;
213129
- const timer = setInterval(() => {
213130
- const closed = registry.tick(now());
213131
- for (const name of closed) logger.info("log channel window closed", {
213132
- tags: { logChannel: name },
213133
- meta: { channel: name }
213134
- });
213135
- }, tickMs);
213136
- timer.unref?.();
213137
- return {
213138
- list: () => registry.list(),
213139
- apply: (input) => {
213140
- const unknown = registry.apply(input.windows, now());
213141
- const armed = registry.armed();
213142
- logger.info("log channels applied", { meta: {
213143
- armed: armed.map((window2) => window2.channel),
213144
- unknown,
213145
- declared: registry.list().length
213146
- } });
213147
- return {
213148
- armed: armed.length,
213149
- unknown
213150
- };
213151
- },
213152
- stop: () => {
213153
- clearInterval(timer);
213154
- }
213155
- };
213156
- }
213157
212934
  var OpsLogDomainSchema = zod.z.enum(["recording", "events"]);
213158
212935
  var OpsLogOpSchema = zod.z.enum([
213159
212936
  "prune",
@@ -213859,21 +213636,20 @@ var require_dist4 = __commonJS({
213859
213636
  * flag at upsert time, not here (the schema is provider-agnostic).
213860
213637
  */
213861
213638
  nodeId: zod.z.string().optional(),
213862
- isDefault: zod.z.boolean().default(false),
213863
213639
  isSystem: zod.z.boolean().default(false),
213864
213640
  /**
213865
- * Operator opt-in: whether consumers that BALANCE across several locations
213866
- * of a type may write here. Recordings reads it today; event media and
213867
- * backups are the next consumers, which is why the flag lives on the
213868
- * location rather than in any one addon's store — nothing has to be
213869
- * 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.
213870
213646
  *
213871
- * OPTIONAL, and ABSENT MEANS ACTIVE. Every location persisted before the
213872
- * flag existed reads back with no flag and keeps working exactly as before;
213873
- * that is the whole compat story, and it is why no migration ships with it.
213874
- * A newly CREATED sibling is stamped `false` by the orchestrator (creating a
213875
- * disk must not silently start writing to it); the default of a type is
213876
- * 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.
213877
213653
  */
213878
213654
  enabled: zod.z.boolean().optional(),
213879
213655
  /** COMPUTED at read time by the orchestrator (statfs of the backing volume
@@ -213886,6 +213662,10 @@ var require_dist4 = __commonJS({
213886
213662
  createdAt: zod.z.number(),
213887
213663
  updatedAt: zod.z.number()
213888
213664
  });
213665
+ var LegacyStorageLocationDefaultSchema = zod.z.object({ isDefault: zod.z.boolean().optional() });
213666
+ function isLocationEnabled(location) {
213667
+ return location.enabled !== false;
213668
+ }
213889
213669
  var StorageLocationRefSchema = zod.z.union([StorageLocationTypeSchema, zod.z.string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/)]);
213890
213670
  var StorageAccessSchema = zod.z.enum(["local-path", "cap-mediated"]);
213891
213671
  var STORAGE_ACCESS_FALLBACK = "local-path";
@@ -214183,163 +213963,253 @@ var require_dist4 = __commonJS({
214183
213963
  function findTimezone(id) {
214184
213964
  return TIMEZONES.find((tz) => tz.id === id);
214185
213965
  }
214186
- function pickPreferredRtspEntry(entries2, pref, deviceId, options = {}) {
214187
- if (entries2.length === 0) return null;
214188
- const prefix = `${deviceId}/`;
214189
- const toPicked = (e) => ({
214190
- brokerId: e.brokerId,
214191
- profileId: e.brokerId.startsWith(prefix) ? e.brokerId.slice(prefix.length) : e.brokerId,
214192
- url: e.url,
214193
- mutedUrl: e.mutedUrl,
214194
- enabled: e.enabled,
214195
- ...e.codec !== void 0 ? { codec: e.codec } : {},
214196
- ...e.resolution !== void 0 ? { resolution: e.resolution } : {}
214197
- });
214198
- if (pref !== "auto") {
214199
- const match = entries2.find((e) => e.enabled && e.url.length > 0 && (e.profile === pref || e.brokerId === `${prefix}${pref}`));
214200
- 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;
214201
214024
  }
214202
- const eligible = entries2.filter((e) => e.enabled && e.url.length > 0);
214203
- if (eligible.length === 0) return null;
214204
- const target = options.targetResolution;
214205
- if (target) {
214206
- const withRes = eligible.filter((e) => e.resolution !== void 0);
214207
- if (withRes.length > 0) {
214208
- const picked = pickClosestResolution(withRes, target);
214209
- if (picked) return toPicked(picked);
214210
- }
214025
+ /** Epoch ms this channel disarms itself at. 0 when disarmed. */
214026
+ get armedUntilMs() {
214027
+ return this.on ? this.closesAtMs : 0;
214211
214028
  }
214212
- return toPicked(eligible[0]);
214213
- }
214214
- function pickClosestResolution(entries2, target) {
214215
- const targetPixels = target.width * target.height;
214216
- let bestAbove;
214217
- let bestBelow;
214218
- for (const e of entries2) {
214219
- if (e.resolution === void 0) continue;
214220
- const pixels = e.resolution.width * e.resolution.height;
214221
- if (pixels >= targetPixels) {
214222
- if (!bestAbove || pixels < bestAbove.pixels) bestAbove = {
214223
- entry: e,
214224
- pixels
214225
- };
214226
- } else if (!bestBelow || pixels > bestBelow.pixels) bestBelow = {
214227
- entry: e,
214228
- pixels
214229
- };
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);
214230
214039
  }
214231
- return bestAbove?.entry ?? bestBelow?.entry;
214232
- }
214233
- var UNATTRIBUTED_BUCKET_KEY = "__unattributed__";
214234
- var ROOT_BUCKET_KEY = "__root__";
214235
- function bucketFor(row) {
214236
- if (row.addonId !== null) return {
214237
- key: row.addonId,
214238
- kind: "addon"
214239
- };
214240
- if (row.classification === "root") return {
214241
- key: ROOT_BUCKET_KEY,
214242
- kind: "root"
214243
- };
214244
- return {
214245
- key: UNATTRIBUTED_BUCKET_KEY,
214246
- kind: "unattributed"
214247
- };
214248
- }
214249
- function deci(value) {
214250
- return Math.round(value * 10) / 10;
214251
- }
214252
- function foldSnapshotByFunction(rows, atMs) {
214253
- const acc = /* @__PURE__ */ new Map();
214254
- for (const row of rows) {
214255
- const { key, kind } = bucketFor(row);
214256
- const cur = acc.get(key) ?? {
214257
- kind,
214258
- main: 0,
214259
- gc: 0,
214260
- lifetime: 0,
214261
- memory: 0,
214262
- count: 0,
214263
- splitKnown: true
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
214264
214052
  };
214265
- const known = row.cpuMainPercent !== null && row.cpuGcPercent !== null;
214266
- acc.set(key, {
214267
- kind: cur.kind,
214268
- main: cur.main + (row.cpuMainPercent ?? 0),
214269
- gc: cur.gc + (row.cpuGcPercent ?? 0),
214270
- lifetime: cur.lifetime + row.cpuPercent,
214271
- memory: cur.memory + row.memoryRssBytes,
214272
- count: cur.count + 1,
214273
- splitKnown: cur.splitKnown && known
214274
- });
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);
214275
214060
  }
214276
- return [...acc.entries()].map(([key, a]) => {
214277
- const main = a.splitKnown ? deci(a.main) : null;
214278
- const gc = a.splitKnown ? deci(a.gc) : null;
214279
- const lifetime = deci(a.lifetime);
214280
- return {
214281
- key,
214282
- kind: a.kind,
214283
- point: {
214284
- atMs,
214285
- samples: 1,
214286
- cpuMainPercent: main,
214287
- cpuMainPercentMin: main,
214288
- cpuGcPercent: gc,
214289
- cpuGcPercentMin: gc,
214290
- cpuLifetimePercent: lifetime,
214291
- cpuLifetimePercentMin: lifetime,
214292
- memoryRssBytes: a.memory,
214293
- memoryRssBytesMin: a.memory,
214294
- processCount: a.count,
214295
- processCountMin: a.count
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;
214296
214131
  }
214297
- };
214298
- });
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);
214138
+ }
214139
+ return unknown;
214140
+ }
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;
214299
214173
  }
214300
- function minNullable(a, b) {
214301
- if (a === null || b === null) return null;
214302
- return a < b ? a : b;
214174
+ function declareLogChannel(descriptor) {
214175
+ return getLogChannelRegistry().declare(descriptor);
214303
214176
  }
214304
- function maxNullable(a, b) {
214305
- if (a === null || b === null) return null;
214306
- return a > b ? a : b;
214177
+ function __resetLogChannelRegistryForTests() {
214178
+ instance = null;
214307
214179
  }
214308
- function mergePoints(held, next, atMs) {
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?.();
214309
214193
  return {
214310
- atMs,
214311
- samples: held.samples + next.samples,
214312
- cpuMainPercent: maxNullable(held.cpuMainPercent, next.cpuMainPercent),
214313
- cpuMainPercentMin: minNullable(held.cpuMainPercentMin, next.cpuMainPercentMin),
214314
- cpuGcPercent: maxNullable(held.cpuGcPercent, next.cpuGcPercent),
214315
- cpuGcPercentMin: minNullable(held.cpuGcPercentMin, next.cpuGcPercentMin),
214316
- cpuLifetimePercent: Math.max(held.cpuLifetimePercent, next.cpuLifetimePercent),
214317
- cpuLifetimePercentMin: Math.min(held.cpuLifetimePercentMin, next.cpuLifetimePercentMin),
214318
- memoryRssBytes: Math.max(held.memoryRssBytes, next.memoryRssBytes),
214319
- memoryRssBytesMin: Math.min(held.memoryRssBytesMin, next.memoryRssBytesMin),
214320
- processCount: Math.max(held.processCount, next.processCount),
214321
- processCountMin: Math.min(held.processCountMin, next.processCountMin)
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
+ };
214207
+ },
214208
+ stop: () => {
214209
+ clearInterval(timer);
214210
+ }
214322
214211
  };
214323
214212
  }
214324
- function resolveBucketMs(spanMs, cadenceMs, maxPoints) {
214325
- if (maxPoints <= 0 || cadenceMs <= 0 || spanMs <= 0) return Math.max(cadenceMs, 1);
214326
- const wanted = spanMs / maxPoints;
214327
- if (wanted <= cadenceMs) return cadenceMs;
214328
- return Math.ceil(wanted / cadenceMs) * cadenceMs;
214329
- }
214330
- function reducePoints(points, bucketMs, origin) {
214331
- if (bucketMs <= 0 || points.length === 0) return points;
214332
- const buckets = /* @__PURE__ */ new Map();
214333
- for (const point of points) {
214334
- const start = origin + Math.floor((point.atMs - origin) / bucketMs) * bucketMs;
214335
- const held = buckets.get(start);
214336
- buckets.set(start, held === void 0 ? {
214337
- ...point,
214338
- atMs: start
214339
- } : mergePoints(held, point, start));
214340
- }
214341
- return [...buckets.values()].toSorted((a, b) => a.atMs - b.atMs);
214342
- }
214343
214213
  var MAX_REASONS_PER_KEY = 16;
214344
214214
  var MAX_KEYS = 1024;
214345
214215
  var OVERFLOW_REASON = "other";
@@ -214431,6 +214301,163 @@ var require_dist4 = __commonJS({
214431
214301
  if (sample.attempts <= 0) return null;
214432
214302
  return (sample.attempts - sample.succeeded) / sample.attempts;
214433
214303
  }
214304
+ var UNATTRIBUTED_BUCKET_KEY = "__unattributed__";
214305
+ var ROOT_BUCKET_KEY = "__root__";
214306
+ function bucketFor(row) {
214307
+ if (row.addonId !== null) return {
214308
+ key: row.addonId,
214309
+ kind: "addon"
214310
+ };
214311
+ if (row.classification === "root") return {
214312
+ key: ROOT_BUCKET_KEY,
214313
+ kind: "root"
214314
+ };
214315
+ return {
214316
+ key: UNATTRIBUTED_BUCKET_KEY,
214317
+ kind: "unattributed"
214318
+ };
214319
+ }
214320
+ function deci(value) {
214321
+ return Math.round(value * 10) / 10;
214322
+ }
214323
+ function foldSnapshotByFunction(rows, atMs) {
214324
+ const acc = /* @__PURE__ */ new Map();
214325
+ for (const row of rows) {
214326
+ const { key, kind } = bucketFor(row);
214327
+ const cur = acc.get(key) ?? {
214328
+ kind,
214329
+ main: 0,
214330
+ gc: 0,
214331
+ lifetime: 0,
214332
+ memory: 0,
214333
+ count: 0,
214334
+ splitKnown: true
214335
+ };
214336
+ const known = row.cpuMainPercent !== null && row.cpuGcPercent !== null;
214337
+ acc.set(key, {
214338
+ kind: cur.kind,
214339
+ main: cur.main + (row.cpuMainPercent ?? 0),
214340
+ gc: cur.gc + (row.cpuGcPercent ?? 0),
214341
+ lifetime: cur.lifetime + row.cpuPercent,
214342
+ memory: cur.memory + row.memoryRssBytes,
214343
+ count: cur.count + 1,
214344
+ splitKnown: cur.splitKnown && known
214345
+ });
214346
+ }
214347
+ return [...acc.entries()].map(([key, a]) => {
214348
+ const main = a.splitKnown ? deci(a.main) : null;
214349
+ const gc = a.splitKnown ? deci(a.gc) : null;
214350
+ const lifetime = deci(a.lifetime);
214351
+ return {
214352
+ key,
214353
+ kind: a.kind,
214354
+ point: {
214355
+ atMs,
214356
+ samples: 1,
214357
+ cpuMainPercent: main,
214358
+ cpuMainPercentMin: main,
214359
+ cpuGcPercent: gc,
214360
+ cpuGcPercentMin: gc,
214361
+ cpuLifetimePercent: lifetime,
214362
+ cpuLifetimePercentMin: lifetime,
214363
+ memoryRssBytes: a.memory,
214364
+ memoryRssBytesMin: a.memory,
214365
+ processCount: a.count,
214366
+ processCountMin: a.count
214367
+ }
214368
+ };
214369
+ });
214370
+ }
214371
+ function minNullable(a, b) {
214372
+ if (a === null || b === null) return null;
214373
+ return a < b ? a : b;
214374
+ }
214375
+ function maxNullable(a, b) {
214376
+ if (a === null || b === null) return null;
214377
+ return a > b ? a : b;
214378
+ }
214379
+ function mergePoints(held, next, atMs) {
214380
+ return {
214381
+ atMs,
214382
+ samples: held.samples + next.samples,
214383
+ cpuMainPercent: maxNullable(held.cpuMainPercent, next.cpuMainPercent),
214384
+ cpuMainPercentMin: minNullable(held.cpuMainPercentMin, next.cpuMainPercentMin),
214385
+ cpuGcPercent: maxNullable(held.cpuGcPercent, next.cpuGcPercent),
214386
+ cpuGcPercentMin: minNullable(held.cpuGcPercentMin, next.cpuGcPercentMin),
214387
+ cpuLifetimePercent: Math.max(held.cpuLifetimePercent, next.cpuLifetimePercent),
214388
+ cpuLifetimePercentMin: Math.min(held.cpuLifetimePercentMin, next.cpuLifetimePercentMin),
214389
+ memoryRssBytes: Math.max(held.memoryRssBytes, next.memoryRssBytes),
214390
+ memoryRssBytesMin: Math.min(held.memoryRssBytesMin, next.memoryRssBytesMin),
214391
+ processCount: Math.max(held.processCount, next.processCount),
214392
+ processCountMin: Math.min(held.processCountMin, next.processCountMin)
214393
+ };
214394
+ }
214395
+ function resolveBucketMs(spanMs, cadenceMs, maxPoints) {
214396
+ if (maxPoints <= 0 || cadenceMs <= 0 || spanMs <= 0) return Math.max(cadenceMs, 1);
214397
+ const wanted = spanMs / maxPoints;
214398
+ if (wanted <= cadenceMs) return cadenceMs;
214399
+ return Math.ceil(wanted / cadenceMs) * cadenceMs;
214400
+ }
214401
+ function reducePoints(points, bucketMs, origin) {
214402
+ if (bucketMs <= 0 || points.length === 0) return points;
214403
+ const buckets = /* @__PURE__ */ new Map();
214404
+ for (const point of points) {
214405
+ const start = origin + Math.floor((point.atMs - origin) / bucketMs) * bucketMs;
214406
+ const held = buckets.get(start);
214407
+ buckets.set(start, held === void 0 ? {
214408
+ ...point,
214409
+ atMs: start
214410
+ } : mergePoints(held, point, start));
214411
+ }
214412
+ return [...buckets.values()].toSorted((a, b) => a.atMs - b.atMs);
214413
+ }
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);
214429
+ }
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);
214438
+ }
214439
+ }
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
+ };
214458
+ }
214459
+ return bestAbove?.entry ?? bestBelow?.entry;
214460
+ }
214434
214461
  var FORMAT_KEYS = [
214435
214462
  "onnx",
214436
214463
  "coreml",
@@ -229960,7 +229987,6 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
229960
229987
  }), zod.z.instanceof(Uint8Array)),
229961
229988
  endDownload: require_sleep.method(zod.z.object({ downloadId: zod.z.string() }), zod.z.void(), { kind: "mutation" }),
229962
229989
  listLocations: require_sleep.method(zod.z.object({ type: StorageLocationTypeSchema.optional() }), zod.z.array(StorageLocationSchema).readonly()),
229963
- getDefaultLocation: require_sleep.method(zod.z.object({ type: StorageLocationTypeSchema }), StorageLocationSchema.nullable()),
229964
229990
  listLocationDeclarations: require_sleep.method(zod.z.void(), zod.z.array(StorageLocationDeclarationSchema).readonly()),
229965
229991
  upsertLocation: require_sleep.method(StorageLocationSchema.omit({
229966
229992
  createdAt: true,
@@ -240233,62 +240259,6 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
240233
240259
  "ffmpeg.threadCount": 0,
240234
240260
  "auth.tokenExpiry": "30d"
240235
240261
  };
240236
- var CONTAINER_CHILD_PRIORITY = [
240237
- "media-player",
240238
- "alarm-panel",
240239
- "thermostat",
240240
- "climate",
240241
- "humidifier",
240242
- "water-heater",
240243
- "lock",
240244
- "cover",
240245
- "valve",
240246
- "fan",
240247
- "vacuum",
240248
- "lawn-mower",
240249
- "light",
240250
- "switch",
240251
- "siren",
240252
- "button",
240253
- "control",
240254
- "notifier",
240255
- "script",
240256
- "automation",
240257
- "update",
240258
- "presence",
240259
- "weather",
240260
- "image",
240261
- "sensor"
240262
- ];
240263
- var CONTAINER_CHILD_ROLE_PRIORITY = [
240264
- "contact-sensor",
240265
- "motion-sensor",
240266
- "occupancy-sensor",
240267
- "smoke-sensor",
240268
- "co-sensor",
240269
- "gas-sensor",
240270
- "leak-sensor",
240271
- "vibration-sensor",
240272
- "tamper-sensor",
240273
- "sound-sensor"
240274
- ];
240275
- function roleRank(role) {
240276
- if (role === void 0) return CONTAINER_CHILD_ROLE_PRIORITY.length;
240277
- const i = CONTAINER_CHILD_ROLE_PRIORITY.indexOf(role);
240278
- return i === -1 ? CONTAINER_CHILD_ROLE_PRIORITY.length : i;
240279
- }
240280
- function rank(type) {
240281
- const i = CONTAINER_CHILD_PRIORITY.indexOf(type);
240282
- return i === -1 ? CONTAINER_CHILD_PRIORITY.length : i;
240283
- }
240284
- function resolveContainerPrimaryChild(children, overrideEntityId, containerName) {
240285
- if (overrideEntityId !== void 0 && overrideEntityId !== null) {
240286
- const picked = children.find((c) => c.stableId === overrideEntityId) ?? children.find((c) => c.entityId !== void 0 && c.entityId === overrideEntityId);
240287
- if (picked !== void 0) return picked;
240288
- }
240289
- const namesake = (c) => containerName !== void 0 && containerName.length > 0 && c.name !== void 0 && c.name.toLowerCase() === containerName.toLowerCase() ? 0 : 1;
240290
- return [...children].toSorted((a, b) => roleRank(a.role) - roleRank(b.role) || namesake(a) - namesake(b) || rank(a.type) - rank(b.type))[0] ?? null;
240291
- }
240292
240262
  var AccessoryKind = {
240293
240263
  Siren: require_sleep.DeviceRole.Siren,
240294
240264
  Floodlight: require_sleep.DeviceRole.Floodlight,
@@ -241739,6 +241709,62 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
241739
241709
  function isBatteryPresenceFault(presence) {
241740
241710
  return presence === "unreachable";
241741
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
+ }
241742
241768
  var DECLARED_INTEGRATION_FIXED_KEY = "fixed";
241743
241769
  function declarationOwnerNodeId(localNodeId) {
241744
241770
  const raw = localNodeId ?? "hub";
@@ -249022,12 +249048,6 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
249022
249048
  addonId: null,
249023
249049
  access: "view"
249024
249050
  },
249025
- "storage.getDefaultLocation": {
249026
- capName: "storage",
249027
- capScope: "system",
249028
- addonId: null,
249029
- access: "view"
249030
- },
249031
249051
  "storage.list": {
249032
249052
  capName: "storage",
249033
249053
  capScope: "system",
@@ -253403,7 +253423,6 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
253403
253423
  readChunk: (input) => dispatch("storage", "readChunk", "query", input),
253404
253424
  endDownload: (input) => dispatch("storage", "endDownload", "mutation", input),
253405
253425
  listLocations: (input) => dispatch("storage", "listLocations", "query", input),
253406
- getDefaultLocation: (input) => dispatch("storage", "getDefaultLocation", "query", input),
253407
253426
  listLocationDeclarations: (input) => dispatch("storage", "listLocationDeclarations", "query", input),
253408
253427
  upsertLocation: (input) => dispatch("storage", "upsertLocation", "mutation", input),
253409
253428
  deleteLocation: (input) => dispatch("storage", "deleteLocation", "mutation", input),
@@ -256581,6 +256600,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
256581
256600
  exports.LedgerWalkReportSchema = LedgerWalkReportSchema;
256582
256601
  exports.LedgerWalkSkipCountsSchema = LedgerWalkSkipCountsSchema;
256583
256602
  exports.LedgerWalkSkipReasonSchema = LedgerWalkSkipReasonSchema;
256603
+ exports.LegacyStorageLocationDefaultSchema = LegacyStorageLocationDefaultSchema;
256584
256604
  exports.LinkedDeviceSchema = LinkedDeviceSchema;
256585
256605
  exports.LinkedDevicesModeSchema = LinkedDevicesModeSchema;
256586
256606
  exports.LlmDefaultSchema = LlmDefaultSchema;
@@ -257433,6 +257453,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
257433
257453
  exports.isEvent = require_sleep.isEvent;
257434
257454
  exports.isFirstLevelMacroClass = isFirstLevelMacroClass;
257435
257455
  exports.isIsolatedBuiltin = isIsolatedBuiltin;
257456
+ exports.isLocationEnabled = isLocationEnabled;
257436
257457
  exports.isNode = isNode;
257437
257458
  exports.isObjectInput = isObjectInput;
257438
257459
  exports.isOccupancyRule = isOccupancyRule;