@camstack/system 1.2.304 → 1.2.306

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 (72) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/addon-utils.d.ts +1 -1
  4. package/dist/addon-utils.js +3 -1
  5. package/dist/addon-utils.mjs +2 -2
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  9. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  10. package/dist/builtins/alerts/alerts.addon.js +1 -1
  11. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  12. package/dist/builtins/autotrack/index.js +1 -1
  13. package/dist/builtins/autotrack/index.mjs +1 -1
  14. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  15. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  16. package/dist/builtins/camera-grid/index.js +1 -1
  17. package/dist/builtins/camera-grid/index.mjs +1 -1
  18. package/dist/builtins/console-logging/index.js +1 -1
  19. package/dist/builtins/console-logging/index.mjs +1 -1
  20. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  21. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  22. package/dist/builtins/device-manager/device-extension-linked-slot.d.ts +27 -0
  23. package/dist/builtins/device-manager/device-linked-devices.d.ts +55 -13
  24. package/dist/builtins/device-manager/device-manager.addon.js +324 -136
  25. package/dist/builtins/device-manager/device-manager.addon.mjs +324 -136
  26. package/dist/builtins/doorbell/binding-mirror.d.ts +37 -7
  27. package/dist/builtins/doorbell/doorbell-extension-slot.d.ts +66 -0
  28. package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +17 -36
  29. package/dist/builtins/doorbell/virtual-doorbell.addon.js +320 -124
  30. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +320 -124
  31. package/dist/builtins/hub-forwarder/index.js +1 -1
  32. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  33. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  34. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  35. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  36. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  37. package/dist/builtins/local-network/local-network.addon.js +1 -1
  38. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  39. package/dist/builtins/loki-logging/index.js +1 -1
  40. package/dist/builtins/loki-logging/index.mjs +1 -1
  41. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  42. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  43. package/dist/builtins/platform-probe/index.js +1 -1
  44. package/dist/builtins/platform-probe/index.mjs +1 -1
  45. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  46. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  47. package/dist/builtins/snapshot/index.js +1 -1
  48. package/dist/builtins/snapshot/index.mjs +1 -1
  49. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  50. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  51. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  52. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  53. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  54. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  55. package/dist/builtins/system-config/system-config.addon.js +1 -1
  56. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  57. package/dist/builtins/winston-logging/index.js +1 -1
  58. package/dist/builtins/winston-logging/index.mjs +1 -1
  59. package/dist/{child-cap-dispatch-C1c3rWaX.mjs → child-cap-dispatch-BCJjUtXV.mjs} +9 -1
  60. package/dist/{child-cap-dispatch-De5_HcvR.js → child-cap-dispatch-Be8e2cBP.js} +9 -1
  61. package/dist/{dist-Duqf80QM.mjs → dist-CLfJMMdf.mjs} +1056 -349
  62. package/dist/{dist-C571Y95Z.js → dist-vecbhDzu.js} +1079 -354
  63. package/dist/download/model-downloader.d.ts +15 -0
  64. package/dist/{event-loop-stall-monitor-GPrVvx14.mjs → event-loop-stall-monitor-EXjzIxDz.mjs} +25 -3
  65. package/dist/{event-loop-stall-monitor-CAsHdMwP.js → event-loop-stall-monitor-ey4mbofz.js} +36 -2
  66. package/dist/index.js +21 -3
  67. package/dist/index.mjs +21 -3
  68. package/dist/kernel/config-manager.d.ts +3 -1
  69. package/dist/{retired-settings-keys-0j8rNoa8.js → retired-settings-keys-Bm2p0Rot.js} +1 -1
  70. package/dist/{retired-settings-keys-CeO_dBHU.mjs → retired-settings-keys-C1ODcdnr.mjs} +1 -1
  71. package/package.json +7 -1
  72. package/dist/builtins/device-manager/device-store-sections.d.ts +0 -26
@@ -1,4 +1,4 @@
1
- import { Bn as EventCategory, D as SOURCE_CAPS, En as createEvent, O as SOURCE_DEVICE_TYPES, U as bareAddonId, bt as isSameAddonId, ct as evaluateSensorEdge, dn as errMsg, it as doorbellCapability, pn as BaseAddon, xt as isSourceCap, yn as DeviceType } from "../../dist-Duqf80QM.mjs";
1
+ import { Ct as isSourceCap, D as SOURCE_CAPS, H as bareAddonId, Sn as DeviceType, St as isSameAddonId, Un as EventCategory, X as createDeviceExtensionProvider, et as defineExtensionSlotProvider, gn as BaseAddon, kn as createEvent, mn as errMsg, nt as deviceExtensionCapability, ot as doorbellCapability, ut as evaluateSensorEdge } from "../../dist-CLfJMMdf.mjs";
2
2
  //#region src/builtins/doorbell/trigger-engine.ts
3
3
  /**
4
4
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -161,13 +161,19 @@ var DoorbellTriggerEngine = class {
161
161
  * not survive the next read. So a negative read ARMS the revocation and a
162
162
  * second consecutive negative applies it.
163
163
  *
164
- * ERRING. `undefined` (never read) and `unbind-pending` both ring. Reaching
165
- * the ring path at all means the camera has a configured trigger source,
166
- * and a source can only be configured through this wrapper's own
167
- * binding-gated settings surface — so a configured source is itself
168
- * evidence the wrapper was bound. The cost of erring open is a spurious
169
- * ring on a camera the operator unbound but left configured, for at most
170
- * two reload cycles. The cost of erring closed is the press.
164
+ * ERRING. `undefined` (never read) and `unbind-pending` both ring. The cost
165
+ * of erring open is a spurious ring on a camera whose wrapper is not bound
166
+ * but which has trigger sources, for at most two reload cycles. The cost of
167
+ * erring closed is the press.
168
+ *
169
+ * D655: a configured source is NO LONGER evidence of the binding. D49 leaned
170
+ * on "a source can only be configured through this wrapper's own
171
+ * binding-gated settings surface"; that surface is retired, and the
172
+ * `doorbell-source` extension slot that replaced it is offered on every
173
+ * camera, bound or not (D648). The erring-open window above is therefore
174
+ * reachable by configuring sources on a never-bound camera, not only by
175
+ * unbinding a configured one — still bounded by two reload cycles, and the
176
+ * slot's card says `inactive` once the reads agree.
171
177
  *
172
178
  * ─────────────────────────────────────────────────────────────────────
173
179
  * 2026-08-07 (D72). The corroboration above was DEFEATED, and the reason
@@ -205,7 +211,7 @@ function nextBindingState(previous, boundNow) {
205
211
  }
206
212
  /** May a ring proceed? Only a corroborated unbind stops one. */
207
213
  function allowsRing(state) {
208
- return state !== "unbound";
214
+ return !needsDurableRead(state);
209
215
  }
210
216
  /** True only when a read has positively confirmed the binding. */
211
217
  function hasConfirmedBinding(state) {
@@ -257,6 +263,29 @@ function readDurableWrapperBinding(record, deviceId, capName, ownAddonId) {
257
263
  if (typeof wrapperAddonId !== "string") return "unbound";
258
264
  return isSameAddonId(wrapperAddonId, ownAddonId) ? "bound" : "unbound";
259
265
  }
266
+ /** Does this mirror state need the durable authority before a verdict? Only `unbound` does. */
267
+ function needsDurableRead(state) {
268
+ return state === "unbound";
269
+ }
270
+ /**
271
+ * THE decision — the ring gate (`fireDoorbell` / `ringsDespiteUnboundMirror`)
272
+ * and the Extensions card's activation both call it, so what the card says
273
+ * and what a press does cannot disagree. `durable` is `null` when it was not
274
+ * read, which {@link needsDurableRead} allows for every state but `unbound`.
275
+ * Pure: it reads nothing and writes nothing.
276
+ */
277
+ function ringVerdict(state, durable) {
278
+ if (state === "bound") return "rings";
279
+ if (state === "unbind-pending") return "rings-pending";
280
+ if (state === void 0) return "rings-unconfirmed";
281
+ if (durable === "bound") return "rings";
282
+ if (durable === "unbound") return "silent";
283
+ return "unknown";
284
+ }
285
+ /** Whether a press proceeds under this verdict. */
286
+ function verdictRings(verdict) {
287
+ return verdict !== "silent";
288
+ }
260
289
  //#endregion
261
290
  //#region src/builtins/doorbell/doorbell-settings.ts
262
291
  /**
@@ -349,6 +378,203 @@ function applyDoorbellSourcesPatch(current, patchValue) {
349
378
  return next;
350
379
  }
351
380
  //#endregion
381
+ //#region src/builtins/doorbell/doorbell-extension-slot.ts
382
+ /**
383
+ * The virtual doorbell's side of `device-extension` (D648): the
384
+ * `doorbell-source` slot, served over the wrapper's `doorbellSources` store
385
+ * — since D655 the only editor of a camera's trigger devices.
386
+ *
387
+ * - `list` reads the store through `parseDoorbellSources` — the parser the
388
+ * trigger engine's assignments are built from — so the slot's value is
389
+ * exactly what rings. It also REPORTS whether the wrapper is bound to the
390
+ * camera (`activation`), named as `setWrapperActive`'s: sources on an
391
+ * unbound camera ring nothing, and the card must say so rather than look
392
+ * configured. Nothing here writes it.
393
+ * - The activation costs NOTHING on the common path. A camera with no
394
+ * sources has no gate worth reporting and makes no read. A camera with
395
+ * sources is answered from the addon's D49 binding mirror — the in-memory
396
+ * verdict the ring path itself uses — through `ringVerdict`, the ring
397
+ * gate's own decision function. The card is READ-ONLY: it never refreshes
398
+ * or writes the mirror, and reads the durable authority only for an
399
+ * `unbound` mirror, exactly where the ring gate reads it.
400
+ * `getBindings` was measured at up to 5.24 s on the live hub (2026-08-30),
401
+ * and the dispatcher already paid one for this page.
402
+ * - The picked sources are labelled with ONE batched `listAll({deviceIds})`,
403
+ * never a `getDevice` per id.
404
+ * - `set` goes through the addon's `persistDoorbellSources`, the SAME
405
+ * normalise-and-store path the wrapper has, which also invalidates
406
+ * the assignment cache so the binding mirror is re-read off the ring path
407
+ * (D49). A `setSlot` call is NOT evidence that the wrapper is bound (the
408
+ * contract reaches this provider through `device-extension`'s own
409
+ * binding), so it never confirms the mirror. A NEW pick must be a device
410
+ * that carries a source cap (`SOURCE_CAPS`, the D636 ids-only read) — which
411
+ * also refuses a container, a device with no source cap and an id no
412
+ * device has — and never the camera itself.
413
+ * - `candidates` are those same source-capable devices, devices in the
414
+ * camera's location suggested.
415
+ */
416
+ /** The routing prefix of this owner's `slotRef`s. */
417
+ var VIRTUAL_DOORBELL_EXTENSION_SOURCE = "virtual-doorbell";
418
+ /** The store the slot reads and writes, named (D62). */
419
+ var DOORBELL_SOURCES_AUTHORITY = `virtual-doorbell:${KEY_SOURCES}`;
420
+ /** The authority that decides whether the wrapper serves a camera at all. */
421
+ var WRAPPER_ACTIVATION_AUTHORITY = "device-manager:setWrapperActive";
422
+ var INACTIVE_DETAIL = "The virtual doorbell is not bound to this camera: these devices ring nothing until it is activated in the camera’s bindings.";
423
+ var PENDING_DETAIL = "One bindings read did not see the wrapper; a press still rings until a second read agrees.";
424
+ /** How the card SAYS a verdict. The verdict itself is `ringVerdict`'s, never the card's. */
425
+ function activationFromVerdict(verdict) {
426
+ const authority = WRAPPER_ACTIVATION_AUTHORITY;
427
+ switch (verdict) {
428
+ case "rings": return {
429
+ state: "active",
430
+ authority
431
+ };
432
+ case "rings-pending": return {
433
+ state: "active",
434
+ authority,
435
+ detail: PENDING_DETAIL
436
+ };
437
+ case "rings-unconfirmed": return {
438
+ state: "active",
439
+ authority,
440
+ detail: "No bindings read has confirmed the wrapper yet; a press rings meanwhile."
441
+ };
442
+ case "unknown": return {
443
+ state: "unknown",
444
+ authority,
445
+ detail: "Could not verify: the last reads did not see the wrapper and the bindings store cannot answer. A press still rings."
446
+ };
447
+ case "silent": return {
448
+ state: "inactive",
449
+ authority,
450
+ detail: INACTIVE_DETAIL
451
+ };
452
+ }
453
+ }
454
+ /**
455
+ * Concurrent card views of the same `unbound` camera share ONE durable read,
456
+ * per camera, for as long as it is in flight — a page with several viewers
457
+ * (or a refetch storm) must not multiply the store round trip. Only the
458
+ * CARD's reads are shared: the ring gate calls the addon's
459
+ * `readDurableAuthority` on its own path and never waits behind a card.
460
+ */
461
+ function shareDurableReads(read) {
462
+ const inFlight = /* @__PURE__ */ new Map();
463
+ return (deviceId) => {
464
+ const pending = inFlight.get(deviceId);
465
+ if (pending !== void 0) return pending;
466
+ const started = read(deviceId).finally(() => {
467
+ inFlight.delete(deviceId);
468
+ });
469
+ inFlight.set(deviceId, started);
470
+ return started;
471
+ };
472
+ }
473
+ async function activationOf(deps, readDurable, deviceId) {
474
+ const mirrored = deps.readBindingMirror(deviceId);
475
+ return activationFromVerdict(ringVerdict(mirrored, needsDurableRead(mirrored) ? await readDurable(deviceId) : null));
476
+ }
477
+ function sourceOf(deviceId, byId) {
478
+ const device = byId.get(deviceId);
479
+ if (device === void 0) return {
480
+ deviceId,
481
+ name: null,
482
+ health: "missing",
483
+ detail: "No device has this id any more — it rings nothing until removed."
484
+ };
485
+ return {
486
+ deviceId,
487
+ name: device.name,
488
+ health: "ok"
489
+ };
490
+ }
491
+ async function candidatesFor(deps, deviceId) {
492
+ const [ids, fleet] = await Promise.all([deps.sourceCapableDeviceIds(), deps.listFleet()]);
493
+ const eligible = new Set(ids);
494
+ const byId = new Map(fleet.map((d) => [d.id, d]));
495
+ const location = byId.get(deviceId)?.location ?? null;
496
+ const out = [];
497
+ for (const device of fleet) {
498
+ if (device.id === deviceId || !eligible.has(device.id)) continue;
499
+ const suggested = location !== null && device.location === location;
500
+ const parent = device.parentDeviceId === null ? void 0 : byId.get(device.parentDeviceId);
501
+ out.push({
502
+ deviceId: device.id,
503
+ name: device.name,
504
+ type: device.type,
505
+ location: device.location,
506
+ parentName: parent?.name ?? null,
507
+ suggested,
508
+ reason: suggested ? "same location" : null
509
+ });
510
+ }
511
+ return out.toSorted((a, b) => Number(b.suggested) - Number(a.suggested) || a.name.localeCompare(b.name));
512
+ }
513
+ /** Why each NEW pick is refused, or [] when every one is a legitimate source. */
514
+ async function refusedPicks(deps, deviceId, picked) {
515
+ const persisted = new Set((await deps.readSources(deviceId)).map((s) => s.deviceId));
516
+ const added = picked.filter((id) => !persisted.has(id));
517
+ if (added.length === 0) return [];
518
+ const eligible = new Set(await deps.sourceCapableDeviceIds());
519
+ const refused = [];
520
+ for (const id of added) if (id === deviceId) refused.push(`${id} is the camera itself`);
521
+ else if (!eligible.has(id)) refused.push(`${id} carries no doorbell source capability (a binary sensor or switch)`);
522
+ return refused;
523
+ }
524
+ /** Build the `device-extension` provider the virtual doorbell registers. */
525
+ function createDoorbellExtensionProvider(deps) {
526
+ const readDurable = shareDurableReads(deps.readDurableBinding);
527
+ return createDeviceExtensionProvider({
528
+ source: VIRTUAL_DOORBELL_EXTENSION_SOURCE,
529
+ label: "Virtual doorbell",
530
+ slots: [defineExtensionSlotProvider("doorbell-source", {
531
+ list: async (deviceId) => {
532
+ const ids = (await deps.readSources(deviceId)).map((s) => s.deviceId);
533
+ if (ids.length === 0) return {
534
+ authority: DOORBELL_SOURCES_AUTHORITY,
535
+ enabled: null,
536
+ activation: null,
537
+ value: {
538
+ slot: "doorbell-source",
539
+ deviceIds: []
540
+ },
541
+ sources: []
542
+ };
543
+ const [devices, activation] = await Promise.all([deps.listDevices(ids), activationOf(deps, readDurable, deviceId)]);
544
+ const byId = new Map(devices.map((d) => [d.id, d]));
545
+ return {
546
+ authority: DOORBELL_SOURCES_AUTHORITY,
547
+ enabled: null,
548
+ activation,
549
+ value: {
550
+ slot: "doorbell-source",
551
+ deviceIds: ids
552
+ },
553
+ sources: ids.map((id) => sourceOf(id, byId))
554
+ };
555
+ },
556
+ set: async (deviceId, value) => {
557
+ const refused = await refusedPicks(deps, deviceId, value.deviceIds);
558
+ if (refused.length > 0) {
559
+ deps.logger.warn("virtual-doorbell: extension write refused — nothing persisted", {
560
+ tags: { deviceId },
561
+ meta: { refused }
562
+ });
563
+ throw new Error(`doorbell-source on ${deviceId}: refused — ${refused.join("; ")}`);
564
+ }
565
+ await deps.persistSources(deviceId, value.deviceIds);
566
+ return {
567
+ applied: true,
568
+ reload: "none"
569
+ };
570
+ },
571
+ candidates: (deviceId) => candidatesFor(deps, deviceId)
572
+ })],
573
+ hostTypeOf: async (deviceId) => (await deps.getDevice(deviceId))?.type ?? null,
574
+ logger: deps.logger
575
+ });
576
+ }
577
+ //#endregion
352
578
  //#region src/builtins/doorbell/virtual-doorbell.addon.ts
353
579
  /**
354
580
  * The addon whose store IS the durable authority on wrapper activations.
@@ -397,9 +623,9 @@ function parseDoorbellSlice(slice) {
397
623
  *
398
624
  * 1. The operator binds the wrapper to a camera in the bindings UI
399
625
  * (`defaultActive: false` — never auto-bound).
400
- * 2. In the camera's GENERAL settings (next to "Linked devices") the
401
- * operator picks one or more SOURCE devices from a searchable list —
402
- * that is the ONLY choice. The reaction is derived automatically per
626
+ * 2. In the camera's Extensions section (the `doorbell-source` slot,
627
+ * D648/D655) the operator picks one or more SOURCE devices — that is
628
+ * the ONLY choice. The reaction is derived automatically per
403
629
  * device type: a binary sensor rings when it goes active, a switch
404
630
  * rings when it turns on (see `trigger-engine.ts`). The persisted
405
631
  * shape (`doorbellSources: [{deviceId}]`) stays extensible for a
@@ -446,14 +672,43 @@ var VirtualDoorbellAddon = class extends BaseAddon {
446
672
  });
447
673
  return [{
448
674
  capability: doorbellCapability,
449
- provider: {
450
- getStatus: (input) => this.getStatus(input.deviceId),
451
- getDeviceSettingsContribution: (input) => this.buildDeviceSettingsContribution(input.deviceId),
452
- getDeviceLiveContribution: async () => null,
453
- applyDeviceSettingsPatch: (input) => this.saveDeviceSettingsPatch(input.deviceId, input.patch)
454
- }
675
+ provider: { getStatus: (input) => this.getStatus(input.deviceId) }
676
+ }, {
677
+ capability: deviceExtensionCapability,
678
+ provider: this.buildExtensionProvider()
455
679
  }];
456
680
  }
681
+ /**
682
+ * The `device-extension` provider: the only editor of a camera's trigger
683
+ * devices since D655 (see `doorbell-extension-slot.ts`).
684
+ */
685
+ buildExtensionProvider() {
686
+ const toSlotDevice = (d) => ({
687
+ id: d.id,
688
+ name: d.name,
689
+ type: d.type,
690
+ location: d.location,
691
+ parentDeviceId: d.parentDeviceId
692
+ });
693
+ return createDoorbellExtensionProvider({
694
+ ownAddonId: this.ownAddonId,
695
+ logger: this.ctx.logger,
696
+ readSources: (deviceId) => this.readDeviceSources(deviceId),
697
+ persistSources: (deviceId, deviceIds) => this.persistDoorbellSources(deviceId, deviceIds),
698
+ getDevice: async (deviceId) => {
699
+ const found = await this.ctx.api.deviceManager.getDevice.query({ deviceId });
700
+ return found ? toSlotDevice(found) : null;
701
+ },
702
+ listDevices: async (deviceIds) => (await this.ctx.api.deviceManager.listAll.query({
703
+ projection: "slim",
704
+ deviceIds: [...deviceIds]
705
+ })).map(toSlotDevice),
706
+ readBindingMirror: (deviceId) => this.bindingMirror.get(deviceId),
707
+ readDurableBinding: (deviceId) => this.readDurableAuthority(deviceId),
708
+ sourceCapableDeviceIds: () => this.ctx.api.deviceManager.listDeviceIdsByCaps.query({ capNames: [...SOURCE_CAPS] }),
709
+ listFleet: async () => (await this.ctx.api.deviceManager.listAll.query({ projection: "slim" })).map(toSlotDevice)
710
+ });
711
+ }
457
712
  async handleStateChanged(data) {
458
713
  if (!isSourceCap(data.capName)) return;
459
714
  await this.ensureAssignments();
@@ -671,53 +926,49 @@ var VirtualDoorbellAddon = class extends BaseAddon {
671
926
  */
672
927
  async ringsDespiteUnboundMirror(cameraId, binding) {
673
928
  const authority = await this.readDurableAuthority(cameraId);
674
- if (authority === "bound") {
675
- this.bindingMirror.set(cameraId, "bound");
676
- this.ctx.logger.warn("virtual-doorbell: ring RESCUED — the mirror said unbound but the durable bindings store still names this wrapper; the mirror has been repaired", {
677
- tags: { deviceId: cameraId },
678
- meta: {
679
- bindingState: binding,
680
- ownAddonId: this.ownAddonId
681
- }
682
- });
683
- return true;
684
- }
685
- if (authority === "unreadable") {
686
- this.ctx.logger.warn("virtual-doorbell: ringing on an unbound mirror — the durable bindings store could not answer, and an unanswerable store never destroys a one-shot press", {
687
- tags: { deviceId: cameraId },
688
- meta: {
689
- bindingState: binding,
690
- ownAddonId: this.ownAddonId
691
- }
692
- });
693
- return true;
694
- }
695
- const derived = await this.readDerivedBinding(cameraId);
696
- this.ctx.logger.warn("virtual-doorbell: ring DROPPED — the wrapper is not bound to this camera, but a trigger source is still configured for it", {
697
- tags: { deviceId: cameraId },
698
- meta: {
699
- bindingState: binding,
700
- ownAddonId: this.ownAddonId,
701
- durableAuthority: authority,
702
- entries: derived === null ? "unreadable" : summariseEntries(derived.entries)
929
+ const verdict = ringVerdict(binding, authority);
930
+ switch (verdict) {
931
+ case "rings":
932
+ this.bindingMirror.set(cameraId, "bound");
933
+ this.ctx.logger.warn("virtual-doorbell: ring RESCUED — the mirror said unbound but the durable bindings store still names this wrapper; the mirror has been repaired", {
934
+ tags: { deviceId: cameraId },
935
+ meta: {
936
+ bindingState: binding,
937
+ ownAddonId: this.ownAddonId
938
+ }
939
+ });
940
+ return verdictRings(verdict);
941
+ case "unknown":
942
+ this.ctx.logger.warn("virtual-doorbell: ringing on an unbound mirror — the durable bindings store could not answer, and an unanswerable store never destroys a one-shot press", {
943
+ tags: { deviceId: cameraId },
944
+ meta: {
945
+ bindingState: binding,
946
+ ownAddonId: this.ownAddonId
947
+ }
948
+ });
949
+ return verdictRings(verdict);
950
+ case "silent": {
951
+ const derived = await this.readDerivedBinding(cameraId);
952
+ this.ctx.logger.warn("virtual-doorbell: ring DROPPED — the wrapper is not bound to this camera, but a trigger source is still configured for it", {
953
+ tags: { deviceId: cameraId },
954
+ meta: {
955
+ bindingState: binding,
956
+ ownAddonId: this.ownAddonId,
957
+ durableAuthority: authority,
958
+ entries: derived === null ? "unreadable" : summariseEntries(derived.entries)
959
+ }
960
+ });
961
+ return verdictRings(verdict);
703
962
  }
704
- });
705
- return false;
706
- }
707
- /**
708
- * Record a binding we observed FIRST-HAND. Both callers are cap methods
709
- * the router only reaches by resolving this wrapper for this device —
710
- * which it can only do through the binding — so a call is direct evidence
711
- * and outranks anything the store says.
712
- */
713
- confirmBindingFromCapCall(deviceId) {
714
- this.bindingMirror.set(deviceId, nextBindingState(this.bindingMirror.get(deviceId), true));
963
+ case "rings-pending":
964
+ case "rings-unconfirmed": return verdictRings(verdict);
965
+ }
715
966
  }
716
967
  /**
717
968
  * Lazily (re)build the camera→source assignment set from every
718
969
  * camera's device store. TTL-bounded so devices/settings appearing
719
- * after boot are picked up without a settings save; invalidated
720
- * eagerly by `applyDeviceSettingsPatch`.
970
+ * after boot are picked up without a save; invalidated eagerly by
971
+ * `persistDoorbellSources`.
721
972
  */
722
973
  async ensureAssignments() {
723
974
  const now = Date.now();
@@ -788,72 +1039,17 @@ var VirtualDoorbellAddon = class extends BaseAddon {
788
1039
  return parseDoorbellSources(await this.ctx.settings.readDeviceStore(deviceId));
789
1040
  }
790
1041
  /**
791
- * Single-trip, non-throwing device lookup (mirrors snapshot's
792
- * `lookupDeviceMeta`). Null on any failure — callers fall back to
793
- * rendering the section (better a spurious section than a thrown
794
- * aggregator entry).
1042
+ * THE write path for a camera's trigger sources — the `doorbell-source`
1043
+ * extension slot lands here (D648; its only caller since D655): normalise
1044
+ * to `doorbellSources: [{deviceId}]`, strip the legacy keys, and invalidate
1045
+ * the assignment cache so the next rise (and the binding mirror's next
1046
+ * refresh, off the ring path) sees the new wiring.
795
1047
  */
796
- async lookupDevice(deviceId) {
797
- try {
798
- const found = await this.ctx.api.deviceManager.getDevice.query({ deviceId });
799
- return found ? { type: found.type } : null;
800
- } catch (err) {
801
- this.ctx.logger.debug("virtual-doorbell: getDevice failed during contribution", {
802
- tags: { deviceId },
803
- meta: { error: errMsg(err) }
804
- });
805
- return null;
806
- }
807
- }
808
- /**
809
- * Settings section for the device-detail page — contributed into the
810
- * GENERAL settings surface (no `tab`, so it lands in the same tab as
811
- * "Linked devices", ordered just after it). Camera-only: the doorbell
812
- * cap also applies to Button accessories (native providers) where a
813
- * virtual-source picker is meaningless.
814
- *
815
- * A SINGLE `device-multiselect` field carries only a declarative
816
- * `deviceFilter` — the admin-ui renders the SHARED searchable device
817
- * picker (same idiom as "Linked devices") whose options come from
818
- * `deviceManager.listAll` filtered CLIENT-SIDE. The addon NO LONGER
819
- * computes the option list server-side: the old path enumerated
820
- * `deviceManager.getAllBindings`, which returns an empty set on the live
821
- * cluster and left the picker blank. The reaction is still derived
822
- * automatically per device type — no trigger/eventType controls.
823
- */
824
- async buildDeviceSettingsContribution(deviceId) {
825
- const device = await this.lookupDevice(deviceId);
826
- if (device && device.type !== DeviceType.Camera) return null;
827
- this.confirmBindingFromCapCall(deviceId);
828
- const sources = await this.readDeviceSources(deviceId);
829
- return { sections: [{
830
- id: "virtual-doorbell",
831
- title: "Virtual doorbell",
832
- description: "Ring this camera’s doorbell when a chosen device becomes active — a contact opening, a switch turning on, or a sensor firing. Pick any binary sensor or switch below; the reaction is automatic per device type.",
833
- order: 71,
834
- fields: [{
835
- type: "device-multiselect",
836
- key: KEY_SOURCES,
837
- label: "Trigger devices",
838
- description: "Any of these becoming active rings this camera. Binary sensors ring when they go active; switches ring when they turn on.",
839
- deviceFilter: {
840
- caps: SOURCE_CAPS,
841
- types: SOURCE_DEVICE_TYPES,
842
- excludeDeviceId: deviceId
843
- },
844
- value: sources.map((s) => String(s.deviceId))
845
- }]
846
- }] };
847
- }
848
- async saveDeviceSettingsPatch(deviceId, patch) {
1048
+ async persistDoorbellSources(deviceId, value) {
849
1049
  if (!this.ctx.settings) throw new Error("[virtual-doorbell] settings store unavailable — cannot persist per-device settings");
850
- this.confirmBindingFromCapCall(deviceId);
851
- if ("doorbellSources" in patch) {
852
- const next = applyDoorbellSourcesPatch(await this.ctx.settings.readDeviceStore(deviceId), patch[KEY_SOURCES]);
853
- await this.ctx.settings.writeDeviceStore(deviceId, next);
854
- this.invalidateAssignments();
855
- }
856
- return { success: true };
1050
+ const current = await this.ctx.settings.readDeviceStore(deviceId);
1051
+ await this.ctx.settings.writeDeviceStore(deviceId, applyDoorbellSourcesPatch(current, value));
1052
+ this.invalidateAssignments();
857
1053
  }
858
1054
  };
859
1055
  //#endregion
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C571Y95Z.js");
6
+ const require_dist = require("../../dist-vecbhDzu.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
9
9
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,4 +1,4 @@
1
- import { kt as logDestinationCapability, pn as BaseAddon } from "../../dist-Duqf80QM.mjs";
1
+ import { gn as BaseAddon, jt as logDestinationCapability } from "../../dist-CLfJMMdf.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
4
4
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-C571Y95Z.js");
3
+ const require_dist = require("../../dist-vecbhDzu.js");
4
4
  //#region src/builtins/liveness-monitor/liveness-checks.ts
5
5
  var NO_DEVICES = "liveness:no-devices";
6
6
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -1,4 +1,4 @@
1
- import { Bn as EventCategory, C as RECORDING_SOURCE_CAMSTACK_ADDON, E as RecordingStatusSchema, En as createEvent, dn as errMsg, pn as BaseAddon } from "../../dist-Duqf80QM.mjs";
1
+ import { C as RECORDING_SOURCE_CAMSTACK_ADDON, E as RecordingStatusSchema, Un as EventCategory, gn as BaseAddon, kn as createEvent, mn as errMsg } from "../../dist-CLfJMMdf.mjs";
2
2
  //#region src/builtins/liveness-monitor/liveness-checks.ts
3
3
  var NO_DEVICES = "liveness:no-devices";
4
4
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C571Y95Z.js");
6
+ const require_dist = require("../../dist-vecbhDzu.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  node_crypto = require_chunk.__toESM(node_crypto);
9
9
  let crypto$1 = require("crypto");
@@ -1,5 +1,5 @@
1
1
  import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
2
- import { I as UserRecordSchema, V as authProviderCapability, j as ScopedTokenSchema, pn as BaseAddon, r as ApiKeyRecordSchema, sn as userManagementCapability } from "../../dist-Duqf80QM.mjs";
2
+ import { A as ScopedTokenSchema, B as authProviderCapability, F as UserRecordSchema, gn as BaseAddon, ln as userManagementCapability, r as ApiKeyRecordSchema } from "../../dist-CLfJMMdf.mjs";
3
3
  import * as crypto$2 from "node:crypto";
4
4
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
5
5
  import nodeCrypto from "crypto";
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-C571Y95Z.js");
3
+ const require_dist = require("../../dist-vecbhDzu.js");
4
4
  const require_tls = require("../../tls-D_wUeOTD.js");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Bn as EventCategory, Dt as localNetworkCapability, pn as BaseAddon } from "../../dist-Duqf80QM.mjs";
1
+ import { Un as EventCategory, gn as BaseAddon, kt as localNetworkCapability } from "../../dist-CLfJMMdf.mjs";
2
2
  import { S as readLanHttpState, _ as DEFAULT_LAN_HTTP_PORT, a as writeTlsMode, i as writeExtraSans, l as reissueTlsLeaf, n as readTlsAccessStatus, o as validateUploadedTls, r as readTlsMode, s as ensureTlsCert, t as readExtraSans, y as applyLanHttp } from "../../tls-IM-v7JV4.mjs";
3
3
  import { mkdir, writeFile } from "node:fs/promises";
4
4
  import { join } from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C571Y95Z.js");
6
+ const require_dist = require("../../dist-vecbhDzu.js");
7
7
  //#region src/builtins/loki-logging/loki-payload.ts
8
8
  /**
9
9
  * Loki rejects a label name that is not a valid Prometheus label
@@ -1,4 +1,4 @@
1
- import { kt as logDestinationCapability, pn as BaseAddon } from "../../dist-Duqf80QM.mjs";
1
+ import { gn as BaseAddon, jt as logDestinationCapability } from "../../dist-CLfJMMdf.mjs";
2
2
  //#region src/builtins/loki-logging/loki-payload.ts
3
3
  /**
4
4
  * Loki rejects a label name that is not a valid Prometheus label
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C571Y95Z.js");
6
+ const require_dist = require("../../dist-vecbhDzu.js");
7
7
  let node_fs_promises = require("node:fs/promises");
8
8
  let node_fs = require("node:fs");
9
9
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { Bn as EventCategory, En as createEvent, Gt as resolveBucketMs, Pt as metricsProviderCapability, Wt as reducePoints, dn as errMsg, pn as BaseAddon, pt as foldSnapshotByFunction } from "../../dist-Duqf80QM.mjs";
1
+ import { It as metricsProviderCapability, Kt as reducePoints, Un as EventCategory, gn as BaseAddon, ht as foldSnapshotByFunction, kn as createEvent, mn as errMsg, qt as resolveBucketMs } from "../../dist-CLfJMMdf.mjs";
2
2
  import { access, readFile, readdir } from "node:fs/promises";
3
3
  import * as fs from "node:fs";
4
4
  import { constants, readFileSync } from "node:fs";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C571Y95Z.js");
6
+ const require_dist = require("../../dist-vecbhDzu.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_child_process = require("node:child_process");
@@ -1,5 +1,5 @@
1
1
  import { o as __toESM } from "../../chunk-CNf5ZN-e.mjs";
2
- import { Bn as EventCategory, On as emitReadiness, Vt as platformProbeCapability, Zt as scoreRuntimes, at as enumerateInferenceDevices, dn as errMsg, pn as BaseAddon } from "../../dist-Duqf80QM.mjs";
2
+ import { $t as scoreRuntimes, Un as EventCategory, Ut as platformProbeCapability, gn as BaseAddon, jn as emitReadiness, mn as errMsg, st as enumerateInferenceDevices } from "../../dist-CLfJMMdf.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { execFile } from "node:child_process";
5
5
  import * as os from "node:os";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C571Y95Z.js");
6
+ const require_dist = require("../../dist-vecbhDzu.js");
7
7
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
8
8
  /**
9
9
  * Reconcile the durable `enabledProviders` set against authoritative