camstack 1.2.82 → 1.2.84

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-C0IXtI_E.js
23637
- var require_dist_C0IXtI_E = __commonJS({
23638
- "../system/dist/dist-C0IXtI_E.js"(exports) {
23636
+ // ../system/dist/dist-TEwQZL3g.js
23637
+ var require_dist_TEwQZL3g = __commonJS({
23638
+ "../system/dist/dist-TEwQZL3g.js"(exports) {
23639
23639
  "use strict";
23640
23640
  var zod = require_zod();
23641
23641
  var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
@@ -28488,6 +28488,68 @@ var require_dist_C0IXtI_E = __commonJS({
28488
28488
  fileCount: zod.z.number(),
28489
28489
  present: zod.z.boolean()
28490
28490
  });
28491
+ var BackupRunStateSchema = zod.z.enum([
28492
+ "queued",
28493
+ "running",
28494
+ "succeeded",
28495
+ "failed",
28496
+ "cancelled"
28497
+ ]);
28498
+ var BackupRunPhaseSchema = zod.z.enum([
28499
+ "queued",
28500
+ "building",
28501
+ "uploading",
28502
+ "done"
28503
+ ]);
28504
+ var BackupRunSchema = zod.z.object({
28505
+ /** Stable run id — the handle `backup.cancel` takes. */
28506
+ id: zod.z.string(),
28507
+ state: BackupRunStateSchema,
28508
+ phase: BackupRunPhaseSchema,
28509
+ /**
28510
+ * Resolved destination location ids. Empty while queued (targets are
28511
+ * resolved when the run starts, against the then-current policies).
28512
+ */
28513
+ destinationIds: zod.z.array(zod.z.string()).readonly(),
28514
+ label: zod.z.string().optional(),
28515
+ /** ms-epoch when the run was submitted (trigger call / schedule fire). */
28516
+ requestedAt: zod.z.number(),
28517
+ /** ms-epoch when the run left the queue and started building. */
28518
+ startedAt: zod.z.number().optional(),
28519
+ /** ms-epoch when the run reached a terminal state. */
28520
+ finishedAt: zod.z.number().optional(),
28521
+ /** Compressed bytes of the staging archive written so far. */
28522
+ stagedBytes: zod.z.number(),
28523
+ /** Final staged archive size, once the build phase completes. */
28524
+ archiveSizeBytes: zod.z.number().optional(),
28525
+ /** Bytes pushed to the destination currently uploading. */
28526
+ uploadedBytes: zod.z.number(),
28527
+ /** Destinations where the archive fully landed (uploaded + indexed). */
28528
+ completedDestinationIds: zod.z.array(zod.z.string()).readonly(),
28529
+ /** Destinations that failed during the fan-out. */
28530
+ failedDestinationIds: zod.z.array(zod.z.string()).readonly(),
28531
+ /** Failure message when `state === 'failed'`. */
28532
+ error: zod.z.string().optional(),
28533
+ /**
28534
+ * 1-based place in the execution queue — 1 = runs next. Present only
28535
+ * while `state === 'queued'`. Stamped by the orchestrator from the
28536
+ * queue's OWN pending order, never derived from timestamps, so the
28537
+ * UI cannot show an order the executor will not honour.
28538
+ */
28539
+ queuePosition: zod.z.number().int().min(1).optional()
28540
+ });
28541
+ var BackupTriggerResultSchema = zod.z.object({
28542
+ /** The run this trigger mapped to — poll it via `listRuns`, stop it via `cancel`. */
28543
+ runId: zod.z.string(),
28544
+ /** True when the run waited behind an in-flight run instead of starting immediately. */
28545
+ queued: zod.z.boolean(),
28546
+ /** True when this trigger was coalesced onto an identical already-queued run. */
28547
+ joined: zod.z.boolean(),
28548
+ /** True when the run was cancelled before completing every destination. */
28549
+ cancelled: zod.z.boolean(),
28550
+ /** One entry per destination the archive landed at (partial on cancel). */
28551
+ entries: zod.z.array(BackupEntrySchema).readonly()
28552
+ });
28491
28553
  var BackupScheduleSchema = zod.z.object({
28492
28554
  /** Stable id. Generated by the orchestrator on first upsert if absent. */
28493
28555
  id: zod.z.string(),
@@ -28524,6 +28586,11 @@ var require_dist_C0IXtI_E = __commonJS({
28524
28586
  * Trigger a backup. Without `destinations` the orchestrator fans
28525
28587
  * out to every destination flagged as enabled in the routing
28526
28588
  * config; with it, only the listed addons receive the archive.
28589
+ *
28590
+ * At most ONE backup run executes at a time — the source tree and
28591
+ * the staging disk are shared by every run, so a second trigger
28592
+ * while one is in flight is enqueued (or joined onto an identical
28593
+ * queued run) and the result says so. See D342.
28527
28594
  */
28528
28595
  trigger: method(zod.z.object({
28529
28596
  /** Subset of registered `backup-destination` addon ids to write to. */
@@ -28537,7 +28604,30 @@ var require_dist_C0IXtI_E = __commonJS({
28537
28604
  * retention (manual runs).
28538
28605
  */
28539
28606
  retentionCount: zod.z.number().int().min(1).max(1e3).optional()
28540
- }).optional(), zod.z.array(BackupEntrySchema).readonly(), {
28607
+ }).optional(), BackupTriggerResultSchema, {
28608
+ kind: "mutation",
28609
+ auth: "admin"
28610
+ }),
28611
+ /**
28612
+ * Every run the orchestrator knows about, in EXECUTION order: the
28613
+ * running run first, then queued runs in the exact order they will
28614
+ * execute (each with `queuePosition`, 1 = next), then the bounded
28615
+ * finished history newest-first. Only one run executes at a time
28616
+ * (D342) — the queued section IS the line. Each run carries live
28617
+ * phase + byte counters so a runaway build is visible in seconds,
28618
+ * not via `du`.
28619
+ */
28620
+ listRuns: method(zod.z.void(), zod.z.array(BackupRunSchema).readonly(), { auth: "admin" }),
28621
+ /**
28622
+ * Stop a backup run. Mirrors `storage-migration.cancel` semantics:
28623
+ * id in, `{ cancelled }` out — `false` when the run is unknown or
28624
+ * already terminal. A QUEUED run is removed before it ever starts;
28625
+ * the RUNNING run has its tar/upload stream actually aborted, the
28626
+ * half-written staging archive is deleted, and the in-flight
28627
+ * destination upload is aborted server-side (partial discarded).
28628
+ * Destinations that already completed keep their archive.
28629
+ */
28630
+ cancel: method(zod.z.object({ runId: zod.z.string() }), zod.z.object({ cancelled: zod.z.boolean() }), {
28541
28631
  kind: "mutation",
28542
28632
  auth: "admin"
28543
28633
  }),
@@ -29277,6 +29367,38 @@ var require_dist_C0IXtI_E = __commonJS({
29277
29367
  auth: "admin"
29278
29368
  }),
29279
29369
  /**
29370
+ * The HARDWARE behind a device number was replaced
29371
+ * (`deviceManager.migrateDevice`). Forget every piece of broker state that
29372
+ * described the old box, so the next catalog pull derives everything from
29373
+ * the camera that is actually there:
29374
+ *
29375
+ * - every `derived:*` stream definition — a derived is authored against a
29376
+ * specific profile layout, and against the wrong hardware its feeder
29377
+ * respawns forever (observed at attempt 2732 on the live hub,
29378
+ * 2026-09-01);
29379
+ * - the profile-slot assignment entry, PURGED (not unassigned — unassign
29380
+ * marks the slot manual, which would pin the stale choice instead of
29381
+ * letting `computeInitialAssignment` re-derive it);
29382
+ * - the probe snapshots (`<deviceId>/…` — probed codec/resolution of the
29383
+ * old hardware);
29384
+ * - the persisted RTSP token rows for the device's brokers (keyed
29385
+ * `<deviceId>/<camStreamId>`; the stream ids change with the hardware,
29386
+ * so the rows are dead URLs).
29387
+ *
29388
+ * An RPC, deliberately — an event is telemetry and may be dropped (D8),
29389
+ * and a dropped forget leaves a feeder respawning against a stream that
29390
+ * does not exist.
29391
+ */
29392
+ forgetDeviceHardware: method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.object({
29393
+ derivedStreamsDeleted: zod.z.array(zod.z.string()).readonly(),
29394
+ assignmentsPurged: zod.z.boolean(),
29395
+ probeSnapshotsDropped: zod.z.number().int().nonnegative(),
29396
+ rtspTokenRowsDeleted: zod.z.number().int().nonnegative()
29397
+ }), {
29398
+ kind: "mutation",
29399
+ auth: "admin"
29400
+ }),
29401
+ /**
29280
29402
  * Render a short GIF or MP4 from the broker's PRE-BUFFER around an instant.
29281
29403
  *
29282
29404
  * The pre-buffer is the only source that already holds the seconds BEFORE
@@ -37159,7 +37281,20 @@ var require_dist_C0IXtI_E = __commonJS({
37159
37281
  projection: TrackProjectionSchema.optional(),
37160
37282
  /** Include stationary-promoted rows (parked objects). Default false: the
37161
37283
  * feed lists passages; parking records live on the stationary registry. */
37162
- includeStationary: zod.z.boolean().optional()
37284
+ includeStationary: zod.z.boolean().optional(),
37285
+ /**
37286
+ * Restrict to these track classes. ABSENT or EMPTY means no filter.
37287
+ *
37288
+ * The same filter `listTracks` takes, because the timeline's class chips must
37289
+ * mean the same thing whether the scope is one camera or twelve. Until this
37290
+ * existed the scoped feed downloaded a page and narrowed it on the phone
37291
+ * while the single-camera path narrowed the read — one filter, two costs.
37292
+ *
37293
+ * A SUPERSET prefilter on `classes[]`, like its single-camera twin: rows
37294
+ * whose class list is unreadable are kept, and the client's rule stays the
37295
+ * exact one.
37296
+ */
37297
+ classes: zod.z.array(zod.z.string()).optional()
37163
37298
  });
37164
37299
  var RecentTracksPageSchema = zod.z.object({
37165
37300
  /** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
@@ -42290,8 +42425,22 @@ var require_dist_C0IXtI_E = __commonJS({
42290
42425
  durability: "session"
42291
42426
  };
42292
42427
  var BatteryStatusSchema = zod.z.object({
42293
- /** 0..100 inclusive. Firmware-reported. */
42294
- percentage: zod.z.number().min(0).max(100),
42428
+ /**
42429
+ * 0..100 inclusive, firmware-reported. **`null` means NOT YET KNOWN** — the
42430
+ * provider has registered the capability but no reading has landed.
42431
+ *
42432
+ * It is nullable because it was not, and the only value a provider could
42433
+ * seed with was `0`. A battery camera behind an NVR therefore announced
42434
+ * itself at 0% on every start and corrected itself a moment later, which is
42435
+ * indistinguishable from a real flat battery: it fires the low-battery alert
42436
+ * every time the hub restarts. Unknown is not empty (D315), and on a battery
42437
+ * reading the difference is an alarm.
42438
+ *
42439
+ * `vacuum-control` and `lawn-mower-control` already model it this way.
42440
+ * Consumers must SKIP a null rather than coerce it — `battery-band` already
42441
+ * declines to band a non-finite reading, which is the correct shape.
42442
+ */
42443
+ percentage: zod.z.number().min(0).max(100).nullable(),
42295
42444
  /**
42296
42445
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
42297
42446
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -50087,7 +50236,7 @@ var require_dist_C0IXtI_E = __commonJS({
50087
50236
  }
50088
50237
  function resolveContainerPrimaryChild(children, overrideEntityId) {
50089
50238
  if (overrideEntityId !== void 0 && overrideEntityId !== null) {
50090
- const picked = children.find((c) => c.entityId === overrideEntityId);
50239
+ const picked = children.find((c) => c.stableId === overrideEntityId) ?? children.find((c) => c.entityId !== void 0 && c.entityId === overrideEntityId);
50091
50240
  if (picked !== void 0) return picked;
50092
50241
  }
50093
50242
  return [...children].toSorted((a, b) => rank(a.type) - rank(b.type))[0] ?? null;
@@ -51105,6 +51254,12 @@ var require_dist_C0IXtI_E = __commonJS({
51105
51254
  addonId: null,
51106
51255
  access: "create"
51107
51256
  },
51257
+ "backup.cancel": {
51258
+ capName: "backup",
51259
+ capScope: "system",
51260
+ addonId: null,
51261
+ access: "create"
51262
+ },
51108
51263
  "backup.delete": {
51109
51264
  capName: "backup",
51110
51265
  capScope: "system",
@@ -51147,6 +51302,12 @@ var require_dist_C0IXtI_E = __commonJS({
51147
51302
  addonId: null,
51148
51303
  access: "view"
51149
51304
  },
51305
+ "backup.listRuns": {
51306
+ capName: "backup",
51307
+ capScope: "system",
51308
+ addonId: null,
51309
+ access: "view"
51310
+ },
51150
51311
  "backup.listSchedules": {
51151
51312
  capName: "backup",
51152
51313
  capScope: "system",
@@ -55713,6 +55874,12 @@ var require_dist_C0IXtI_E = __commonJS({
55713
55874
  addonId: null,
55714
55875
  access: "create"
55715
55876
  },
55877
+ "streamBroker.forgetDeviceHardware": {
55878
+ capName: "stream-broker",
55879
+ capScope: "system",
55880
+ addonId: null,
55881
+ access: "delete"
55882
+ },
55716
55883
  "streamBroker.getAllRtspEntries": {
55717
55884
  capName: "stream-broker",
55718
55885
  capScope: "system",
@@ -58171,6 +58338,11 @@ var require_dist_C0IXtI_E = __commonJS({
58171
58338
  form: "single",
58172
58339
  optional: false
58173
58340
  }],
58341
+ "streamBroker.forgetDeviceHardware": [{
58342
+ name: "deviceId",
58343
+ form: "single",
58344
+ optional: false
58345
+ }],
58174
58346
  "streamBroker.getDeviceAudioMute": [{
58175
58347
  name: "deviceId",
58176
58348
  form: "single",
@@ -59827,7 +59999,7 @@ var require_alerts_addon = __commonJS({
59827
59999
  [Symbol.toStringTag]: { value: "Module" }
59828
60000
  });
59829
60001
  require_chunk_Cek0wNdY();
59830
- var require_dist10 = require_dist_C0IXtI_E();
60002
+ var require_dist10 = require_dist_TEwQZL3g();
59831
60003
  function selectExpired(alerts, cutoffMs) {
59832
60004
  return alerts.filter((a) => a.createdAt < cutoffMs).sort((a, b) => a.createdAt - b.createdAt).map((a) => a.id);
59833
60005
  }
@@ -60646,7 +60818,7 @@ var require_console_logging = __commonJS({
60646
60818
  [Symbol.toStringTag]: { value: "Module" }
60647
60819
  });
60648
60820
  require_chunk_Cek0wNdY();
60649
- var require_dist10 = require_dist_C0IXtI_E();
60821
+ var require_dist10 = require_dist_TEwQZL3g();
60650
60822
  var require_formatter = require_formatter_DqAKDlvN();
60651
60823
  var LEVEL_RANK = {
60652
60824
  debug: 0,
@@ -60740,7 +60912,7 @@ var require_core_blocks_addon = __commonJS({
60740
60912
  "use strict";
60741
60913
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
60742
60914
  var require_chunk = require_chunk_Cek0wNdY();
60743
- var require_dist10 = require_dist_C0IXtI_E();
60915
+ var require_dist10 = require_dist_TEwQZL3g();
60744
60916
  var node_crypto = __require("crypto");
60745
60917
  var node_fs_promises = __require("fs/promises");
60746
60918
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -61637,11 +61809,11 @@ var require_core_blocks = __commonJS({
61637
61809
  }
61638
61810
  });
61639
61811
 
61640
- // ../system/dist/retired-settings-keys-eLv_RCfp.js
61641
- var require_retired_settings_keys_eLv_RCfp = __commonJS({
61642
- "../system/dist/retired-settings-keys-eLv_RCfp.js"(exports) {
61812
+ // ../system/dist/retired-settings-keys-DauPtbFR.js
61813
+ var require_retired_settings_keys_DauPtbFR = __commonJS({
61814
+ "../system/dist/retired-settings-keys-DauPtbFR.js"(exports) {
61643
61815
  "use strict";
61644
- var require_dist10 = require_dist_C0IXtI_E();
61816
+ var require_dist10 = require_dist_TEwQZL3g();
61645
61817
  function settingsStoreIsAuthoritativeHere(env) {
61646
61818
  const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
61647
61819
  return raw === "" || raw === "hub";
@@ -63855,8 +64027,8 @@ var require_device_manager_addon = __commonJS({
63855
64027
  [Symbol.toStringTag]: { value: "Module" }
63856
64028
  });
63857
64029
  require_chunk_Cek0wNdY();
63858
- var require_dist10 = require_dist_C0IXtI_E();
63859
- var require_retired_settings_keys = require_retired_settings_keys_eLv_RCfp();
64030
+ var require_dist10 = require_dist_TEwQZL3g();
64031
+ var require_retired_settings_keys = require_retired_settings_keys_DauPtbFR();
63860
64032
  var node_crypto = __require("crypto");
63861
64033
  var _camstack_types_node = require_node();
63862
64034
  var JOB_HISTORY = 20;
@@ -65498,9 +65670,9 @@ var require_device_manager_addon = __commonJS({
65498
65670
  nextVisited.add(id);
65499
65671
  const primary = require_dist10.resolveContainerPrimaryChild((byParent.get(id) ?? []).map((c) => ({
65500
65672
  id: c.id,
65501
- entityId: String(c.id),
65673
+ stableId: c.stableId,
65502
65674
  type: c.type
65503
- })), void 0);
65675
+ })), byId.get(id)?.primaryChildEntityId);
65504
65676
  if (primary !== null) expandContainer(primary.id, byId, byParent, nextVisited, out);
65505
65677
  }
65506
65678
  function resolveLinkedDeviceIds(params) {
@@ -65519,7 +65691,13 @@ var require_device_manager_addon = __commonJS({
65519
65691
  if (config.mode === "auto") {
65520
65692
  if (selfLocation !== null && selfLocation.length > 0) for (const c of candidates) {
65521
65693
  if (c.type === require_dist10.DeviceType.Container) continue;
65522
- if (c.location === selfLocation) ids.add(c.id);
65694
+ if (c.location !== selfLocation) continue;
65695
+ const parent = c.parentDeviceId !== null ? byId.get(c.parentDeviceId) : void 0;
65696
+ if (parent !== void 0 && parent.type === require_dist10.DeviceType.Container) {
65697
+ expand(parent.id);
65698
+ continue;
65699
+ }
65700
+ ids.add(c.id);
65523
65701
  }
65524
65702
  } else for (const id of config.manualIds) if (byId.has(id)) expand(id);
65525
65703
  ids.delete(selfId);
@@ -66382,6 +66560,16 @@ var require_device_manager_addon = __commonJS({
66382
66560
  const down = [...await applyAll(deps, sourceId, false), ...await applyAll(deps, targetId, false)];
66383
66561
  const sourceStillLive = down.filter((r) => r.deviceId === sourceId && r.outcome === "unreachable").map((r) => r.switchId);
66384
66562
  await deps.swapIds(sourceId, targetId);
66563
+ await deps.swapHardwareState(sourceId, targetId);
66564
+ for (const deviceId of [sourceId, targetId]) try {
66565
+ await deps.forgetStreamState(deviceId);
66566
+ } catch (err) {
66567
+ deps.log("migration: broker hardware state NOT forgotten \u2014 stale stream state survives", {
66568
+ deviceId,
66569
+ error: err instanceof Error ? err.message : String(err),
66570
+ remedy: "delete the device's derived streams and reassign its stream profiles, or update the stream-broker addon and re-run streamBroker.forgetDeviceHardware"
66571
+ });
66572
+ }
66385
66573
  const up = await applyAll(deps, sourceId, true);
66386
66574
  await deps.respawnRunners([sourceId, targetId]);
66387
66575
  deps.log("camera migrated", {
@@ -66398,6 +66586,49 @@ var require_device_manager_addon = __commonJS({
66398
66586
  swapped: true
66399
66587
  };
66400
66588
  }
66589
+ var PROFILE_MAP_CONFIG_KEY = "_profileMap";
66590
+ var ROLE_SCOPED_CONFIG_KEYS = new Set(DEVICE_STORE_SECTIONS.flatMap((section) => [...section.keys]));
66591
+ function splitConfig(config) {
66592
+ const hardware = {};
66593
+ const role = {};
66594
+ for (const [key, value] of Object.entries(config)) {
66595
+ if (key === PROFILE_MAP_CONFIG_KEY) continue;
66596
+ if (ROLE_SCOPED_CONFIG_KEYS.has(key)) role[key] = value;
66597
+ else hardware[key] = value;
66598
+ }
66599
+ return {
66600
+ hardware,
66601
+ role
66602
+ };
66603
+ }
66604
+ async function swapHardwareState(settings, sourceId, targetId) {
66605
+ const sourceConfig = await settings.readDeviceStore(sourceId);
66606
+ const targetConfig = await settings.readDeviceStore(targetId);
66607
+ const source = splitConfig(sourceConfig);
66608
+ const target = splitConfig(targetConfig);
66609
+ await settings.clearDeviceStore(sourceId);
66610
+ await settings.writeDeviceStore(sourceId, {
66611
+ ...target.hardware,
66612
+ ...source.role
66613
+ });
66614
+ await settings.clearDeviceStore(targetId);
66615
+ await settings.writeDeviceStore(targetId, {
66616
+ ...source.hardware,
66617
+ ...target.role
66618
+ });
66619
+ const sourceRuntimeState = await settings.readDeviceRuntimeState(sourceId);
66620
+ const targetRuntimeState = await settings.readDeviceRuntimeState(targetId);
66621
+ await settings.writeDeviceRuntimeState(sourceId, targetRuntimeState);
66622
+ await settings.writeDeviceRuntimeState(targetId, sourceRuntimeState);
66623
+ return {
66624
+ runtimeStateNowAtSource: targetRuntimeState,
66625
+ runtimeStateNowAtTarget: sourceRuntimeState,
66626
+ hardwareKeysToSource: Object.keys(target.hardware).toSorted(),
66627
+ hardwareKeysToTarget: Object.keys(source.hardware).toSorted(),
66628
+ roleKeysKeptAtSource: Object.keys(source.role).toSorted(),
66629
+ roleKeysKeptAtTarget: Object.keys(target.role).toSorted()
66630
+ };
66631
+ }
66401
66632
  async function stampIntegrationId(metaStore, ctx, deviceId, integrationId) {
66402
66633
  await metaStore.withMetaWriteLock(async () => {
66403
66634
  if (!await metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] stampIntegrationId: unknown device id=${deviceId}`);
@@ -66457,13 +66688,42 @@ var require_device_manager_addon = __commonJS({
66457
66688
  swapIds: async (a, b) => {
66458
66689
  await pctx.metaStore.rows.swapIds(a, b, tempId);
66459
66690
  },
66691
+ swapHardwareState: async (a, b) => {
66692
+ const swapped = await swapHardwareState(pctx.settings, a, b);
66693
+ pctx.host.seedMirror(a, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtSource));
66694
+ pctx.host.seedMirror(b, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtTarget));
66695
+ ctx.logger.info("migration: hardware-scoped device state swapped", {
66696
+ tags: { deviceId: a },
66697
+ meta: {
66698
+ sourceId: a,
66699
+ targetId: b,
66700
+ hardwareKeysToSource: swapped.hardwareKeysToSource,
66701
+ hardwareKeysToTarget: swapped.hardwareKeysToTarget,
66702
+ roleKeysKeptAtSource: swapped.roleKeysKeptAtSource,
66703
+ roleKeysKeptAtTarget: swapped.roleKeysKeptAtTarget
66704
+ }
66705
+ });
66706
+ },
66707
+ forgetStreamState: async (deviceId) => {
66708
+ await ctx.api.streamBroker.forgetDeviceHardware.mutate({ deviceId });
66709
+ },
66460
66710
  respawnRunners: async (deviceIds) => {
66461
66711
  const owners = /* @__PURE__ */ new Set();
66462
66712
  for (const deviceId of deviceIds) {
66463
66713
  const addonId = pctx.metaStore.idToAddonId.get(deviceId);
66464
66714
  if (addonId !== void 0) owners.add(addonId);
66465
66715
  }
66466
- for (const addonId of owners) await ctx.api.addons.restartAddon.mutate({ addonId });
66716
+ for (const addonId of owners) {
66717
+ const owned = [...pctx.metaStore.idToAddonId.entries()].filter(([, a]) => a === addonId).length;
66718
+ ctx.logger.info("migration: restarting a whole provider to flush the id maps", {
66719
+ tags: { deviceId: sourceId },
66720
+ meta: {
66721
+ addonId,
66722
+ devicesTakenDown: owned
66723
+ }
66724
+ });
66725
+ await ctx.api.addons.restartAddon.mutate({ addonId });
66726
+ }
66467
66727
  },
66468
66728
  log: (message, meta) => {
66469
66729
  ctx.logger.info(message, {
@@ -68896,7 +69156,7 @@ var require_hub_forwarder = __commonJS({
68896
69156
  [Symbol.toStringTag]: { value: "Module" }
68897
69157
  });
68898
69158
  require_chunk_Cek0wNdY();
68899
- var require_dist10 = require_dist_C0IXtI_E();
69159
+ var require_dist10 = require_dist_TEwQZL3g();
68900
69160
  var require_formatter = require_formatter_DqAKDlvN();
68901
69161
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
68902
69162
  var HubForwarderDestination = class {
@@ -69033,7 +69293,7 @@ var require_liveness_monitor_addon = __commonJS({
69033
69293
  "use strict";
69034
69294
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
69035
69295
  require_chunk_Cek0wNdY();
69036
- var require_dist10 = require_dist_C0IXtI_E();
69296
+ var require_dist10 = require_dist_TEwQZL3g();
69037
69297
  var NO_DEVICES = "liveness:no-devices";
69038
69298
  var ALL_OFFLINE = "liveness:all-devices-offline";
69039
69299
  var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
@@ -69223,7 +69483,7 @@ var require_local_auth_addon = __commonJS({
69223
69483
  [Symbol.toStringTag]: { value: "Module" }
69224
69484
  });
69225
69485
  var require_chunk = require_chunk_Cek0wNdY();
69226
- var require_dist10 = require_dist_C0IXtI_E();
69486
+ var require_dist10 = require_dist_TEwQZL3g();
69227
69487
  var node_crypto = __require("crypto");
69228
69488
  node_crypto = require_chunk.__toESM(node_crypto);
69229
69489
  var crypto$1 = __require("crypto");
@@ -77036,7 +77296,7 @@ var require_loki_logging = __commonJS({
77036
77296
  [Symbol.toStringTag]: { value: "Module" }
77037
77297
  });
77038
77298
  require_chunk_Cek0wNdY();
77039
- var require_dist10 = require_dist_C0IXtI_E();
77299
+ var require_dist10 = require_dist_TEwQZL3g();
77040
77300
  function sanitizeLabelName(raw) {
77041
77301
  const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
77042
77302
  return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
@@ -77601,7 +77861,7 @@ var require_native_metrics_addon = __commonJS({
77601
77861
  [Symbol.toStringTag]: { value: "Module" }
77602
77862
  });
77603
77863
  var require_chunk = require_chunk_Cek0wNdY();
77604
- var require_dist10 = require_dist_C0IXtI_E();
77864
+ var require_dist10 = require_dist_TEwQZL3g();
77605
77865
  var node_fs_promises = __require("fs/promises");
77606
77866
  var node_child_process = __require("child_process");
77607
77867
  var node_util = __require("util");
@@ -80185,7 +80445,7 @@ var require_filesystem_storage_addon = __commonJS({
80185
80445
  [Symbol.toStringTag]: { value: "Module" }
80186
80446
  });
80187
80447
  var require_chunk = require_chunk_Cek0wNdY();
80188
- var require_dist10 = require_dist_C0IXtI_E();
80448
+ var require_dist10 = require_dist_TEwQZL3g();
80189
80449
  var node_crypto = __require("crypto");
80190
80450
  var node_fs_promises = __require("fs/promises");
80191
80451
  var node_path = __require("path");
@@ -81301,8 +81561,8 @@ var require_sqlite_settings_addon = __commonJS({
81301
81561
  [Symbol.toStringTag]: { value: "Module" }
81302
81562
  });
81303
81563
  var require_chunk = require_chunk_Cek0wNdY();
81304
- var require_dist10 = require_dist_C0IXtI_E();
81305
- var require_retired_settings_keys = require_retired_settings_keys_eLv_RCfp();
81564
+ var require_dist10 = require_dist_TEwQZL3g();
81565
+ var require_retired_settings_keys = require_retired_settings_keys_DauPtbFR();
81306
81566
  var node_crypto = __require("crypto");
81307
81567
  var node_fs = __require("fs");
81308
81568
  var node_module = __require("module");
@@ -83792,7 +84052,7 @@ var require_storage_orchestrator_addon = __commonJS({
83792
84052
  [Symbol.toStringTag]: { value: "Module" }
83793
84053
  });
83794
84054
  var require_chunk = require_chunk_Cek0wNdY();
83795
- var require_dist10 = require_dist_C0IXtI_E();
84055
+ var require_dist10 = require_dist_TEwQZL3g();
83796
84056
  var require_hub_cap_forward = require_hub_cap_forward_DmHNjbB0();
83797
84057
  var zod = require_zod();
83798
84058
  var node_crypto = __require("crypto");
@@ -86743,7 +87003,7 @@ var require_system_config_addon = __commonJS({
86743
87003
  [Symbol.toStringTag]: { value: "Module" }
86744
87004
  });
86745
87005
  require_chunk_Cek0wNdY();
86746
- var require_dist10 = require_dist_C0IXtI_E();
87006
+ var require_dist10 = require_dist_TEwQZL3g();
86747
87007
  var SECTION_TITLES = {
86748
87008
  server: "Server",
86749
87009
  auth: "Authentication"
@@ -104804,7 +105064,7 @@ var require_winston_logging = __commonJS({
104804
105064
  [Symbol.toStringTag]: { value: "Module" }
104805
105065
  });
104806
105066
  var require_chunk = require_chunk_Cek0wNdY();
104807
- var require_dist10 = require_dist_C0IXtI_E();
105067
+ var require_dist10 = require_dist_TEwQZL3g();
104808
105068
  var require_formatter = require_formatter_DqAKDlvN();
104809
105069
  var node_path = __require("path");
104810
105070
  node_path = require_chunk.__toESM(node_path);
@@ -106747,9 +107007,9 @@ var require_event_category_BaEgqJNv = __commonJS({
106747
107007
  }
106748
107008
  });
106749
107009
 
106750
- // ../types/dist/sleep-BwGJ_wL_.js
106751
- var require_sleep_BwGJ_wL = __commonJS({
106752
- "../types/dist/sleep-BwGJ_wL_.js"(exports) {
107010
+ // ../types/dist/sleep-BDmIj1HV.js
107011
+ var require_sleep_BDmIj1HV = __commonJS({
107012
+ "../types/dist/sleep-BDmIj1HV.js"(exports) {
106753
107013
  "use strict";
106754
107014
  var require_event_category = require_event_category_BaEgqJNv();
106755
107015
  var zod = require_zod();
@@ -109757,6 +110017,7 @@ var require_sleep_BwGJ_wL = __commonJS({
109757
110017
  retractCameraStream: (input) => dispatchSystem("streamBroker", "retractCameraStream", "mutation", input),
109758
110018
  assignProfile: (input) => dispatchSystem("streamBroker", "assignProfile", "mutation", input),
109759
110019
  unassignProfile: (input) => dispatchSystem("streamBroker", "unassignProfile", "mutation", input),
110020
+ forgetDeviceHardware: (input) => dispatchSystem("streamBroker", "forgetDeviceHardware", "mutation", input),
109760
110021
  renderPreBufferClip: (input) => dispatchSystem("streamBroker", "renderPreBufferClip", "mutation", input),
109761
110022
  produceEventMedia: (input) => dispatchSystem("streamBroker", "produceEventMedia", "mutation", input),
109762
110023
  restartProfile: (input) => dispatchSystem("streamBroker", "restartProfile", "mutation", input),
@@ -110421,7 +110682,7 @@ var require_addon = __commonJS({
110421
110682
  "use strict";
110422
110683
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
110423
110684
  var require_event_category = require_event_category_BaEgqJNv();
110424
- var require_sleep = require_sleep_BwGJ_wL();
110685
+ var require_sleep = require_sleep_BDmIj1HV();
110425
110686
  var require_err_msg = require_err_msg_COpsHMw2();
110426
110687
  var CAP_INPUT_DEFAULTS = Object.freeze({
110427
110688
  "addons": { "getLogs": { "limit": 100 } },
@@ -117304,12 +117565,12 @@ var require_dist2 = __commonJS({
117304
117565
  }
117305
117566
  });
117306
117567
 
117307
- // ../system/dist/manifest-system-deps-D6ncx0jA.js
117308
- var require_manifest_system_deps_D6ncx0jA = __commonJS({
117309
- "../system/dist/manifest-system-deps-D6ncx0jA.js"(exports) {
117568
+ // ../system/dist/manifest-system-deps-BVIr3MYJ.js
117569
+ var require_manifest_system_deps_BVIr3MYJ = __commonJS({
117570
+ "../system/dist/manifest-system-deps-BVIr3MYJ.js"(exports) {
117310
117571
  "use strict";
117311
117572
  var require_chunk = require_chunk_Cek0wNdY();
117312
- require_dist_C0IXtI_E();
117573
+ require_dist_TEwQZL3g();
117313
117574
  require_hub_cap_forward_DmHNjbB0();
117314
117575
  var node_crypto = __require("crypto");
117315
117576
  node_crypto = require_chunk.__toESM(node_crypto);
@@ -129376,7 +129637,7 @@ var require_dist3 = __commonJS({
129376
129637
  "use strict";
129377
129638
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
129378
129639
  var require_chunk = require_chunk_Cek0wNdY();
129379
- var require_dist10 = require_dist_C0IXtI_E();
129640
+ var require_dist10 = require_dist_TEwQZL3g();
129380
129641
  var require_builtins_alerts_alerts_addon = require_alerts_addon();
129381
129642
  require_alerts();
129382
129643
  var require_formatter = require_formatter_DqAKDlvN();
@@ -129403,7 +129664,7 @@ var require_dist3 = __commonJS({
129403
129664
  var require_builtins_winston_logging_index = require_winston_logging();
129404
129665
  var require_file_data_plane = require_file_data_plane_DO8KbxCe();
129405
129666
  var require_tls$1 = require_tls_BxQlomxd();
129406
- var require_manifest_system_deps = require_manifest_system_deps_D6ncx0jA();
129667
+ var require_manifest_system_deps = require_manifest_system_deps_BVIr3MYJ();
129407
129668
  var require_resource_monitor = require_resource_monitor_CdnzxBLP();
129408
129669
  var require_custom_action_registry = require_custom_action_registry_jY0NOZK8();
129409
129670
  var zod = require_zod();
@@ -132441,7 +132702,265 @@ var require_dist3 = __commonJS({
132441
132702
  };
132442
132703
  }
132443
132704
  };
132705
+ function parseVersion(value) {
132706
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(value.trim());
132707
+ if (!m) return null;
132708
+ return {
132709
+ major: Number(m[1]),
132710
+ minor: Number(m[2]),
132711
+ patch: Number(m[3]),
132712
+ prerelease: m[4] ?? null
132713
+ };
132714
+ }
132715
+ function compareTriple(a, b) {
132716
+ if (a.major !== b.major) return a.major < b.major ? -1 : 1;
132717
+ if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1;
132718
+ if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1;
132719
+ return 0;
132720
+ }
132721
+ function closureSatisfiesRange(range, version2) {
132722
+ const trimmedRange = range.trim();
132723
+ const v2 = parseVersion(version2);
132724
+ if (v2 === null) return false;
132725
+ if (v2.prerelease !== null) return trimmedRange === version2.trim() || trimmedRange === `=${version2.trim()}`;
132726
+ const exactBase = parseVersion(trimmedRange.startsWith("=") ? trimmedRange.slice(1) : trimmedRange);
132727
+ if (exactBase !== null) return exactBase.prerelease === null && compareTriple(v2, exactBase) === 0;
132728
+ const operator = trimmedRange[0];
132729
+ if (operator !== "^" && operator !== "~") return false;
132730
+ const base = parseVersion(trimmedRange.slice(1));
132731
+ if (base === null || base.prerelease !== null) return false;
132732
+ if (operator === "~") return v2.major === base.major && v2.minor === base.minor && compareTriple(v2, base) >= 0;
132733
+ if (base.major > 0) return v2.major === base.major && compareTriple(v2, base) >= 0;
132734
+ if (base.minor > 0) return v2.major === 0 && v2.minor === base.minor && compareTriple(v2, base) >= 0;
132735
+ return compareTriple(v2, base) === 0;
132736
+ }
132737
+ function sameCompatLine(a, b) {
132738
+ const pa = parseVersion(a);
132739
+ const pb = parseVersion(b);
132740
+ if (pa === null || pb === null) return false;
132741
+ if (pa.major !== pb.major) return false;
132742
+ if (pa.major === 0) return pa.minor === pb.minor;
132743
+ return true;
132744
+ }
132745
+ function planClosureDedupe(input) {
132746
+ const links = [];
132747
+ const keeps = [];
132748
+ for (const candidate of input.candidates) {
132749
+ if (candidate.name.startsWith("@camstack/")) {
132750
+ keeps.push({
132751
+ name: candidate.name,
132752
+ reason: "camstack-package"
132753
+ });
132754
+ continue;
132755
+ }
132756
+ if (input.neverLink.has(candidate.name)) {
132757
+ keeps.push({
132758
+ name: candidate.name,
132759
+ reason: "never-link"
132760
+ });
132761
+ continue;
132762
+ }
132763
+ const closureVersion = input.closureVersions.get(candidate.name);
132764
+ if (closureVersion === void 0) {
132765
+ keeps.push({
132766
+ name: candidate.name,
132767
+ reason: "not-in-closure"
132768
+ });
132769
+ continue;
132770
+ }
132771
+ if (closureVersion === candidate.version) {
132772
+ links.push({
132773
+ name: candidate.name,
132774
+ closureVersion
132775
+ });
132776
+ continue;
132777
+ }
132778
+ const ranges = input.requirements.get(candidate.name) ?? [];
132779
+ if (ranges.length > 0 && ranges.every((range) => closureSatisfiesRange(range, closureVersion)) && sameCompatLine(closureVersion, candidate.version)) links.push({
132780
+ name: candidate.name,
132781
+ closureVersion
132782
+ });
132783
+ else keeps.push({
132784
+ name: candidate.name,
132785
+ reason: "version-incompatible"
132786
+ });
132787
+ }
132788
+ return {
132789
+ links,
132790
+ keeps
132791
+ };
132792
+ }
132793
+ function readTreeManifest(pkgJsonPath) {
132794
+ let raw;
132795
+ try {
132796
+ raw = require_dist10.asJsonObject(require_dist10.parseJsonUnknown(node_fs.readFileSync(pkgJsonPath, "utf-8")));
132797
+ } catch {
132798
+ return null;
132799
+ }
132800
+ if (raw === null) return null;
132801
+ const ranges = /* @__PURE__ */ new Map();
132802
+ for (const key of [
132803
+ "dependencies",
132804
+ "optionalDependencies",
132805
+ "peerDependencies"
132806
+ ]) {
132807
+ const deps = require_dist10.asJsonObject(raw[key]);
132808
+ if (deps === null) continue;
132809
+ for (const [name2, range] of Object.entries(deps)) if (typeof range === "string" && !ranges.has(name2)) ranges.set(name2, range);
132810
+ }
132811
+ const version2 = require_dist10.asString(raw["version"]);
132812
+ return {
132813
+ version: version2.length > 0 ? version2 : null,
132814
+ ranges
132815
+ };
132816
+ }
132817
+ function listTopLevelEntries(nodeModulesDir) {
132818
+ let entries;
132819
+ try {
132820
+ entries = node_fs.readdirSync(nodeModulesDir, { withFileTypes: true });
132821
+ } catch {
132822
+ return [];
132823
+ }
132824
+ const names = [];
132825
+ for (const entry of entries) {
132826
+ if (entry.name.startsWith(".")) continue;
132827
+ if (entry.name.startsWith("@")) {
132828
+ let inner;
132829
+ try {
132830
+ inner = node_fs.readdirSync(node_path.join(nodeModulesDir, entry.name), { withFileTypes: true });
132831
+ } catch {
132832
+ continue;
132833
+ }
132834
+ for (const sub of inner) if (!sub.name.startsWith(".")) names.push(`${entry.name}/${sub.name}`);
132835
+ } else names.push(entry.name);
132836
+ }
132837
+ return names;
132838
+ }
132839
+ function collectAddonTreeState(addonDir) {
132840
+ const nodeModulesDir = node_path.join(addonDir, "node_modules");
132841
+ const requirements = /* @__PURE__ */ new Map();
132842
+ const addRange = (name2, range) => {
132843
+ const existing = requirements.get(name2);
132844
+ if (existing === void 0) requirements.set(name2, [range]);
132845
+ else existing.push(range);
132846
+ };
132847
+ const rootManifest = readTreeManifest(node_path.join(addonDir, "package.json"));
132848
+ if (rootManifest !== null) for (const [name2, range] of rootManifest.ranges) addRange(name2, range);
132849
+ const candidates = [];
132850
+ const existingLinks = [];
132851
+ for (const name2 of listTopLevelEntries(nodeModulesDir)) {
132852
+ const entryDir = node_path.join(nodeModulesDir, name2);
132853
+ let isLink = false;
132854
+ try {
132855
+ isLink = node_fs.lstatSync(entryDir).isSymbolicLink();
132856
+ } catch {
132857
+ continue;
132858
+ }
132859
+ if (isLink) {
132860
+ let target = "";
132861
+ try {
132862
+ target = node_fs.readlinkSync(entryDir);
132863
+ } catch {
132864
+ }
132865
+ existingLinks.push({
132866
+ name: name2,
132867
+ target
132868
+ });
132869
+ }
132870
+ const manifest = readTreeManifest(node_path.join(entryDir, "package.json"));
132871
+ if (manifest === null) continue;
132872
+ if (!isLink && manifest.version !== null) candidates.push({
132873
+ name: name2,
132874
+ version: manifest.version
132875
+ });
132876
+ for (const [depName, range] of manifest.ranges) {
132877
+ if (node_fs.existsSync(node_path.join(entryDir, "node_modules", depName))) continue;
132878
+ addRange(depName, range);
132879
+ }
132880
+ }
132881
+ return {
132882
+ candidates,
132883
+ requirements,
132884
+ existingLinks
132885
+ };
132886
+ }
132887
+ function readClosureVersions(closureNodeModulesDir, names) {
132888
+ const versions = /* @__PURE__ */ new Map();
132889
+ for (const name2 of names) {
132890
+ const manifest = readTreeManifest(node_path.join(closureNodeModulesDir, name2, "package.json"));
132891
+ if (manifest !== null && manifest.version !== null) versions.set(name2, manifest.version);
132892
+ }
132893
+ return versions;
132894
+ }
132895
+ function resolveClosureNodeModulesDir(explicitDir) {
132896
+ const frameworkDir = explicitDir ?? process.env["CAMSTACK_FRAMEWORK_DIR"];
132897
+ if (frameworkDir === void 0 || frameworkDir.length === 0) return null;
132898
+ const nodeModulesDir = explicitDir ?? node_path.join(frameworkDir, "node_modules");
132899
+ return node_fs.existsSync(nodeModulesDir) ? nodeModulesDir : null;
132900
+ }
132901
+ function symlinkType() {
132902
+ return process.platform === "win32" ? "junction" : "dir";
132903
+ }
132904
+ function executeClosureDedupe(addonDir, closureNodeModulesDir, plan, logger) {
132905
+ if (plan.links.length === 0) return [];
132906
+ const nodeModulesDir = node_path.join(addonDir, "node_modules");
132907
+ const probePath = node_path.join(nodeModulesDir, ".closure-link-probe");
132908
+ try {
132909
+ node_fs.mkdirSync(nodeModulesDir, { recursive: true });
132910
+ node_fs.rmSync(probePath, { force: true });
132911
+ node_fs.symlinkSync(closureNodeModulesDir, probePath, symlinkType());
132912
+ node_fs.rmSync(probePath, { force: true });
132913
+ } catch (err) {
132914
+ logger.warn("closure dedupe skipped \u2014 symlinks unavailable on this volume", { meta: {
132915
+ addonDir,
132916
+ error: require_dist10.errMsg(err)
132917
+ } });
132918
+ return [];
132919
+ }
132920
+ const linked = [];
132921
+ for (const link of plan.links) {
132922
+ const target = node_path.join(closureNodeModulesDir, link.name);
132923
+ const manifest = readTreeManifest(node_path.join(target, "package.json"));
132924
+ if (manifest === null || manifest.version !== link.closureVersion) {
132925
+ logger.warn("closure dedupe \u2014 closure changed under the plan; keeping own copy", { meta: {
132926
+ name: link.name,
132927
+ planned: link.closureVersion,
132928
+ found: manifest?.version ?? null
132929
+ } });
132930
+ continue;
132931
+ }
132932
+ const entryDir = node_path.join(nodeModulesDir, link.name);
132933
+ try {
132934
+ node_fs.rmSync(entryDir, {
132935
+ recursive: true,
132936
+ force: true
132937
+ });
132938
+ node_fs.mkdirSync(node_path.dirname(entryDir), { recursive: true });
132939
+ node_fs.symlinkSync(target, entryDir, symlinkType());
132940
+ linked.push(link.name);
132941
+ } catch (err) {
132942
+ logger.warn("closure dedupe \u2014 link failed; entry removed copy may need reinstall", { meta: {
132943
+ name: link.name,
132944
+ error: require_dist10.errMsg(err)
132945
+ } });
132946
+ }
132947
+ }
132948
+ return linked;
132949
+ }
132950
+ function isClosureLinkTarget(linkTarget, closureNodeModulesDir) {
132951
+ if (linkTarget.length === 0) return false;
132952
+ const normalized = node_path.resolve(linkTarget);
132953
+ const root = node_path.resolve(closureNodeModulesDir);
132954
+ return normalized === root || normalized.startsWith(`${root}${node_path.sep}`);
132955
+ }
132444
132956
  var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
132957
+ function readNativeDependencyNames(pkg) {
132958
+ if (pkg === void 0) return /* @__PURE__ */ new Set();
132959
+ const camstack = require_dist10.asJsonObject(pkg["camstack"]);
132960
+ if (camstack === null) return /* @__PURE__ */ new Set();
132961
+ const native = require_dist10.asJsonObject(camstack["nativeDependencies"]);
132962
+ return native === null ? /* @__PURE__ */ new Set() : new Set(Object.keys(native));
132963
+ }
132445
132964
  function parseInstallSource(value) {
132446
132965
  switch (value) {
132447
132966
  case "npm":
@@ -132503,6 +133022,8 @@ var require_dist3 = __commonJS({
132503
133022
  installSource;
132504
133023
  logger;
132505
133024
  isPackageResolvable;
133025
+ /** Test seam — production resolves via `CAMSTACK_FRAMEWORK_DIR`. */
133026
+ closureNodeModulesDir;
132506
133027
  /**
132507
133028
  * Central manifest of installed addons. Tracks version + source +
132508
133029
  * timestamps + last-backup pointer per package, written atomically
@@ -132524,6 +133045,7 @@ var require_dist3 = __commonJS({
132524
133045
  this.logger = logger;
132525
133046
  this.manifest = new AddonManifest(config.addonsDir);
132526
133047
  this.isPackageResolvable = config.isPackageResolvable;
133048
+ this.closureNodeModulesDir = config.closureNodeModulesDir;
132527
133049
  if (config.installSource) this.installSource = config.installSource;
132528
133050
  else this.installSource = "npm";
132529
133051
  }
@@ -132711,6 +133233,140 @@ var require_dist3 = __commonJS({
132711
133233
  return changes;
132712
133234
  }
132713
133235
  /**
133236
+ * Make every INSTALLED addon's runtime dependencies resolvable — doing the
133237
+ * least work that achieves it. Runs at boot on hub and agent, which is what
133238
+ * turns a restore back into a working node: a system backup archives no
133239
+ * node_modules (D338), so before this pass every restored non-bootstrap
133240
+ * addon failed its first import until someone redeployed it by hand.
133241
+ *
133242
+ * Per addon, in order:
133243
+ * 1. drop closure links that no longer hold (dangling, or the closure
133244
+ * moved to a version the addon tree's recorded ranges refuse) — those
133245
+ * addons get a reinstall;
133246
+ * 2. a declared dep that already resolves in the addon tree costs nothing;
133247
+ * 3. a missing declared dep the closure provides at a COMPATIBLE version
133248
+ * becomes a link — no npm, no network;
133249
+ * 4. only an addon left with a genuinely unresolvable dep gets an npm
133250
+ * install (then dedupe + native ensure, same tail as a fresh install).
133251
+ *
133252
+ * "Only when necessary" is the contract: an addon whose deps are all
133253
+ * satisfied is not touched at all. Failures are per-addon and non-fatal —
133254
+ * a dep that cannot be re-fetched leaves that addon exactly as broken as
133255
+ * it already was, but LOUDLY, and never blocks the rest of the boot.
133256
+ */
133257
+ async ensureAddonRuntimeDeps() {
133258
+ const closureNodeModules = resolveClosureNodeModulesDir(this.closureNodeModulesDir);
133259
+ const linked = [];
133260
+ const reinstalled = [];
133261
+ const failed = [];
133262
+ const installedPackages = this.listInstalled();
133263
+ for (const pkg of installedPackages) {
133264
+ try {
133265
+ if (node_fs.lstatSync(pkg.dir).isSymbolicLink()) continue;
133266
+ } catch {
133267
+ continue;
133268
+ }
133269
+ try {
133270
+ const outcome = this.ensureOneAddonDeps(pkg.dir, pkg.name, closureNodeModules);
133271
+ for (const dep of outcome.linked) linked.push(`${pkg.name}:${dep}`);
133272
+ if (outcome.needsInstall) {
133273
+ await this.reinstallAddonDeps(pkg.dir, pkg.name);
133274
+ reinstalled.push(pkg.name);
133275
+ }
133276
+ } catch (err) {
133277
+ failed.push(pkg.name);
133278
+ this.logger.error(`${pkg.name} \u2014 runtime dependencies could NOT be ensured; the addon will fail to load until its deps install`, { meta: {
133279
+ dir: pkg.dir,
133280
+ error: require_dist10.errMsg(err)
133281
+ } });
133282
+ }
133283
+ }
133284
+ return {
133285
+ checked: installedPackages.length,
133286
+ linked,
133287
+ reinstalled,
133288
+ failed
133289
+ };
133290
+ }
133291
+ /**
133292
+ * One addon's dependency audit (steps 1–3 of `ensureAddonRuntimeDeps`).
133293
+ * Pure decision + link creation; the npm fallback is the caller's.
133294
+ */
133295
+ ensureOneAddonDeps(addonDir, packageName, closureNodeModules) {
133296
+ const pkgView = readPackageJson(node_path.join(addonDir, "package.json"));
133297
+ const declared = pkgView === null ? null : require_dist10.asJsonObject(pkgView.raw["dependencies"]);
133298
+ const declaredEntries = declared === null ? [] : Object.entries(declared);
133299
+ const state = collectAddonTreeState(addonDir);
133300
+ const nodeModulesDir = node_path.join(addonDir, "node_modules");
133301
+ let needsInstall = false;
133302
+ if (closureNodeModules !== null) for (const link of state.existingLinks) {
133303
+ if (!isClosureLinkTarget(link.target, closureNodeModules)) continue;
133304
+ const linkDir = node_path.join(nodeModulesDir, link.name);
133305
+ const targetManifest = node_fs.existsSync(node_path.join(linkDir, "package.json"));
133306
+ const ranges = state.requirements.get(link.name) ?? [];
133307
+ let holds = targetManifest;
133308
+ if (holds && ranges.length > 0) {
133309
+ const version2 = readClosureVersions(closureNodeModules, [link.name]).get(link.name);
133310
+ holds = version2 !== void 0 && ranges.every((range) => closureSatisfiesRange(range, version2));
133311
+ }
133312
+ if (!holds) {
133313
+ this.logger.warn(`${packageName} \u2014 closure link for ${link.name} no longer holds; reinstalling this addon's deps`, { meta: { target: link.target } });
133314
+ node_fs.rmSync(linkDir, { force: true });
133315
+ needsInstall = true;
133316
+ }
133317
+ }
133318
+ const linked = [];
133319
+ for (const [depName, range] of declaredEntries) {
133320
+ if (typeof range !== "string") continue;
133321
+ if (node_fs.existsSync(node_path.join(nodeModulesDir, depName, "package.json"))) continue;
133322
+ const closureVersion = closureNodeModules === null ? void 0 : readClosureVersions(closureNodeModules, [depName]).get(depName);
133323
+ if (closureNodeModules !== null && closureVersion !== void 0 && closureSatisfiesRange(range, closureVersion)) {
133324
+ if (executeClosureDedupe(addonDir, closureNodeModules, {
133325
+ links: [{
133326
+ name: depName,
133327
+ closureVersion
133328
+ }],
133329
+ keeps: []
133330
+ }, this.logger).length === 1) {
133331
+ this.logger.info(`${packageName} \u2014 ${depName} linked from the server closure`, { meta: { closureVersion } });
133332
+ linked.push(depName);
133333
+ continue;
133334
+ }
133335
+ }
133336
+ needsInstall = true;
133337
+ }
133338
+ return {
133339
+ linked,
133340
+ needsInstall
133341
+ };
133342
+ }
133343
+ /**
133344
+ * npm-install an installed addon's runtime deps IN PLACE (the restore /
133345
+ * repair path — the tree is not yet running at boot), then the same tail
133346
+ * every fresh install gets: closure dedupe + native deps + prebuild ensure.
133347
+ * Closure links are stripped first so npm reifies against plain missing
133348
+ * dirs instead of symlinks it does not own.
133349
+ */
133350
+ async reinstallAddonDeps(addonDir, packageName) {
133351
+ const closureNodeModules = resolveClosureNodeModulesDir(this.closureNodeModulesDir);
133352
+ if (closureNodeModules !== null) {
133353
+ const state = collectAddonTreeState(addonDir);
133354
+ for (const link of state.existingLinks) if (isClosureLinkTarget(link.target, closureNodeModules)) node_fs.rmSync(node_path.join(addonDir, "node_modules", link.name), { force: true });
133355
+ }
133356
+ this.logger.info(`${packageName} \u2014 reinstalling runtime dependencies`, { meta: { addonDir } });
133357
+ await require_manifest_system_deps.runNpm([
133358
+ "install",
133359
+ "--omit=dev",
133360
+ "--omit=peer",
133361
+ "--no-audit",
133362
+ "--no-fund",
133363
+ "--no-package-lock",
133364
+ ...this.registry ? ["--registry", this.registry] : []
133365
+ ], this.npmRunOptions(addonDir, 24e4));
133366
+ this.dedupeTreeAgainstClosure(addonDir, packageName);
133367
+ await this.installNativeDependencies(addonDir, packageName);
133368
+ }
133369
+ /**
132714
133370
  * Install a single addon — on-demand installer used by AddonPackageService
132715
133371
  * (admin UI "Install addon" / "Reinstall" buttons).
132716
133372
  *
@@ -132818,6 +133474,7 @@ var require_dist3 = __commonJS({
132818
133474
  logger: this.logger,
132819
133475
  bundledSourceDir: sourceDir
132820
133476
  });
133477
+ this.dedupeTreeAgainstClosure(targetDir, packageName);
132821
133478
  this.logger.info(`${packageName} \u2014 copied from ${sourceDir}`);
132822
133479
  return {
132823
133480
  name: pkgData.name,
@@ -133122,6 +133779,40 @@ var require_dist3 = __commonJS({
133122
133779
  logger: this.logger
133123
133780
  });
133124
133781
  }
133782
+ /**
133783
+ * Stop copying what the server closure already provides: replace each
133784
+ * top-level `node_modules` entry the closure serves at a COMPATIBLE version
133785
+ * with a symlink into the closure (see `deps/closure-dedupe.ts` for the
133786
+ * plan rules and the live measurements). Runs LAST in tree
133787
+ * materialisation — after the runtime-dep npm install AND the native-dep
133788
+ * steps — so no later npm invocation can fight the links.
133789
+ *
133790
+ * Non-fatal by design: dedupe is an optimisation, and its failure mode is
133791
+ * "the addon keeps its own copies", which is exactly today's behaviour. A
133792
+ * refusal on version grounds is logged loudly — that line is the answer to
133793
+ * "why does this addon still carry its own zod".
133794
+ */
133795
+ dedupeTreeAgainstClosure(pkgDir, packageName) {
133796
+ const closureNodeModules = resolveClosureNodeModulesDir(this.closureNodeModulesDir);
133797
+ if (closureNodeModules === null) return;
133798
+ try {
133799
+ const state = collectAddonTreeState(pkgDir);
133800
+ if (state.candidates.length === 0) return;
133801
+ const pkgView = readPackageJson(node_path.join(pkgDir, "package.json"));
133802
+ const plan = planClosureDedupe({
133803
+ candidates: state.candidates,
133804
+ closureVersions: readClosureVersions(closureNodeModules, state.candidates.map((c) => c.name)),
133805
+ requirements: state.requirements,
133806
+ neverLink: readNativeDependencyNames(pkgView?.raw)
133807
+ });
133808
+ const linked = executeClosureDedupe(pkgDir, closureNodeModules, plan, this.logger);
133809
+ if (linked.length > 0) this.logger.info(`${packageName} \u2014 ${linked.length} node_modules entr${linked.length === 1 ? "y" : "ies"} now resolve from the server closure`, { meta: { linked } });
133810
+ const refused = plan.keeps.filter((k2) => k2.reason === "version-incompatible");
133811
+ if (refused.length > 0) this.logger.info(`${packageName} \u2014 kept own copy where the closure version is incompatible`, { meta: { kept: refused.map((k2) => k2.name) } });
133812
+ } catch (err) {
133813
+ this.logger.warn(`${packageName} \u2014 closure dedupe failed (copies kept)`, { meta: { error: require_dist10.errMsg(err) } });
133814
+ }
133815
+ }
133125
133816
  /** Install addon from a tgz file (uploaded or downloaded) */
133126
133817
  async installFromTgz(tgzPath) {
133127
133818
  const stagingRoot = node_path.join(this.addonsDir, ".staging");
@@ -133147,6 +133838,7 @@ var require_dist3 = __commonJS({
133147
133838
  const targetDir = node_path.join(this.addonsDir, pkgView.name);
133148
133839
  await this.installRuntimeDependencies(pkgDir, pkgView.name);
133149
133840
  await this.installNativeDependencies(pkgDir, pkgView.name);
133841
+ this.dedupeTreeAgainstClosure(pkgDir, pkgView.name);
133150
133842
  await this.evictInstallDir(targetDir);
133151
133843
  ensureDir(node_path.dirname(targetDir));
133152
133844
  await node_fs.promises.rename(pkgDir, targetDir);
@@ -133279,6 +133971,7 @@ var require_dist3 = __commonJS({
133279
133971
  if (!node_fs.existsSync(addonDir)) throw new Error(`Cannot update ${packageName}: install dir ${addonDir} missing`);
133280
133972
  await this.installRuntimeDependencies(stagedPath, packageName);
133281
133973
  await this.installNativeDependencies(stagedPath, packageName);
133974
+ this.dedupeTreeAgainstClosure(stagedPath, packageName);
133282
133975
  const ts2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
133283
133976
  const backupRoot = node_path.join(this.addonsDir, ".backups", packageName);
133284
133977
  ensureDir(backupRoot);
@@ -163494,7 +164187,7 @@ globstar while`, t, d, e, u, m), this.matchOne(t.slice(d), e.slice(u), s)) retur
163494
164187
  }));
163495
164188
  var require_symlink_type2 = /* @__PURE__ */ require_chunk.__commonJSMin(((exports2, module2) => {
163496
164189
  var fs2 = require_graceful_fs2();
163497
- function symlinkType(srcpath, type2, callback) {
164190
+ function symlinkType2(srcpath, type2, callback) {
163498
164191
  callback = typeof type2 === "function" ? type2 : callback;
163499
164192
  type2 = typeof type2 === "function" ? false : type2;
163500
164193
  if (type2) return callback(null, type2);
@@ -163515,7 +164208,7 @@ globstar while`, t, d, e, u, m), this.matchOne(t.slice(d), e.slice(u), s)) retur
163515
164208
  return stats && stats.isDirectory() ? "dir" : "file";
163516
164209
  }
163517
164210
  module2.exports = {
163518
- symlinkType,
164211
+ symlinkType: symlinkType2,
163519
164212
  symlinkTypeSync
163520
164213
  };
163521
164214
  }));
@@ -163530,7 +164223,7 @@ globstar while`, t, d, e, u, m), this.matchOne(t.slice(d), e.slice(u), s)) retur
163530
164223
  var symlinkPaths = _symlinkPaths.symlinkPaths;
163531
164224
  var symlinkPathsSync = _symlinkPaths.symlinkPathsSync;
163532
164225
  var _symlinkType = require_symlink_type2();
163533
- var symlinkType = _symlinkType.symlinkType;
164226
+ var symlinkType2 = _symlinkType.symlinkType;
163534
164227
  var symlinkTypeSync = _symlinkType.symlinkTypeSync;
163535
164228
  var pathExists = require_path_exists2().pathExists;
163536
164229
  function createSymlink(srcpath, dstpath, type2, callback) {
@@ -163542,7 +164235,7 @@ globstar while`, t, d, e, u, m), this.matchOne(t.slice(d), e.slice(u), s)) retur
163542
164235
  symlinkPaths(srcpath, dstpath, (err2, relative) => {
163543
164236
  if (err2) return callback(err2);
163544
164237
  srcpath = relative.toDst;
163545
- symlinkType(relative.toCwd, type2, (err3, type3) => {
164238
+ symlinkType2(relative.toCwd, type2, (err3, type3) => {
163546
164239
  if (err3) return callback(err3);
163547
164240
  const dir = path$23.dirname(dstpath);
163548
164241
  pathExists(dir, (err4, dirExists) => {
@@ -210061,7 +210754,7 @@ var require_dist4 = __commonJS({
210061
210754
  "use strict";
210062
210755
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
210063
210756
  var require_event_category = require_event_category_BaEgqJNv();
210064
- var require_sleep = require_sleep_BwGJ_wL();
210757
+ var require_sleep = require_sleep_BDmIj1HV();
210065
210758
  var require_canonical_hash = require_canonical_hash_DNV8S5ET();
210066
210759
  var require_enums2 = require_enums();
210067
210760
  var require_err_msg = require_err_msg_COpsHMw2();
@@ -214671,6 +215364,68 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
214671
215364
  fileCount: zod.z.number(),
214672
215365
  present: zod.z.boolean()
214673
215366
  });
215367
+ var BackupRunStateSchema = zod.z.enum([
215368
+ "queued",
215369
+ "running",
215370
+ "succeeded",
215371
+ "failed",
215372
+ "cancelled"
215373
+ ]);
215374
+ var BackupRunPhaseSchema = zod.z.enum([
215375
+ "queued",
215376
+ "building",
215377
+ "uploading",
215378
+ "done"
215379
+ ]);
215380
+ var BackupRunSchema = zod.z.object({
215381
+ /** Stable run id — the handle `backup.cancel` takes. */
215382
+ id: zod.z.string(),
215383
+ state: BackupRunStateSchema,
215384
+ phase: BackupRunPhaseSchema,
215385
+ /**
215386
+ * Resolved destination location ids. Empty while queued (targets are
215387
+ * resolved when the run starts, against the then-current policies).
215388
+ */
215389
+ destinationIds: zod.z.array(zod.z.string()).readonly(),
215390
+ label: zod.z.string().optional(),
215391
+ /** ms-epoch when the run was submitted (trigger call / schedule fire). */
215392
+ requestedAt: zod.z.number(),
215393
+ /** ms-epoch when the run left the queue and started building. */
215394
+ startedAt: zod.z.number().optional(),
215395
+ /** ms-epoch when the run reached a terminal state. */
215396
+ finishedAt: zod.z.number().optional(),
215397
+ /** Compressed bytes of the staging archive written so far. */
215398
+ stagedBytes: zod.z.number(),
215399
+ /** Final staged archive size, once the build phase completes. */
215400
+ archiveSizeBytes: zod.z.number().optional(),
215401
+ /** Bytes pushed to the destination currently uploading. */
215402
+ uploadedBytes: zod.z.number(),
215403
+ /** Destinations where the archive fully landed (uploaded + indexed). */
215404
+ completedDestinationIds: zod.z.array(zod.z.string()).readonly(),
215405
+ /** Destinations that failed during the fan-out. */
215406
+ failedDestinationIds: zod.z.array(zod.z.string()).readonly(),
215407
+ /** Failure message when `state === 'failed'`. */
215408
+ error: zod.z.string().optional(),
215409
+ /**
215410
+ * 1-based place in the execution queue — 1 = runs next. Present only
215411
+ * while `state === 'queued'`. Stamped by the orchestrator from the
215412
+ * queue's OWN pending order, never derived from timestamps, so the
215413
+ * UI cannot show an order the executor will not honour.
215414
+ */
215415
+ queuePosition: zod.z.number().int().min(1).optional()
215416
+ });
215417
+ var BackupTriggerResultSchema = zod.z.object({
215418
+ /** The run this trigger mapped to — poll it via `listRuns`, stop it via `cancel`. */
215419
+ runId: zod.z.string(),
215420
+ /** True when the run waited behind an in-flight run instead of starting immediately. */
215421
+ queued: zod.z.boolean(),
215422
+ /** True when this trigger was coalesced onto an identical already-queued run. */
215423
+ joined: zod.z.boolean(),
215424
+ /** True when the run was cancelled before completing every destination. */
215425
+ cancelled: zod.z.boolean(),
215426
+ /** One entry per destination the archive landed at (partial on cancel). */
215427
+ entries: zod.z.array(BackupEntrySchema).readonly()
215428
+ });
214674
215429
  var BackupScheduleSchema = zod.z.object({
214675
215430
  /** Stable id. Generated by the orchestrator on first upsert if absent. */
214676
215431
  id: zod.z.string(),
@@ -214707,6 +215462,11 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
214707
215462
  * Trigger a backup. Without `destinations` the orchestrator fans
214708
215463
  * out to every destination flagged as enabled in the routing
214709
215464
  * config; with it, only the listed addons receive the archive.
215465
+ *
215466
+ * At most ONE backup run executes at a time — the source tree and
215467
+ * the staging disk are shared by every run, so a second trigger
215468
+ * while one is in flight is enqueued (or joined onto an identical
215469
+ * queued run) and the result says so. See D342.
214710
215470
  */
214711
215471
  trigger: require_sleep.method(zod.z.object({
214712
215472
  /** Subset of registered `backup-destination` addon ids to write to. */
@@ -214720,7 +215480,30 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
214720
215480
  * retention (manual runs).
214721
215481
  */
214722
215482
  retentionCount: zod.z.number().int().min(1).max(1e3).optional()
214723
- }).optional(), zod.z.array(BackupEntrySchema).readonly(), {
215483
+ }).optional(), BackupTriggerResultSchema, {
215484
+ kind: "mutation",
215485
+ auth: "admin"
215486
+ }),
215487
+ /**
215488
+ * Every run the orchestrator knows about, in EXECUTION order: the
215489
+ * running run first, then queued runs in the exact order they will
215490
+ * execute (each with `queuePosition`, 1 = next), then the bounded
215491
+ * finished history newest-first. Only one run executes at a time
215492
+ * (D342) — the queued section IS the line. Each run carries live
215493
+ * phase + byte counters so a runaway build is visible in seconds,
215494
+ * not via `du`.
215495
+ */
215496
+ listRuns: require_sleep.method(zod.z.void(), zod.z.array(BackupRunSchema).readonly(), { auth: "admin" }),
215497
+ /**
215498
+ * Stop a backup run. Mirrors `storage-migration.cancel` semantics:
215499
+ * id in, `{ cancelled }` out — `false` when the run is unknown or
215500
+ * already terminal. A QUEUED run is removed before it ever starts;
215501
+ * the RUNNING run has its tar/upload stream actually aborted, the
215502
+ * half-written staging archive is deleted, and the in-flight
215503
+ * destination upload is aborted server-side (partial discarded).
215504
+ * Destinations that already completed keep their archive.
215505
+ */
215506
+ cancel: require_sleep.method(zod.z.object({ runId: zod.z.string() }), zod.z.object({ cancelled: zod.z.boolean() }), {
214724
215507
  kind: "mutation",
214725
215508
  auth: "admin"
214726
215509
  }),
@@ -215460,6 +216243,38 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
215460
216243
  auth: "admin"
215461
216244
  }),
215462
216245
  /**
216246
+ * The HARDWARE behind a device number was replaced
216247
+ * (`deviceManager.migrateDevice`). Forget every piece of broker state that
216248
+ * described the old box, so the next catalog pull derives everything from
216249
+ * the camera that is actually there:
216250
+ *
216251
+ * - every `derived:*` stream definition — a derived is authored against a
216252
+ * specific profile layout, and against the wrong hardware its feeder
216253
+ * respawns forever (observed at attempt 2732 on the live hub,
216254
+ * 2026-09-01);
216255
+ * - the profile-slot assignment entry, PURGED (not unassigned — unassign
216256
+ * marks the slot manual, which would pin the stale choice instead of
216257
+ * letting `computeInitialAssignment` re-derive it);
216258
+ * - the probe snapshots (`<deviceId>/…` — probed codec/resolution of the
216259
+ * old hardware);
216260
+ * - the persisted RTSP token rows for the device's brokers (keyed
216261
+ * `<deviceId>/<camStreamId>`; the stream ids change with the hardware,
216262
+ * so the rows are dead URLs).
216263
+ *
216264
+ * An RPC, deliberately — an event is telemetry and may be dropped (D8),
216265
+ * and a dropped forget leaves a feeder respawning against a stream that
216266
+ * does not exist.
216267
+ */
216268
+ forgetDeviceHardware: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.object({
216269
+ derivedStreamsDeleted: zod.z.array(zod.z.string()).readonly(),
216270
+ assignmentsPurged: zod.z.boolean(),
216271
+ probeSnapshotsDropped: zod.z.number().int().nonnegative(),
216272
+ rtspTokenRowsDeleted: zod.z.number().int().nonnegative()
216273
+ }), {
216274
+ kind: "mutation",
216275
+ auth: "admin"
216276
+ }),
216277
+ /**
215463
216278
  * Render a short GIF or MP4 from the broker's PRE-BUFFER around an instant.
215464
216279
  *
215465
216280
  * The pre-buffer is the only source that already holds the seconds BEFORE
@@ -224265,7 +225080,20 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
224265
225080
  projection: TrackProjectionSchema.optional(),
224266
225081
  /** Include stationary-promoted rows (parked objects). Default false: the
224267
225082
  * feed lists passages; parking records live on the stationary registry. */
224268
- includeStationary: zod.z.boolean().optional()
225083
+ includeStationary: zod.z.boolean().optional(),
225084
+ /**
225085
+ * Restrict to these track classes. ABSENT or EMPTY means no filter.
225086
+ *
225087
+ * The same filter `listTracks` takes, because the timeline's class chips must
225088
+ * mean the same thing whether the scope is one camera or twelve. Until this
225089
+ * existed the scoped feed downloaded a page and narrowed it on the phone
225090
+ * while the single-camera path narrowed the read — one filter, two costs.
225091
+ *
225092
+ * A SUPERSET prefilter on `classes[]`, like its single-camera twin: rows
225093
+ * whose class list is unreadable are kept, and the client's rule stays the
225094
+ * exact one.
225095
+ */
225096
+ classes: zod.z.array(zod.z.string()).optional()
224269
225097
  });
224270
225098
  var RecentTracksPageSchema = zod.z.object({
224271
225099
  /** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
@@ -229563,8 +230391,22 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
229563
230391
  durability: "session"
229564
230392
  };
229565
230393
  var BatteryStatusSchema = zod.z.object({
229566
- /** 0..100 inclusive. Firmware-reported. */
229567
- percentage: zod.z.number().min(0).max(100),
230394
+ /**
230395
+ * 0..100 inclusive, firmware-reported. **`null` means NOT YET KNOWN** — the
230396
+ * provider has registered the capability but no reading has landed.
230397
+ *
230398
+ * It is nullable because it was not, and the only value a provider could
230399
+ * seed with was `0`. A battery camera behind an NVR therefore announced
230400
+ * itself at 0% on every start and corrected itself a moment later, which is
230401
+ * indistinguishable from a real flat battery: it fires the low-battery alert
230402
+ * every time the hub restarts. Unknown is not empty (D315), and on a battery
230403
+ * reading the difference is an alarm.
230404
+ *
230405
+ * `vacuum-control` and `lawn-mower-control` already model it this way.
230406
+ * Consumers must SKIP a null rather than coerce it — `battery-band` already
230407
+ * declines to band a non-finite reading, which is the correct shape.
230408
+ */
230409
+ percentage: zod.z.number().min(0).max(100).nullable(),
229568
230410
  /**
229569
230411
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
229570
230412
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -237456,7 +238298,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
237456
238298
  }
237457
238299
  function resolveContainerPrimaryChild(children, overrideEntityId) {
237458
238300
  if (overrideEntityId !== void 0 && overrideEntityId !== null) {
237459
- const picked = children.find((c) => c.entityId === overrideEntityId);
238301
+ const picked = children.find((c) => c.stableId === overrideEntityId) ?? children.find((c) => c.entityId !== void 0 && c.entityId === overrideEntityId);
237460
238302
  if (picked !== void 0) return picked;
237461
238303
  }
237462
238304
  return [...children].toSorted((a, b) => rank(a.type) - rank(b.type))[0] ?? null;
@@ -241322,6 +242164,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
241322
242164
  addonId: null,
241323
242165
  access: "create"
241324
242166
  },
242167
+ "backup.cancel": {
242168
+ capName: "backup",
242169
+ capScope: "system",
242170
+ addonId: null,
242171
+ access: "create"
242172
+ },
241325
242173
  "backup.delete": {
241326
242174
  capName: "backup",
241327
242175
  capScope: "system",
@@ -241364,6 +242212,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
241364
242212
  addonId: null,
241365
242213
  access: "view"
241366
242214
  },
242215
+ "backup.listRuns": {
242216
+ capName: "backup",
242217
+ capScope: "system",
242218
+ addonId: null,
242219
+ access: "view"
242220
+ },
241367
242221
  "backup.listSchedules": {
241368
242222
  capName: "backup",
241369
242223
  capScope: "system",
@@ -245930,6 +246784,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
245930
246784
  addonId: null,
245931
246785
  access: "create"
245932
246786
  },
246787
+ "streamBroker.forgetDeviceHardware": {
246788
+ capName: "stream-broker",
246789
+ capScope: "system",
246790
+ addonId: null,
246791
+ access: "delete"
246792
+ },
245933
246793
  "streamBroker.getAllRtspEntries": {
245934
246794
  capName: "stream-broker",
245935
246795
  capScope: "system",
@@ -248646,6 +249506,11 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
248646
249506
  form: "single",
248647
249507
  optional: false
248648
249508
  }],
249509
+ "streamBroker.forgetDeviceHardware": [{
249510
+ name: "deviceId",
249511
+ form: "single",
249512
+ optional: false
249513
+ }],
248649
249514
  "streamBroker.getDeviceAudioMute": [{
248650
249515
  name: "deviceId",
248651
249516
  form: "single",
@@ -249017,6 +249882,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
249017
249882
  "recordingExport.listExports",
249018
249883
  "streamBroker.acquireEgressTranscode",
249019
249884
  "streamBroker.assignProfile",
249885
+ "streamBroker.forgetDeviceHardware",
249020
249886
  "streamBroker.getDeviceAudioMute",
249021
249887
  "streamBroker.getStreamWithCodec",
249022
249888
  "streamBroker.produceEventMedia",
@@ -249515,6 +250381,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
249515
250381
  backup: {
249516
250382
  listDestinations: (input) => dispatch("backup", "listDestinations", "query", input),
249517
250383
  trigger: (input) => dispatch("backup", "trigger", "mutation", input),
250384
+ listRuns: (input) => dispatch("backup", "listRuns", "query", input),
250385
+ cancel: (input) => dispatch("backup", "cancel", "mutation", input),
249518
250386
  list: (input) => dispatch("backup", "list", "query", input),
249519
250387
  listLocations: (input) => dispatch("backup", "listLocations", "query", input),
249520
250388
  getEntries: (input) => dispatch("backup", "getEntries", "query", input),
@@ -252794,6 +253662,10 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
252794
253662
  exports.BacklightModeSchema = BacklightModeSchema;
252795
253663
  exports.BackupDestinationInfoSchema = BackupDestinationInfoSchema;
252796
253664
  exports.BackupEntrySchema = BackupEntrySchema;
253665
+ exports.BackupRunPhaseSchema = BackupRunPhaseSchema;
253666
+ exports.BackupRunSchema = BackupRunSchema;
253667
+ exports.BackupRunStateSchema = BackupRunStateSchema;
253668
+ exports.BackupTriggerResultSchema = BackupTriggerResultSchema;
252797
253669
  exports.BaseAddon = require_sleep.BaseAddon;
252798
253670
  exports.BaseDevice = BaseDevice;
252799
253671
  exports.BaseDeviceProvider = BaseDeviceProvider;
@@ -387765,6 +388637,16 @@ var require_main2 = __commonJS({
387765
388637
  installSource: resolvedSource
387766
388638
  });
387767
388639
  await installer.ensureRequiredPackages(system_1.AddonInstaller.AGENT_PACKAGES);
388640
+ if (typeof installer.ensureAddonRuntimeDeps === "function") {
388641
+ try {
388642
+ const depsResult = await installer.ensureAddonRuntimeDeps();
388643
+ if (depsResult.linked.length > 0 || depsResult.reinstalled.length > 0 || depsResult.failed.length > 0) {
388644
+ console.log(`[Agent] Addon runtime deps ensured \u2014 linked: [${depsResult.linked.join(", ")}], reinstalled: [${depsResult.reinstalled.join(", ")}], failed: [${depsResult.failed.join(", ")}] (${depsResult.checked} checked)`);
388645
+ }
388646
+ } catch (err) {
388647
+ console.warn(`[Agent] ensureAddonRuntimeDeps failed: ${err instanceof Error ? err.message : String(err)}`);
388648
+ }
388649
+ }
387768
388650
  console.log(`[Agent] Addon packages ready in ${config.addonsDir}`);
387769
388651
  let broker = (0, system_1.createBroker)({
387770
388652
  nodeID: config.nodeId,
@@ -427620,6 +428502,18 @@ var require_launcher = __commonJS({
427620
428502
  console.warn(`[launcher] first-boot addon plan failed: ${err instanceof Error ? err.message : String(err)}`);
427621
428503
  }
427622
428504
  await installer.ensureRequiredPackages(bootstrapRequired);
428505
+ if (typeof installer.ensureAddonRuntimeDeps === "function") {
428506
+ try {
428507
+ const depsResult = await installer.ensureAddonRuntimeDeps();
428508
+ if (depsResult.linked.length > 0 || depsResult.reinstalled.length > 0 || depsResult.failed.length > 0) {
428509
+ console.log(`[launcher] Addon runtime deps ensured \u2014 linked: [${depsResult.linked.join(", ")}], reinstalled: [${depsResult.reinstalled.join(", ")}], failed: [${depsResult.failed.join(", ")}] (${depsResult.checked} checked)`);
428510
+ }
428511
+ } catch (err) {
428512
+ console.warn(`[launcher] ensureAddonRuntimeDeps failed: ${err instanceof Error ? err.message : String(err)}`);
428513
+ }
428514
+ } else {
428515
+ console.warn("[launcher] ensureAddonRuntimeDeps unavailable \u2014 skipping addon dep repair");
428516
+ }
427623
428517
  if (typeof installer.reconcileManifest === "function") {
427624
428518
  const reconciled = installer.reconcileManifest();
427625
428519
  if (reconciled > 0) {