@camstack/addon-pipeline 1.2.370 → 1.2.372

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/audio-analyzer/index.js +3 -3
  2. package/dist/audio-analyzer/index.mjs +3 -3
  3. package/dist/{custom-model-catalog-CVbK0xKc.mjs → custom-model-catalog-C6C2cSwd.mjs} +2 -2
  4. package/dist/{custom-model-catalog-BFosahFu.js → custom-model-catalog-n9v8MUL6.js} +2 -2
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-CbFPOFAD.mjs → dist-8AEiKOFO.mjs} +291 -21
  8. package/dist/{dist-jkwxbcbK.js → dist-DmuX7IFJ.js} +308 -20
  9. package/dist/{model-catalogs-DJmwfLfC.mjs → model-catalogs-BSWAHZJh.mjs} +1 -1
  10. package/dist/{model-catalogs-Ds2hC2Br.js → model-catalogs-DO4aA1qo.js} +1 -1
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/{node-C-2fB9kH.mjs → node-Bd4va7c-.mjs} +1 -1
  14. package/dist/{node-C_1U1uzn.js → node-DqA6nD1z.js} +1 -1
  15. package/dist/pipeline-runner/index.js +3 -3
  16. package/dist/pipeline-runner/index.mjs +3 -3
  17. package/dist/{process-memory-Cfh1BQYg.js → process-memory-2OQ2oxrz.js} +1 -1
  18. package/dist/{process-memory-B3I-NvnX.mjs → process-memory-BvS2Z88w.mjs} +1 -1
  19. package/dist/recorder/index.js +189 -54
  20. package/dist/recorder/index.mjs +189 -54
  21. package/dist/{segment-demux-js-uxBcJy15.js → segment-demux-js-BkGK1rhu.js} +1 -1
  22. package/dist/{segment-demux-js-D3JBJeN6.mjs → segment-demux-js-By9MLn_g.mjs} +1 -1
  23. package/dist/stream-broker/_stub.js +1188 -861
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DwwDKaZJ.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DjoAY42S.mjs} +3 -3
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C3ssSNKh.mjs +26 -0
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-nua-KuDg.mjs +26 -0
  27. package/dist/stream-broker/demux-worker-child.js +1 -1
  28. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  29. package/dist/stream-broker/{hostInit-DVwYY7oo.mjs → hostInit-DPiGNc5u.mjs} +3 -3
  30. package/dist/stream-broker/index.js +3 -3
  31. package/dist/stream-broker/index.mjs +3 -3
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/i18n/en.json +8 -0
  34. package/i18n/it.json +8 -0
  35. package/package.json +6 -1
  36. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Bna0L8uC.mjs +0 -26
  37. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BAvXcDKy.mjs +0 -26
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-jkwxbcbK.js");
7
- const require_node = require("../node-C_1U1uzn.js");
6
+ const require_dist = require("../dist-DmuX7IFJ.js");
7
+ const require_node = require("../node-DqA6nD1z.js");
8
8
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
9
9
  const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
10
10
  const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
@@ -4996,9 +4996,10 @@ function archiveUsageComplete(ledger, locationIds) {
4996
4996
  /**
4997
4997
  * Per-device recording-config persistence for the recorder addon.
4998
4998
  *
4999
- * ONE ROW PER CAMERA in `recorder:recording-configs`, through
5000
- * {@link RowMapStore}. Asking what camera 617 records is a primary-key lookup;
5001
- * saving its config writes that row and touches no other.
4999
+ * A camera's config is its PER-DEVICE SETTINGS (D865), key `recordingConfig`,
5000
+ * through `ctx.settings.*DeviceStore`. Asking what camera 617 records is one
5001
+ * scope read; saving its config writes that key and touches no other camera.
5002
+ * Deleting the camera clears the scope centrally.
5002
5003
  *
5003
5004
  * ## What this replaced, and why
5004
5005
  *
@@ -5019,54 +5020,80 @@ function archiveUsageComplete(ledger, locationIds) {
5019
5020
  * empty fallback (= the whole fleet silently unconfigured).
5020
5021
  */
5021
5022
  /**
5022
- * @durable class=config owner=recorder
5023
- * write="one row per camera, written by `saveDeviceConfig` (the
5024
- * `recording-archive` cap's setDeviceConfig / band authoring) — the only
5025
- * writer"
5026
- * retention="none by age. A row goes only when the operator deletes the camera's
5027
- * configuration; an unconfigured camera has no row and reads as DEFAULT_DEVICE_CONFIG"
5023
+ * READ is forgiving where the wire contract is strict — see the header.
5024
+ * `.strip()` keeps a row written before a key was retired readable.
5025
+ *
5026
+ * The preprocess is the D371→D380 migration, applied on EVERY read so a row
5027
+ * behaves correctly from the first read of the boot, before
5028
+ * {@link migrateLegacyDeviceSignalRows} has persisted anything. Pure and
5029
+ * idempotent — a row with no legacy key passes through by reference.
5028
5030
  */
5029
- var RECORDING_CONFIGS_COLLECTION = "recorder:recording-configs";
5030
- var RECORDING_CONFIGS_SPEC = {
5031
- collection: RECORDING_CONFIGS_COLLECTION,
5032
- schema: require_dist.preprocess((raw) => require_dist.migrateLegacyDeviceSignalConfig(raw).value, require_dist.RecordingConfigSchema.strip()).transform((config) => require_dist.stripRetiredBandPreBufferSec(config).config),
5033
- columns: [
5034
- {
5035
- name: "deviceId",
5036
- type: "INTEGER",
5037
- notNull: true
5038
- },
5039
- {
5040
- name: "enabled",
5041
- type: "BOOLEAN",
5042
- notNull: true
5043
- },
5044
- {
5045
- name: "mode",
5046
- type: "TEXT"
5031
+ var StoredRecordingConfigSchema = require_dist.preprocess((raw) => require_dist.migrateLegacyDeviceSignalConfig(raw).value, require_dist.RecordingConfigSchema.strip()).transform((config) => require_dist.stripRetiredBandPreBufferSec(config).config);
5032
+ /** The per-device settings key a camera's recording config lives under. */
5033
+ var RECORDING_CONFIG_KEY = "recordingConfig";
5034
+ var DeviceSettingsConfigsState = class {
5035
+ deps;
5036
+ constructor(deps) {
5037
+ this.deps = deps;
5038
+ }
5039
+ async declare() {}
5040
+ parse(deviceId, raw) {
5041
+ const parsed = StoredRecordingConfigSchema.safeParse(raw);
5042
+ if (parsed.success) return parsed.data;
5043
+ this.deps.logger.warn("recording config row skipped — it does not parse", {
5044
+ tags: { deviceId },
5045
+ meta: { error: parsed.error.message }
5046
+ });
5047
+ return null;
5048
+ }
5049
+ async read(key) {
5050
+ const deviceId = Number(key);
5051
+ if (!Number.isInteger(deviceId)) return null;
5052
+ const held = (await this.deps.settings.readDeviceStore(deviceId))[RECORDING_CONFIG_KEY];
5053
+ return held === void 0 ? null : this.parse(deviceId, held);
5054
+ }
5055
+ async readAllRaw() {
5056
+ const out = /* @__PURE__ */ new Map();
5057
+ const list = this.deps.settings.listDeviceStoreIds;
5058
+ const fallback = this.deps.listDeviceIds;
5059
+ if (list === void 0 && fallback === void 0) throw new Error("the per-device settings cannot list their devices on this view");
5060
+ const ids = list !== void 0 ? await list.call(this.deps.settings) : fallback !== void 0 ? await fallback() : [];
5061
+ const batch = this.deps.settings.readDeviceStoreBatch;
5062
+ const blobs = batch === void 0 ? new Map(await Promise.all(ids.map(async (id) => [id, await this.deps.settings.readDeviceStore(id)]))) : await batch.call(this.deps.settings, ids);
5063
+ for (const [id, blob] of blobs) {
5064
+ const held = blob[RECORDING_CONFIG_KEY];
5065
+ if (held !== void 0) out.set(String(id), held);
5047
5066
  }
5048
- ],
5049
- project: (key, value) => ({
5050
- deviceId: Number(key),
5051
- enabled: value.enabled,
5052
- mode: value.mode ?? null
5053
- }),
5054
- deviceIdColumn: "deviceId",
5055
- loadLimit: 2e4
5067
+ return out;
5068
+ }
5069
+ async readAll() {
5070
+ const out = /* @__PURE__ */ new Map();
5071
+ for (const [key, raw] of await this.readAllRaw()) {
5072
+ const parsed = this.parse(Number(key), raw);
5073
+ if (parsed !== null) out.set(key, parsed);
5074
+ }
5075
+ return out;
5076
+ }
5077
+ async write(key, value) {
5078
+ const deviceId = Number(key);
5079
+ if (!Number.isInteger(deviceId)) throw new Error(`recording config: bad device id "${key}"`);
5080
+ await this.deps.settings.writeDeviceStore(deviceId, { [RECORDING_CONFIG_KEY]: value });
5081
+ }
5082
+ async remove(key) {
5083
+ const deviceId = Number(key);
5084
+ if (!Number.isInteger(deviceId)) return;
5085
+ await this.deps.settings.writeDeviceStore(deviceId, { [RECORDING_CONFIG_KEY]: void 0 });
5086
+ }
5056
5087
  };
5088
+ /** Build the handle over the standard per-device settings. */
5089
+ function recordingConfigsState(deps) {
5090
+ return new DeviceSettingsConfigsState(deps);
5091
+ }
5057
5092
  /** Cold-start default for a camera that has never been configured: disabled, no bands. */
5058
5093
  var DEFAULT_DEVICE_CONFIG = {
5059
5094
  enabled: false,
5060
5095
  bands: []
5061
5096
  };
5062
- /** Build the handle. `declare()` MUST run once at boot before any read/write. */
5063
- function recordingConfigsState(store, logger) {
5064
- return new require_retire_root_keys.RowMapStore({
5065
- spec: RECORDING_CONFIGS_SPEC,
5066
- store,
5067
- logger
5068
- });
5069
- }
5070
5097
  /** Read every persisted device config, keyed by numeric deviceId. */
5071
5098
  async function readDeviceConfigs(state) {
5072
5099
  const rows = await state.readAll();
@@ -6194,6 +6221,7 @@ Object.freeze({
6194
6221
  "device-export": ["listExposedDevices", "listSupportedDeviceKinds"],
6195
6222
  "device-extension": ["listSlots", "listSources"],
6196
6223
  "device-provider": ["discoverDevices", "getDevices"],
6224
+ "device-removal-hook": ["listDeviceData"],
6197
6225
  "failure-contribution": ["list"],
6198
6226
  "llm": [
6199
6227
  "getDefaults",
@@ -11106,7 +11134,7 @@ function buildRecordingProvider(deps) {
11106
11134
  limit
11107
11135
  }) ?? [],
11108
11136
  renderClip: async (input) => {
11109
- if (!deps.clipRender) throw new Error("clip rendering unavailable on this node");
11137
+ if (!deps.clipRender) throw require_dist.userError("recording.clip-render-unavailable", void 0, "clip rendering unavailable on this node");
11110
11138
  return deps.clipRender(input);
11111
11139
  },
11112
11140
  renderGif: async (input) => {
@@ -20079,7 +20107,7 @@ async function hasFootage(index, deviceId, profile, fromMs, toMs) {
20079
20107
  /** Load a record or throw a clear NOT_FOUND-style error. */
20080
20108
  async function requireRecord(state, exportId) {
20081
20109
  const rec = await getExportRecord(state, exportId);
20082
- if (!rec) throw new Error(`recording-export: unknown export "${exportId}"`);
20110
+ if (!rec) throw require_dist.userError("recording.export-unknown", { id: exportId }, `recording-export: unknown export "${exportId}"`);
20083
20111
  return rec;
20084
20112
  }
20085
20113
  /** Build the `recordingExport` provider over the injected deps. */
@@ -20192,11 +20220,14 @@ function buildRecordingExportProvider(deps) {
20192
20220
  if (subject.kind === "clip") return await createClipExport(subject, input.options);
20193
20221
  const { deviceId, profiles, fromMs, toMs } = subject;
20194
20222
  const { options } = input;
20195
- if (!(fromMs < toMs)) throw new Error(`recording-export: invalid range ${fromMs}..${toMs}`);
20223
+ if (!(fromMs < toMs)) throw require_dist.userError("recording.export-invalid-range", {
20224
+ fromMs,
20225
+ toMs
20226
+ }, `recording-export: invalid range ${fromMs}..${toMs}`);
20196
20227
  for (const profile of profiles) if (!await hasFootage(deps.index, deviceId, profile, fromMs, toMs)) throw new Error(`recording-export: no footage for ${deviceId}/${profile} in ${fromMs}..${toMs}`);
20197
20228
  const now = deps.now();
20198
20229
  const [head, ...rest] = profiles;
20199
- if (head === void 0) throw new Error("recording-export: profiles must contain at least one profile");
20230
+ if (head === void 0) throw require_dist.userError("recording.export-no-profiles", void 0, "recording-export: profiles must contain at least one profile");
20200
20231
  const first = await queueOne({
20201
20232
  deviceId,
20202
20233
  profile: head,
@@ -20247,7 +20278,10 @@ function buildRecordingExportProvider(deps) {
20247
20278
  state: rec.state
20248
20279
  }
20249
20280
  });
20250
- throw new Error(`recording-export: export "${exportId}" is not ready (${rec.state})`);
20281
+ throw require_dist.userError("recording.export-not-ready", {
20282
+ id: exportId,
20283
+ state: rec.state
20284
+ }, `recording-export: export "${exportId}" is not ready (${rec.state})`);
20251
20285
  }
20252
20286
  if (deps.now() >= rec.expiresAt) {
20253
20287
  deps.logger.warn("export byte read refused: export has expired", {
@@ -20257,7 +20291,7 @@ function buildRecordingExportProvider(deps) {
20257
20291
  expiresAt: rec.expiresAt
20258
20292
  }
20259
20293
  });
20260
- throw new Error(`recording-export: export "${exportId}" has expired`);
20294
+ throw require_dist.userError("recording.export-expired", { id: exportId }, `recording-export: export "${exportId}" has expired`);
20261
20295
  }
20262
20296
  const file = deps.outPath(rec);
20263
20297
  let size;
@@ -20271,7 +20305,7 @@ function buildRecordingExportProvider(deps) {
20271
20305
  error: String(err)
20272
20306
  }
20273
20307
  });
20274
- throw new Error(`recording-export: export "${exportId}" has no file on disk`, { cause: err });
20308
+ throw Object.assign(require_dist.userError("recording.export-no-file", { id: exportId }, `recording-export: export "${exportId}" has no file on disk`), { cause: err });
20275
20309
  }
20276
20310
  if (size > 52428800) {
20277
20311
  const shrink = deps.shrinkToBound;
@@ -20352,6 +20386,75 @@ function buildRecordingExportProvider(deps) {
20352
20386
  * the host extracted from `CAMSTACK_HUB_URL`).
20353
20387
  */
20354
20388
  var resolveRecordingHubHostname = require_hub_hostname.resolveHubHostname;
20389
+ async function nextVictims(deps, deviceId) {
20390
+ const archive = deps.archive?.() ?? null;
20391
+ if (archive === null) return null;
20392
+ const victims = [];
20393
+ for await (const page of archive.streamSegments({ deviceId })) {
20394
+ victims.push(...page);
20395
+ if (victims.length >= 2e4) break;
20396
+ }
20397
+ return victims;
20398
+ }
20399
+ /** Delete every segment the archive holds for `deviceId`, across every location of this node. */
20400
+ async function purgeDeviceFootage(deps, deviceId) {
20401
+ let deletedBuckets = 0;
20402
+ let reclaimedBytes = 0;
20403
+ for (let pass = 0; pass < 200; pass++) {
20404
+ const victims = await nextVictims(deps, deviceId);
20405
+ if (victims === null) throw new Error("no durable hour ledger on this node — footage cannot be enumerated");
20406
+ if (victims.length === 0) break;
20407
+ const freed = await evictVictims(deps, deviceId, victims);
20408
+ deletedBuckets += freed.deletedBuckets;
20409
+ reclaimedBytes += freed.reclaimedBytes;
20410
+ if (freed.deletedBuckets === 0) break;
20411
+ }
20412
+ const census = await deps.archive?.()?.census({ deviceId });
20413
+ if (census === null || census === void 0 || census.segments > 0) throw new Error(`footage of device ${deviceId} is not fully removed (${census?.segments ?? "unknown"} segments left)`);
20414
+ if (deletedBuckets > 0) await deps.opsLog?.append({
20415
+ op: "prune",
20416
+ reason: "orphaned-device",
20417
+ deviceId,
20418
+ itemsAffected: deletedBuckets,
20419
+ bytesReclaimed: reclaimedBytes,
20420
+ detail: "the device was deleted — its footage was removed with it"
20421
+ });
20422
+ return {
20423
+ deletedBuckets,
20424
+ reclaimedBytes
20425
+ };
20426
+ }
20427
+ function createRecorderRemovalHook(deps) {
20428
+ const removeDevice = async (input) => {
20429
+ const { deviceId } = input;
20430
+ const footage = await purgeDeviceFootage(deps, deviceId);
20431
+ const counts = { footage: footage.deletedBuckets };
20432
+ if (footage.deletedBuckets > 0) deps.logger.info("recorder: removed the data of a deleted device", {
20433
+ tags: { deviceId },
20434
+ meta: {
20435
+ counts,
20436
+ reclaimedBytes: footage.reclaimedBytes,
20437
+ deletedAt: input.deletedAt
20438
+ }
20439
+ });
20440
+ return { counts };
20441
+ };
20442
+ return {
20443
+ onDeviceRemoved: removeDevice,
20444
+ listDeviceData: async () => {
20445
+ const out = [];
20446
+ const locationIds = new Set(deps.locations().map((l) => l.id));
20447
+ const usage = deps.archiveDeviceUsage?.(locationIds) ?? null;
20448
+ if (usage === null) throw new Error("no durable hour ledger on this node — footage cannot be enumerated");
20449
+ for (const [deviceId, u] of usage) if (u.segments > 0) out.push({
20450
+ store: "footage",
20451
+ deviceId,
20452
+ count: u.segments
20453
+ });
20454
+ return out;
20455
+ }
20456
+ };
20457
+ }
20355
20458
  //#endregion
20356
20459
  //#region src/recorder/addon/recording-source-provider.ts
20357
20460
  /** What the picker calls us. */
@@ -20858,7 +20961,7 @@ var RECORDER_RETIRED_ROOT_KEYS = [
20858
20961
  },
20859
20962
  {
20860
20963
  key: "recordingConfigs",
20861
- successor: RECORDING_CONFIGS_COLLECTION,
20964
+ successor: "the recorder per-device settings (key recordingConfig)",
20862
20965
  reason: "one row per camera since the flatten; as a blob, asking what camera 617 records parsed every camera in the installation"
20863
20966
  }
20864
20967
  ];
@@ -23213,6 +23316,8 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
23213
23316
  * Already ALIAS-GUARDED: a writable same-type duplicate of another
23214
23317
  * location's physical root arrives here demoted to read-only. */
23215
23318
  resolvedLocations = [];
23319
+ /** The `device-removal-hook` provider (D865); its reconcile is armed in `onHubReachable`. */
23320
+ removalHook = null;
23216
23321
  /** Attach-boundary placement (assignment + mount probe + audit). Null on
23217
23322
  * non-recording nodes / before `onInitialize`. */
23218
23323
  placement = null;
@@ -23916,6 +24021,8 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
23916
24021
  migrateLegacyDeviceSignalRows(this.configStore(), this.ctx.logger).catch((err) => {
23917
24022
  this.ctx.logger.warn("recorder: device-signal config migration failed", { meta: { error: require_dist.errMsg(err) } });
23918
24023
  }).then(() => warnLostBands(this.configStore(), this.ctx.logger));
24024
+ const removalHook = createRecorderRemovalHook(recordingDeps);
24025
+ this.removalHook = removalHook;
23919
24026
  const providers = [
23920
24027
  {
23921
24028
  capability: require_dist.recordingArchiveCapability,
@@ -23947,6 +24054,10 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
23947
24054
  {
23948
24055
  capability: require_dist.loadContributionCapability,
23949
24056
  provider: { list: () => this.costRegistry.contributions() }
24057
+ },
24058
+ {
24059
+ capability: require_dist.deviceRemovalHookCapability,
24060
+ provider: removalHook
23950
24061
  }
23951
24062
  ];
23952
24063
  this.footageDiscard = createFootageDiscardJob({
@@ -24078,6 +24189,12 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
24078
24189
  async onHubReachable() {
24079
24190
  if (!this.isRecordingNode) return;
24080
24191
  await this.refreshLocations();
24192
+ const removalHook = this.removalHook;
24193
+ if (removalHook !== null) this.ctx.addDisposer(require_dist.armRemovalReconcile({
24194
+ api: this.ctx.api,
24195
+ logger: this.ctx.logger,
24196
+ remove: (input) => removalHook.onDeviceRemoved(input)
24197
+ }));
24081
24198
  try {
24082
24199
  this.exportsRoot = await this.ctx.api.storage.resolve.query({
24083
24200
  location: ARTIFACTS_MEDIA_LOCATION,
@@ -25059,19 +25176,25 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
25059
25176
  return this.schema({ sections: [{
25060
25177
  id: "cluster-recording",
25061
25178
  title: "Cluster recording",
25179
+ titleKey: "pipeline:globalSettings.s_cluster-recording.title",
25062
25180
  description: "Which node runs recording (continuous + events write path). A SINGLE node — no multi-node balancing; every other node stays fully inert. Default: hub.",
25181
+ descriptionKey: "pipeline:globalSettings.s_cluster-recording.description",
25063
25182
  columns: 1,
25064
25183
  fields: [{
25065
25184
  type: "text",
25066
25185
  key: "recordingNodeId",
25067
25186
  label: "Recording node",
25187
+ labelKey: "pipeline:globalSettings.s_cluster-recording.recordingNodeId.label",
25068
25188
  description: "Node id that records + serves playback (e.g. \"hub\"). Leave \"hub\" unless you intentionally move recording to an agent. Takes effect on addon restart.",
25189
+ descriptionKey: "pipeline:globalSettings.s_cluster-recording.recordingNodeId.description",
25069
25190
  default: RECORDING_NODE_DEFAULT
25070
25191
  }, {
25071
25192
  type: "text",
25072
25193
  key: "recordingHubHostname",
25073
25194
  label: "Hub host (cross-node)",
25195
+ labelKey: "pipeline:globalSettings.s_cluster-recording.recordingHubHostname.label",
25074
25196
  description: "Advanced: only used when the recording node is an AGENT. The hub's host the agent dials to pull broker streams (the RTSP restream port must be reachable there). Leave empty to auto-derive from the agent's hub connection.",
25197
+ descriptionKey: "pipeline:globalSettings.s_cluster-recording.recordingHubHostname.description",
25075
25198
  default: ""
25076
25199
  }]
25077
25200
  }] });
@@ -25145,8 +25268,20 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
25145
25268
  this.censusedSettingsStoreClient ??= censusStoreClient(this.ctx.api.settingsStore, this.readCensus);
25146
25269
  return this.censusedSettingsStoreClient;
25147
25270
  }
25271
+ configsState = null;
25272
+ /** A camera's recording config — its per-device settings (D865), migrated on read. */
25148
25273
  configStore() {
25149
- return recordingConfigsState(this.censusedSettingsStore(), this.ctx.logger);
25274
+ const settings = this.ctx.settings;
25275
+ if (settings === void 0) throw new Error("recorder: device settings are not available");
25276
+ this.configsState ??= recordingConfigsState({
25277
+ settings,
25278
+ logger: this.ctx.logger,
25279
+ listDeviceIds: async () => (await this.ctx.api.deviceManager.listAll.query({
25280
+ isCamera: true,
25281
+ projection: "slim"
25282
+ })).map((device) => device.id)
25283
+ });
25284
+ return this.configsState;
25150
25285
  }
25151
25286
  /** Durable handle over the exportId→ExportRecord blob (same `BaseAddon.state()`
25152
25287
  * wrapper as the config store). */