@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
@@ -1,5 +1,5 @@
1
- import { $t as recordingExportCapability, An as BaseAddon, At as isFirstLevelMacroClass, B as RECORDING_SOURCE_CAMSTACK_ADDON, Bt as modeMayWrite, C as EVENT_PAD_MS, Cn as evaluateSensorEdge, Ct as exportSubjectOf, Dn as DeviceType, En as DeviceFeature, Et as gbToBytes, G as VIDEOCLIPS_MAX_OFFER_BYTES, H as RecordingConfigSchema, It as mayReadLocation, Jn as selectAssignedProfileSlots, K as VISIT_MERGE_GAP_MS, Kn as nodePin, Ln as customAction, Lt as mayWriteToLocation, N as OpsLogEntrySchema, Nt as loadContributionCapability, On as errMsg$1, Qn as boolean, Qt as recordingCapability, R as RECORDING_EXPORT_MAX_READ_BYTES, Rn as defineCustomActions, Rt as memoizePinnedFfmpeg, S as EVENT_OWNER_TYPES, St as evictionPolicyOfLocation, T as ExportRecordSchema, U as RecordingSignalStatusSchema, Vn as hydrateSchema, Xn as _enum, Yn as sleep$1, Zn as array, Zt as recordingArchiveCapability, _n as videoclipsCapability, ar as record, at as clipExportFailureOf, cr as EventCategory, en as recordingSignalCapability, et as batteryCapability, fn as storageEvictableCapability, ft as deriveBatteryPresence, hn as stripRetiredBandPreBufferSec, in as resolveLocationMode, ir as preprocess, it as clipExportFailure, mt as deriveRecordingMode, nn as resolveCreateExportSubject, nr as number, o as BatteryStatusSchema, or as string, pn as storageOccupancyCapability, rr as object, s as CLIP_BROKER_PACED_RATES, sn as resolveRecordingProfiles, v as DEFAULT_EVENTS_BAND_BUFFER_SEC, xt as evictionPolicyForMode, z as RECORDING_SOURCE_CAMSTACK, zt as migrateLegacyDeviceSignalConfig } from "../dist-CbFPOFAD.mjs";
2
- import { a as physicalRootOf, i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-C-2fB9kH.mjs";
1
+ import { $n as _enum, $t as recordingArchiveCapability, An as errMsg$1, B as RECORDING_SOURCE_CAMSTACK_ADDON, Bn as defineCustomActions, Bt as memoizePinnedFfmpeg, C as EVENT_PAD_MS, Ct as evictionPolicyForMode, Ft as loadContributionCapability, G as VIDEOCLIPS_MAX_OFFER_BYTES, H as RecordingConfigSchema, Ht as modeMayWrite, Jn as nodePin, K as VISIT_MERGE_GAP_MS, Mn as BaseAddon, Mt as isFirstLevelMacroClass, N as OpsLogEntrySchema, On as DeviceFeature, Ot as gbToBytes, Qn as userError, R as RECORDING_EXPORT_MAX_READ_BYTES, Rt as mayReadLocation, S as EVENT_OWNER_TYPES, T as ExportRecordSchema, Tn as evaluateSensorEdge, Tt as exportSubjectOf, U as RecordingSignalStatusSchema, Un as hydrateSchema, Vt as migrateLegacyDeviceSignalConfig, Xn as selectAssignedProfileSlots, Z as armRemovalReconcile, Zn as sleep$1, _n as stripRetiredBandPreBufferSec, ar as number, at as clipExportFailure, cr as record, dr as EventCategory, en as recordingCapability, er as array, hn as storageOccupancyCapability, ht as deriveRecordingMode, in as resolveCreateExportSubject, kn as DeviceType, ln as resolveRecordingProfiles, lr as string, mn as storageEvictableCapability, nn as recordingSignalCapability, o as BatteryStatusSchema, on as resolveLocationMode, or as object, ot as clipExportFailureOf, pt as deriveBatteryPresence, s as CLIP_BROKER_PACED_RATES, sr as preprocess, tn as recordingExportCapability, tr as boolean, tt as batteryCapability, v as DEFAULT_EVENTS_BAND_BUFFER_SEC, vt as deviceRemovalHookCapability, wt as evictionPolicyOfLocation, yn as videoclipsCapability, z as RECORDING_SOURCE_CAMSTACK, zn as customAction, zt as mayWriteToLocation } from "../dist-8AEiKOFO.mjs";
2
+ import { a as physicalRootOf, i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-Bd4va7c-.mjs";
3
3
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
4
4
  import { d as PREBUFFER_MAX_MS, l as withRecordingIntent, r as RECORDING_PRE_ROLL_REQUEST_MS } from "../restream-intent-DyGmLAU9.mjs";
5
5
  import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
@@ -4989,9 +4989,10 @@ function archiveUsageComplete(ledger, locationIds) {
4989
4989
  /**
4990
4990
  * Per-device recording-config persistence for the recorder addon.
4991
4991
  *
4992
- * ONE ROW PER CAMERA in `recorder:recording-configs`, through
4993
- * {@link RowMapStore}. Asking what camera 617 records is a primary-key lookup;
4994
- * saving its config writes that row and touches no other.
4992
+ * A camera's config is its PER-DEVICE SETTINGS (D865), key `recordingConfig`,
4993
+ * through `ctx.settings.*DeviceStore`. Asking what camera 617 records is one
4994
+ * scope read; saving its config writes that key and touches no other camera.
4995
+ * Deleting the camera clears the scope centrally.
4995
4996
  *
4996
4997
  * ## What this replaced, and why
4997
4998
  *
@@ -5012,54 +5013,80 @@ function archiveUsageComplete(ledger, locationIds) {
5012
5013
  * empty fallback (= the whole fleet silently unconfigured).
5013
5014
  */
5014
5015
  /**
5015
- * @durable class=config owner=recorder
5016
- * write="one row per camera, written by `saveDeviceConfig` (the
5017
- * `recording-archive` cap's setDeviceConfig / band authoring) — the only
5018
- * writer"
5019
- * retention="none by age. A row goes only when the operator deletes the camera's
5020
- * configuration; an unconfigured camera has no row and reads as DEFAULT_DEVICE_CONFIG"
5016
+ * READ is forgiving where the wire contract is strict — see the header.
5017
+ * `.strip()` keeps a row written before a key was retired readable.
5018
+ *
5019
+ * The preprocess is the D371→D380 migration, applied on EVERY read so a row
5020
+ * behaves correctly from the first read of the boot, before
5021
+ * {@link migrateLegacyDeviceSignalRows} has persisted anything. Pure and
5022
+ * idempotent — a row with no legacy key passes through by reference.
5021
5023
  */
5022
- var RECORDING_CONFIGS_COLLECTION = "recorder:recording-configs";
5023
- var RECORDING_CONFIGS_SPEC = {
5024
- collection: RECORDING_CONFIGS_COLLECTION,
5025
- schema: preprocess((raw) => migrateLegacyDeviceSignalConfig(raw).value, RecordingConfigSchema.strip()).transform((config) => stripRetiredBandPreBufferSec(config).config),
5026
- columns: [
5027
- {
5028
- name: "deviceId",
5029
- type: "INTEGER",
5030
- notNull: true
5031
- },
5032
- {
5033
- name: "enabled",
5034
- type: "BOOLEAN",
5035
- notNull: true
5036
- },
5037
- {
5038
- name: "mode",
5039
- type: "TEXT"
5024
+ var StoredRecordingConfigSchema = preprocess((raw) => migrateLegacyDeviceSignalConfig(raw).value, RecordingConfigSchema.strip()).transform((config) => stripRetiredBandPreBufferSec(config).config);
5025
+ /** The per-device settings key a camera's recording config lives under. */
5026
+ var RECORDING_CONFIG_KEY = "recordingConfig";
5027
+ var DeviceSettingsConfigsState = class {
5028
+ deps;
5029
+ constructor(deps) {
5030
+ this.deps = deps;
5031
+ }
5032
+ async declare() {}
5033
+ parse(deviceId, raw) {
5034
+ const parsed = StoredRecordingConfigSchema.safeParse(raw);
5035
+ if (parsed.success) return parsed.data;
5036
+ this.deps.logger.warn("recording config row skipped — it does not parse", {
5037
+ tags: { deviceId },
5038
+ meta: { error: parsed.error.message }
5039
+ });
5040
+ return null;
5041
+ }
5042
+ async read(key) {
5043
+ const deviceId = Number(key);
5044
+ if (!Number.isInteger(deviceId)) return null;
5045
+ const held = (await this.deps.settings.readDeviceStore(deviceId))[RECORDING_CONFIG_KEY];
5046
+ return held === void 0 ? null : this.parse(deviceId, held);
5047
+ }
5048
+ async readAllRaw() {
5049
+ const out = /* @__PURE__ */ new Map();
5050
+ const list = this.deps.settings.listDeviceStoreIds;
5051
+ const fallback = this.deps.listDeviceIds;
5052
+ if (list === void 0 && fallback === void 0) throw new Error("the per-device settings cannot list their devices on this view");
5053
+ const ids = list !== void 0 ? await list.call(this.deps.settings) : fallback !== void 0 ? await fallback() : [];
5054
+ const batch = this.deps.settings.readDeviceStoreBatch;
5055
+ 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);
5056
+ for (const [id, blob] of blobs) {
5057
+ const held = blob[RECORDING_CONFIG_KEY];
5058
+ if (held !== void 0) out.set(String(id), held);
5040
5059
  }
5041
- ],
5042
- project: (key, value) => ({
5043
- deviceId: Number(key),
5044
- enabled: value.enabled,
5045
- mode: value.mode ?? null
5046
- }),
5047
- deviceIdColumn: "deviceId",
5048
- loadLimit: 2e4
5060
+ return out;
5061
+ }
5062
+ async readAll() {
5063
+ const out = /* @__PURE__ */ new Map();
5064
+ for (const [key, raw] of await this.readAllRaw()) {
5065
+ const parsed = this.parse(Number(key), raw);
5066
+ if (parsed !== null) out.set(key, parsed);
5067
+ }
5068
+ return out;
5069
+ }
5070
+ async write(key, value) {
5071
+ const deviceId = Number(key);
5072
+ if (!Number.isInteger(deviceId)) throw new Error(`recording config: bad device id "${key}"`);
5073
+ await this.deps.settings.writeDeviceStore(deviceId, { [RECORDING_CONFIG_KEY]: value });
5074
+ }
5075
+ async remove(key) {
5076
+ const deviceId = Number(key);
5077
+ if (!Number.isInteger(deviceId)) return;
5078
+ await this.deps.settings.writeDeviceStore(deviceId, { [RECORDING_CONFIG_KEY]: void 0 });
5079
+ }
5049
5080
  };
5081
+ /** Build the handle over the standard per-device settings. */
5082
+ function recordingConfigsState(deps) {
5083
+ return new DeviceSettingsConfigsState(deps);
5084
+ }
5050
5085
  /** Cold-start default for a camera that has never been configured: disabled, no bands. */
5051
5086
  var DEFAULT_DEVICE_CONFIG = {
5052
5087
  enabled: false,
5053
5088
  bands: []
5054
5089
  };
5055
- /** Build the handle. `declare()` MUST run once at boot before any read/write. */
5056
- function recordingConfigsState(store, logger) {
5057
- return new RowMapStore({
5058
- spec: RECORDING_CONFIGS_SPEC,
5059
- store,
5060
- logger
5061
- });
5062
- }
5063
5090
  /** Read every persisted device config, keyed by numeric deviceId. */
5064
5091
  async function readDeviceConfigs(state) {
5065
5092
  const rows = await state.readAll();
@@ -6186,6 +6213,7 @@ Object.freeze({
6186
6213
  "device-export": ["listExposedDevices", "listSupportedDeviceKinds"],
6187
6214
  "device-extension": ["listSlots", "listSources"],
6188
6215
  "device-provider": ["discoverDevices", "getDevices"],
6216
+ "device-removal-hook": ["listDeviceData"],
6189
6217
  "failure-contribution": ["list"],
6190
6218
  "llm": [
6191
6219
  "getDefaults",
@@ -11098,7 +11126,7 @@ function buildRecordingProvider(deps) {
11098
11126
  limit
11099
11127
  }) ?? [],
11100
11128
  renderClip: async (input) => {
11101
- if (!deps.clipRender) throw new Error("clip rendering unavailable on this node");
11129
+ if (!deps.clipRender) throw userError("recording.clip-render-unavailable", void 0, "clip rendering unavailable on this node");
11102
11130
  return deps.clipRender(input);
11103
11131
  },
11104
11132
  renderGif: async (input) => {
@@ -20071,7 +20099,7 @@ async function hasFootage(index, deviceId, profile, fromMs, toMs) {
20071
20099
  /** Load a record or throw a clear NOT_FOUND-style error. */
20072
20100
  async function requireRecord(state, exportId) {
20073
20101
  const rec = await getExportRecord(state, exportId);
20074
- if (!rec) throw new Error(`recording-export: unknown export "${exportId}"`);
20102
+ if (!rec) throw userError("recording.export-unknown", { id: exportId }, `recording-export: unknown export "${exportId}"`);
20075
20103
  return rec;
20076
20104
  }
20077
20105
  /** Build the `recordingExport` provider over the injected deps. */
@@ -20184,11 +20212,14 @@ function buildRecordingExportProvider(deps) {
20184
20212
  if (subject.kind === "clip") return await createClipExport(subject, input.options);
20185
20213
  const { deviceId, profiles, fromMs, toMs } = subject;
20186
20214
  const { options } = input;
20187
- if (!(fromMs < toMs)) throw new Error(`recording-export: invalid range ${fromMs}..${toMs}`);
20215
+ if (!(fromMs < toMs)) throw userError("recording.export-invalid-range", {
20216
+ fromMs,
20217
+ toMs
20218
+ }, `recording-export: invalid range ${fromMs}..${toMs}`);
20188
20219
  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}`);
20189
20220
  const now = deps.now();
20190
20221
  const [head, ...rest] = profiles;
20191
- if (head === void 0) throw new Error("recording-export: profiles must contain at least one profile");
20222
+ if (head === void 0) throw userError("recording.export-no-profiles", void 0, "recording-export: profiles must contain at least one profile");
20192
20223
  const first = await queueOne({
20193
20224
  deviceId,
20194
20225
  profile: head,
@@ -20239,7 +20270,10 @@ function buildRecordingExportProvider(deps) {
20239
20270
  state: rec.state
20240
20271
  }
20241
20272
  });
20242
- throw new Error(`recording-export: export "${exportId}" is not ready (${rec.state})`);
20273
+ throw userError("recording.export-not-ready", {
20274
+ id: exportId,
20275
+ state: rec.state
20276
+ }, `recording-export: export "${exportId}" is not ready (${rec.state})`);
20243
20277
  }
20244
20278
  if (deps.now() >= rec.expiresAt) {
20245
20279
  deps.logger.warn("export byte read refused: export has expired", {
@@ -20249,7 +20283,7 @@ function buildRecordingExportProvider(deps) {
20249
20283
  expiresAt: rec.expiresAt
20250
20284
  }
20251
20285
  });
20252
- throw new Error(`recording-export: export "${exportId}" has expired`);
20286
+ throw userError("recording.export-expired", { id: exportId }, `recording-export: export "${exportId}" has expired`);
20253
20287
  }
20254
20288
  const file = deps.outPath(rec);
20255
20289
  let size;
@@ -20263,7 +20297,7 @@ function buildRecordingExportProvider(deps) {
20263
20297
  error: String(err)
20264
20298
  }
20265
20299
  });
20266
- throw new Error(`recording-export: export "${exportId}" has no file on disk`, { cause: err });
20300
+ throw Object.assign(userError("recording.export-no-file", { id: exportId }, `recording-export: export "${exportId}" has no file on disk`), { cause: err });
20267
20301
  }
20268
20302
  if (size > 52428800) {
20269
20303
  const shrink = deps.shrinkToBound;
@@ -20344,6 +20378,75 @@ function buildRecordingExportProvider(deps) {
20344
20378
  * the host extracted from `CAMSTACK_HUB_URL`).
20345
20379
  */
20346
20380
  var resolveRecordingHubHostname = resolveHubHostname;
20381
+ async function nextVictims(deps, deviceId) {
20382
+ const archive = deps.archive?.() ?? null;
20383
+ if (archive === null) return null;
20384
+ const victims = [];
20385
+ for await (const page of archive.streamSegments({ deviceId })) {
20386
+ victims.push(...page);
20387
+ if (victims.length >= 2e4) break;
20388
+ }
20389
+ return victims;
20390
+ }
20391
+ /** Delete every segment the archive holds for `deviceId`, across every location of this node. */
20392
+ async function purgeDeviceFootage(deps, deviceId) {
20393
+ let deletedBuckets = 0;
20394
+ let reclaimedBytes = 0;
20395
+ for (let pass = 0; pass < 200; pass++) {
20396
+ const victims = await nextVictims(deps, deviceId);
20397
+ if (victims === null) throw new Error("no durable hour ledger on this node — footage cannot be enumerated");
20398
+ if (victims.length === 0) break;
20399
+ const freed = await evictVictims(deps, deviceId, victims);
20400
+ deletedBuckets += freed.deletedBuckets;
20401
+ reclaimedBytes += freed.reclaimedBytes;
20402
+ if (freed.deletedBuckets === 0) break;
20403
+ }
20404
+ const census = await deps.archive?.()?.census({ deviceId });
20405
+ 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)`);
20406
+ if (deletedBuckets > 0) await deps.opsLog?.append({
20407
+ op: "prune",
20408
+ reason: "orphaned-device",
20409
+ deviceId,
20410
+ itemsAffected: deletedBuckets,
20411
+ bytesReclaimed: reclaimedBytes,
20412
+ detail: "the device was deleted — its footage was removed with it"
20413
+ });
20414
+ return {
20415
+ deletedBuckets,
20416
+ reclaimedBytes
20417
+ };
20418
+ }
20419
+ function createRecorderRemovalHook(deps) {
20420
+ const removeDevice = async (input) => {
20421
+ const { deviceId } = input;
20422
+ const footage = await purgeDeviceFootage(deps, deviceId);
20423
+ const counts = { footage: footage.deletedBuckets };
20424
+ if (footage.deletedBuckets > 0) deps.logger.info("recorder: removed the data of a deleted device", {
20425
+ tags: { deviceId },
20426
+ meta: {
20427
+ counts,
20428
+ reclaimedBytes: footage.reclaimedBytes,
20429
+ deletedAt: input.deletedAt
20430
+ }
20431
+ });
20432
+ return { counts };
20433
+ };
20434
+ return {
20435
+ onDeviceRemoved: removeDevice,
20436
+ listDeviceData: async () => {
20437
+ const out = [];
20438
+ const locationIds = new Set(deps.locations().map((l) => l.id));
20439
+ const usage = deps.archiveDeviceUsage?.(locationIds) ?? null;
20440
+ if (usage === null) throw new Error("no durable hour ledger on this node — footage cannot be enumerated");
20441
+ for (const [deviceId, u] of usage) if (u.segments > 0) out.push({
20442
+ store: "footage",
20443
+ deviceId,
20444
+ count: u.segments
20445
+ });
20446
+ return out;
20447
+ }
20448
+ };
20449
+ }
20347
20450
  //#endregion
20348
20451
  //#region src/recorder/addon/recording-source-provider.ts
20349
20452
  /** What the picker calls us. */
@@ -20850,7 +20953,7 @@ var RECORDER_RETIRED_ROOT_KEYS = [
20850
20953
  },
20851
20954
  {
20852
20955
  key: "recordingConfigs",
20853
- successor: RECORDING_CONFIGS_COLLECTION,
20956
+ successor: "the recorder per-device settings (key recordingConfig)",
20854
20957
  reason: "one row per camera since the flatten; as a blob, asking what camera 617 records parsed every camera in the installation"
20855
20958
  }
20856
20959
  ];
@@ -23205,6 +23308,8 @@ var RecorderV2Addon = class extends BaseAddon {
23205
23308
  * Already ALIAS-GUARDED: a writable same-type duplicate of another
23206
23309
  * location's physical root arrives here demoted to read-only. */
23207
23310
  resolvedLocations = [];
23311
+ /** The `device-removal-hook` provider (D865); its reconcile is armed in `onHubReachable`. */
23312
+ removalHook = null;
23208
23313
  /** Attach-boundary placement (assignment + mount probe + audit). Null on
23209
23314
  * non-recording nodes / before `onInitialize`. */
23210
23315
  placement = null;
@@ -23908,6 +24013,8 @@ var RecorderV2Addon = class extends BaseAddon {
23908
24013
  migrateLegacyDeviceSignalRows(this.configStore(), this.ctx.logger).catch((err) => {
23909
24014
  this.ctx.logger.warn("recorder: device-signal config migration failed", { meta: { error: errMsg$1(err) } });
23910
24015
  }).then(() => warnLostBands(this.configStore(), this.ctx.logger));
24016
+ const removalHook = createRecorderRemovalHook(recordingDeps);
24017
+ this.removalHook = removalHook;
23911
24018
  const providers = [
23912
24019
  {
23913
24020
  capability: recordingArchiveCapability,
@@ -23939,6 +24046,10 @@ var RecorderV2Addon = class extends BaseAddon {
23939
24046
  {
23940
24047
  capability: loadContributionCapability,
23941
24048
  provider: { list: () => this.costRegistry.contributions() }
24049
+ },
24050
+ {
24051
+ capability: deviceRemovalHookCapability,
24052
+ provider: removalHook
23942
24053
  }
23943
24054
  ];
23944
24055
  this.footageDiscard = createFootageDiscardJob({
@@ -24070,6 +24181,12 @@ var RecorderV2Addon = class extends BaseAddon {
24070
24181
  async onHubReachable() {
24071
24182
  if (!this.isRecordingNode) return;
24072
24183
  await this.refreshLocations();
24184
+ const removalHook = this.removalHook;
24185
+ if (removalHook !== null) this.ctx.addDisposer(armRemovalReconcile({
24186
+ api: this.ctx.api,
24187
+ logger: this.ctx.logger,
24188
+ remove: (input) => removalHook.onDeviceRemoved(input)
24189
+ }));
24073
24190
  try {
24074
24191
  this.exportsRoot = await this.ctx.api.storage.resolve.query({
24075
24192
  location: ARTIFACTS_MEDIA_LOCATION,
@@ -25051,19 +25168,25 @@ var RecorderV2Addon = class extends BaseAddon {
25051
25168
  return this.schema({ sections: [{
25052
25169
  id: "cluster-recording",
25053
25170
  title: "Cluster recording",
25171
+ titleKey: "pipeline:globalSettings.s_cluster-recording.title",
25054
25172
  description: "Which node runs recording (continuous + events write path). A SINGLE node — no multi-node balancing; every other node stays fully inert. Default: hub.",
25173
+ descriptionKey: "pipeline:globalSettings.s_cluster-recording.description",
25055
25174
  columns: 1,
25056
25175
  fields: [{
25057
25176
  type: "text",
25058
25177
  key: "recordingNodeId",
25059
25178
  label: "Recording node",
25179
+ labelKey: "pipeline:globalSettings.s_cluster-recording.recordingNodeId.label",
25060
25180
  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.",
25181
+ descriptionKey: "pipeline:globalSettings.s_cluster-recording.recordingNodeId.description",
25061
25182
  default: RECORDING_NODE_DEFAULT
25062
25183
  }, {
25063
25184
  type: "text",
25064
25185
  key: "recordingHubHostname",
25065
25186
  label: "Hub host (cross-node)",
25187
+ labelKey: "pipeline:globalSettings.s_cluster-recording.recordingHubHostname.label",
25066
25188
  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.",
25189
+ descriptionKey: "pipeline:globalSettings.s_cluster-recording.recordingHubHostname.description",
25067
25190
  default: ""
25068
25191
  }]
25069
25192
  }] });
@@ -25137,8 +25260,20 @@ var RecorderV2Addon = class extends BaseAddon {
25137
25260
  this.censusedSettingsStoreClient ??= censusStoreClient(this.ctx.api.settingsStore, this.readCensus);
25138
25261
  return this.censusedSettingsStoreClient;
25139
25262
  }
25263
+ configsState = null;
25264
+ /** A camera's recording config — its per-device settings (D865), migrated on read. */
25140
25265
  configStore() {
25141
- return recordingConfigsState(this.censusedSettingsStore(), this.ctx.logger);
25266
+ const settings = this.ctx.settings;
25267
+ if (settings === void 0) throw new Error("recorder: device settings are not available");
25268
+ this.configsState ??= recordingConfigsState({
25269
+ settings,
25270
+ logger: this.ctx.logger,
25271
+ listDeviceIds: async () => (await this.ctx.api.deviceManager.listAll.query({
25272
+ isCamera: true,
25273
+ projection: "slim"
25274
+ })).map((device) => device.id)
25275
+ });
25276
+ return this.configsState;
25142
25277
  }
25143
25278
  /** Durable handle over the exportId→ExportRecord blob (same `BaseAddon.state()`
25144
25279
  * wrapper as the config store). */
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-emK7D4bc.js");
2
- const require_dist = require("./dist-jkwxbcbK.js");
2
+ const require_dist = require("./dist-DmuX7IFJ.js");
3
3
  let node_child_process = require("node:child_process");
4
4
  //#region src/stream-broker/recorded/avcc-annexb.ts
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./chunk-DnnnRqeS.mjs";
2
- import { On as errMsg } from "./dist-CbFPOFAD.mjs";
2
+ import { An as errMsg } from "./dist-8AEiKOFO.mjs";
3
3
  import { spawn } from "node:child_process";
4
4
  //#region src/stream-broker/recorded/avcc-annexb.ts
5
5
  /**