@camstack/addon-admin-ui 1.2.36 → 1.2.37

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 (114) hide show
  1. package/dist/assets/{AddLocationWizard-D3WNwwzO.js → AddLocationWizard-ruAb2ySj.js} +1 -1
  2. package/dist/assets/AddonCollectionPage-y9PlVqw3.js +1 -0
  3. package/dist/assets/Addons-cAsBu2OQ.js +1 -0
  4. package/dist/assets/{AdminTabs-EuBAqr82.js → AdminTabs-BVHcviOJ.js} +1 -1
  5. package/dist/assets/{AiProfiles-YpD0cNHK.js → AiProfiles-B6EskvNu.js} +1 -1
  6. package/dist/assets/{BrokerForm-lcZzCH84.js → BrokerForm-DIw2g_KM.js} +1 -1
  7. package/dist/assets/Build-sVmfeGFf.js +29 -0
  8. package/dist/assets/Capabilities-CTlKJVid.js +1 -0
  9. package/dist/assets/{CapabilityBadges-DDCH_NHd.js → CapabilityBadges-B_ZC7k04.js} +1 -1
  10. package/dist/assets/Cluster-DkXZdWpm.js +1 -0
  11. package/dist/assets/{ClusterRecordingTab-uoQhw_ij.js → ClusterRecordingTab-U0cdF9UF.js} +1 -1
  12. package/dist/assets/{Dashboard-BM-5S-Cj.js → Dashboard-BCNcg--7.js} +1 -1
  13. package/dist/assets/Data-ZtVnR1Fe.js +1 -0
  14. package/dist/assets/DetectionIntelligence-BlZl9E8S.js +1 -0
  15. package/dist/assets/{DeviceAggregateSection-Cfju7u5B.js → DeviceAggregateSection-UxtJHE5B.js} +1 -1
  16. package/dist/assets/DeviceDetail-BbYvgUSY.js +2 -0
  17. package/dist/assets/{Devices-dbkRu6sh.js → Devices-DzLVWcFg.js} +1 -1
  18. package/dist/assets/{EmbedPlayerPage-CCTm073s.js → EmbedPlayerPage-C5yT7D0W.js} +1 -1
  19. package/dist/assets/Events-BGJ5su4V.js +1 -0
  20. package/dist/assets/FormBuilder-C0N6NRfe.js +1 -0
  21. package/dist/assets/{Identity-EphebNT0.js → Identity-FI4f8ox5.js} +1 -1
  22. package/dist/assets/IntegrationDetail-DxAOxN6t.js +1 -0
  23. package/dist/assets/Integrations-D5S4Yf1A.js +1 -0
  24. package/dist/assets/{Integrations-B3fddOYM.js → Integrations-w3RuUxDV.js} +1 -1
  25. package/dist/assets/{Logs-xK4--bh0.js → Logs-BfDIf7QZ.js} +1 -1
  26. package/dist/assets/{MotionZonesSettings-C0sxf_os.js → MotionZonesSettings-CHyWV5lm.js} +1 -1
  27. package/dist/assets/{MyAccess-D043-D3_.js → MyAccess-76Y7QhMu.js} +1 -1
  28. package/dist/assets/Network-BAUdETHi.js +1 -0
  29. package/dist/assets/{NodeAddonsSettingsPanel-CV-reVXD.js → NodeAddonsSettingsPanel-CeWEr-Zg.js} +1 -1
  30. package/dist/assets/NodeInferenceDevices-CUinkSSN.js +1 -0
  31. package/dist/assets/{Notifications-DZMSKwIB.js → Notifications-CK3PuxFQ.js} +1 -1
  32. package/dist/assets/Pipeline-CTSS1aGu.js +1 -0
  33. package/dist/assets/Plates-Cns09Gg2.js +1 -0
  34. package/dist/assets/{PrivacyMaskSettings-Cj47lnFx.js → PrivacyMaskSettings-C4ycA7Un.js} +1 -1
  35. package/dist/assets/{ProviderIcon-DFIIdGww.js → ProviderIcon-DyK-gNEs.js} +1 -1
  36. package/dist/assets/Recordings-UAlZznSu.js +1 -0
  37. package/dist/assets/{ScanNetworkModal-DEK5U2cd.js → ScanNetworkModal-h3WtjMhA.js} +1 -1
  38. package/dist/assets/{Settings-Casus0ex.js → Settings-oupU5GDf.js} +1 -1
  39. package/dist/assets/{Showroom-f88Jp6ju.js → Showroom-HqqCUr23.js} +1 -1
  40. package/dist/assets/{Terminal-M_8UHE7n.js → Terminal-7B0qx8ho.js} +2 -2
  41. package/dist/assets/{_virtual_mf-localSharedImportMap___mfe_internal__admin_ui_host-AQtYY37w.js → _virtual_mf-localSharedImportMap___mfe_internal__admin_ui_host-BnceOEA8.js} +1 -1
  42. package/dist/assets/authed-media-B8T1dsFh.js +1 -0
  43. package/dist/assets/{rotate-ccw-DGfCNuX0.js → boxes-BS6UzXs9.js} +1 -1
  44. package/dist/assets/{build-cluster-graph-CAb1HaUw.js → build-cluster-graph-CO-MCBmD.js} +2 -2
  45. package/dist/assets/circle-check-big-DKZCFZff.js +1 -0
  46. package/dist/assets/clock-DqPUDB1Y.js +1 -0
  47. package/dist/assets/{copy-BtwlORlH.js → copy-bKWchDUS.js} +1 -1
  48. package/dist/assets/debounced-invalidate-CoCA-chF.js +1 -0
  49. package/dist/assets/dist-B66gZe27.js +7 -0
  50. package/dist/assets/{dist-B_UrLLw9.js → dist-CXHmz2j1.js} +1 -1
  51. package/dist/assets/{download-T_lOWVlB.js → download-C3-lXLSJ.js} +1 -1
  52. package/dist/assets/{face-bbox-f3p_Yctl.js → face-bbox-Cb2eJK0t.js} +1 -1
  53. package/dist/assets/{hostInit-tEAagC07.js → hostInit-DtkE57IB.js} +1 -1
  54. package/dist/assets/index-Bke_0boK.js +4 -0
  55. package/dist/assets/index-mdJi1UIz.css +2 -0
  56. package/dist/assets/{key-round-DDU-8is6.js → key-round-CSNhr2jN.js} +1 -1
  57. package/dist/assets/{mf-entry-bootstrap-0-482a4da7.js → mf-entry-bootstrap-0-e0821d3f.js} +2 -2
  58. package/dist/assets/{pencil-DXM3cvvP.js → pencil-Dbscj0iB.js} +1 -1
  59. package/dist/assets/{player-overlays-Ba-DoE5F.js → player-overlays-CCOENCr_.js} +1 -1
  60. package/dist/assets/plus-kjGKne3v.js +1 -0
  61. package/dist/assets/{radio-DGfartpy.js → radio-BCrXf-T5.js} +1 -1
  62. package/dist/assets/{refresh-cw-NwlTy6_-.js → refresh-cw-B50UNH29.js} +1 -1
  63. package/dist/assets/remoteEntry-DSjWOwlm.js +1 -0
  64. package/dist/assets/rotate-ccw-CnFi79eH.js +1 -0
  65. package/dist/assets/rotate-cw-BMi2-EKp.js +1 -0
  66. package/dist/assets/search-Cqbwip0Z.js +1 -0
  67. package/dist/assets/{send-zoWD-FsL.js → send-vIA2Sg01.js} +1 -1
  68. package/dist/assets/{server-Bv5NUu2G.js → server-CVgGKwKo.js} +1 -1
  69. package/dist/assets/square-NIJNDIvZ.js +1 -0
  70. package/dist/assets/{square-check-big-C_0qc0Ud.js → square-check-big-DTiBKI6p.js} +1 -1
  71. package/dist/assets/src-C3IwZqep.js +7 -0
  72. package/dist/assets/src-DiF3p-RR.js +67 -0
  73. package/dist/assets/{undo-2-B5eeo7kt.js → undo-2-buMXiXDJ.js} +1 -1
  74. package/dist/assets/{upload-B2QyA3Ml.js → upload-YICZuTHD.js} +1 -1
  75. package/dist/assets/{useEventInvalidation-CQp2HDcf.js → useEventInvalidation-CGT7WKQk.js} +1 -1
  76. package/dist/assets/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__admin_ui_host__remoteEntry-_hash_-BmbjlXvs.js → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__admin_ui_host__remoteEntry-_hash_-CbdC118i.js} +2 -2
  77. package/dist/assets/{wifi-DiUaWSk8.js → wifi-B6uMvniy.js} +1 -1
  78. package/dist/assets/{zap-y-GpHg-7.js → zap-O3h5iXhW.js} +1 -1
  79. package/dist/index.html +6 -6
  80. package/dist/server/addon.js +1040 -1065
  81. package/package.json +1 -1
  82. package/dist/assets/AddonCollectionPage-D6-DslHs.js +0 -1
  83. package/dist/assets/Addons-xiv8mENL.js +0 -1
  84. package/dist/assets/Automation-Bnvadu_q.js +0 -29
  85. package/dist/assets/Capabilities-BO4EdQiS.js +0 -1
  86. package/dist/assets/Cluster-HqhJz5p-.js +0 -1
  87. package/dist/assets/Data-DBvjn1Ul.js +0 -1
  88. package/dist/assets/DetectionIntelligence-pRUA7cQB.js +0 -1
  89. package/dist/assets/DeviceDetail-DUZkWQjP.js +0 -2
  90. package/dist/assets/Events-D9bI1DWk.js +0 -1
  91. package/dist/assets/FormBuilder-pSCqn9Pp.js +0 -1
  92. package/dist/assets/IntegrationDetail-vt_WT2kb.js +0 -1
  93. package/dist/assets/Integrations-D2z39cEx.js +0 -1
  94. package/dist/assets/Network-DQkG5Rat.js +0 -1
  95. package/dist/assets/NodeInferenceDevices-tLptbnD4.js +0 -1
  96. package/dist/assets/Pipeline-f6X5D5vg.js +0 -1
  97. package/dist/assets/Plates-Dct--YKd.js +0 -1
  98. package/dist/assets/Recordings-Sn1cAwz6.js +0 -1
  99. package/dist/assets/Retrain-cSIFr_tD.js +0 -1
  100. package/dist/assets/authed-media-XfBD4Ma-.js +0 -1
  101. package/dist/assets/circle-check-big-DG4qagl7.js +0 -1
  102. package/dist/assets/clock-XeL04zsb.js +0 -1
  103. package/dist/assets/debounced-invalidate-D3Cu80c9.js +0 -1
  104. package/dist/assets/dist-rfpANeiT.js +0 -7
  105. package/dist/assets/image-off-CxrOL9bA.js +0 -1
  106. package/dist/assets/index-BJkpCYtR.css +0 -2
  107. package/dist/assets/index-CY7HK9zV.js +0 -4
  108. package/dist/assets/plus-DrqAQeFJ.js +0 -1
  109. package/dist/assets/remoteEntry-BzAN7Tig.js +0 -1
  110. package/dist/assets/rotate-cw-W4djelbj.js +0 -1
  111. package/dist/assets/search-DOYQxoHy.js +0 -1
  112. package/dist/assets/square-CBtc15_U.js +0 -1
  113. package/dist/assets/src-CPanq9UB.js +0 -67
  114. package/dist/assets/src-EmgPMReM.js +0 -7
@@ -9414,12 +9414,34 @@ method(object({
9414
9414
  kinds: array(EventMediaKindSchema).min(1).default(["mp4"]),
9415
9415
  /** GIF geometry. The video keeps the source's own. */
9416
9416
  gifMaxWidth: number().int().min(120).max(1280).default(640),
9417
- gifFps: number().int().min(1).max(15).default(8),
9418
9417
  /**
9419
- * Playback rate, applied to EVERY container so they stay one clip.
9420
- * `1` is real time and is what allows the copy branch.
9418
+ * The gif's own PLAYBACK rate in frames per second what the finished
9419
+ * gif runs at, not how many source frames feed it. The decimation that
9420
+ * feeds it samples `gifFps / gifSpeed` source frames per second, so at
9421
+ * the defaults a 12 fps gif is built out of 3 source frames a second.
9421
9422
  */
9422
- speed: number().min(1).max(8).default(1)
9423
+ gifFps: number().int().min(1).max(15).default(12),
9424
+ /**
9425
+ * How fast the GIF plays against real time, independent of `speed`.
9426
+ *
9427
+ * 4× by default, by operator request: a notification gif is glanced at
9428
+ * on a lock screen, so a ~12 s window has to be over in ~3 s. It stays
9429
+ * a separate knob from `speed` even though both now default to 4 —
9430
+ * a caller wanting a real-time video and a fast gif must not have to
9431
+ * choose.
9432
+ */
9433
+ gifSpeed: number().min(1).max(8).default(4),
9434
+ /**
9435
+ * Playback rate of the VIDEO. Also 4× by default, by operator decision.
9436
+ *
9437
+ * `1` is real time and is the ONLY value that allows the copy branch —
9438
+ * anything else forces `libx264` over the window. That was priced
9439
+ * before it was chosen: a per-event burst measured at 0.23 s and 254 KB
9440
+ * on a real 615 720p cut, against 922 KB for the copy it replaces. A
9441
+ * re-encode is capped at 720p (`EVENT_CLIP_ENCODE_MAX_WIDTH`), because
9442
+ * once the decode is forced the width stops being free.
9443
+ */
9444
+ speed: number().min(1).max(8).default(4)
9423
9445
  }), EventMediaProductionSchema, {
9424
9446
  kind: "mutation",
9425
9447
  auth: "admin"
@@ -9622,25 +9644,6 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
9622
9644
  }),
9623
9645
  lastChangedAt: number()
9624
9646
  });
9625
- /**
9626
- * core-blocks — user-authored TypeScript, stored in the kernel and executed in
9627
- * its own process.
9628
- *
9629
- * Spec: `docs/superpowers/specs/2026-08-04-core-blocks-and-synthetic-devices-design.md`.
9630
- *
9631
- * The first use is **owning devices without being a device provider**: a block
9632
- * declares devices under a system or custom integration and drives their state,
9633
- * with the same `ctx` an addon gets. Automations come later; nothing here
9634
- * models a trigger.
9635
- *
9636
- * **Stated plainly, because it does not change by being true:** a block has an
9637
- * addon's powers — devices, storage, the event bus, `ctx.api`. It is a plugin
9638
- * with no review step. What makes that survivable is not a sandbox, it is
9639
- * PROCESS ISOLATION: one process per block, supervised by `CrashSupervisor`,
9640
- * so a block that throws or never returns is marked `failed` and visible
9641
- * instead of taking the hub with it (D6). Every method here is admin-only, and
9642
- * must stay so.
9643
- */
9644
9647
  /** Where a block runs. The operator chooses — a block driving a device on an
9645
9648
  * agent is the reason placement is not fixed to the hub. */
9646
9649
  var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
@@ -9712,6 +9715,9 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
9712
9715
  }), object({ block: CoreBlockSchema }), {
9713
9716
  kind: "mutation",
9714
9717
  auth: "admin"
9718
+ }), method(object({ blockId: string() }), object({ block: CoreBlockSchema }), {
9719
+ kind: "mutation",
9720
+ auth: "admin"
9715
9721
  }), method(object({ code: string() }), CoreBlockCompileResultSchema, {
9716
9722
  kind: "mutation",
9717
9723
  auth: "admin"
@@ -10390,850 +10396,169 @@ var ExposeInputSchema = object({
10390
10396
  });
10391
10397
  var UnexposeInputSchema = object({ deviceId: string() });
10392
10398
  method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), { kind: "mutation" }), method(UnexposeInputSchema, _void(), { kind: "mutation" });
10399
+ var ProviderStatusSchema = object({
10400
+ connected: boolean(),
10401
+ deviceCount: number(),
10402
+ error: string().optional()
10403
+ });
10404
+ object({
10405
+ externalId: string(),
10406
+ name: string(),
10407
+ type: string(),
10408
+ metadata: record(string(), unknown()).optional()
10409
+ });
10393
10410
  /**
10394
- * Resource-bound constants for the safe expression engine.
10395
- *
10396
- * Every bound is defense-in-depth: the grammar is non-Turing-complete (no
10397
- * loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
10398
- * O(nodeCount) by construction. These caps merely put a hard ceiling on the
10399
- * work a single author-supplied expression can request, so a hostile or
10400
- * accidental pathological string can never spend unbounded CPU/memory.
10411
+ * Candidate handed back from discovery and accepted by
10412
+ * `adoptDiscoveredDevice`. Shape mirrors the in-process
10413
+ * `DiscoveredDevice` interface used by `DeviceDiscovery`.
10401
10414
  */
10402
- /** Max source length (chars) — checked BEFORE tokenizing so a huge string is
10403
- * rejected without allocation. */
10404
- var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
10405
- /** A legal binding / identifier name. */
10406
- var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
10407
- /** Binding names an author may NOT use: `now` is auto-injected; the literal
10408
- * keywords lex as values, not identifiers, so binding to them is meaningless. */
10409
- var RESERVED_BINDING_NAMES = new Set([
10410
- "now",
10411
- "true",
10412
- "false",
10413
- "null"
10414
- ]);
10415
+ var DiscoveryCandidateSchema = object({
10416
+ stableId: string(),
10417
+ type: _enum(DeviceType),
10418
+ suggestedName: string(),
10419
+ prefilledConfig: record(string(), unknown()),
10420
+ /**
10421
+ * Optional upstream-system identity (HA entity_id, vendor MAC, …).
10422
+ * Discovery pre-populates this for systems that know the upstream
10423
+ * identity ahead of adoption. Rendering metadata (unit, precision)
10424
+ * flows live through the cap STATUS SLICE after adoption.
10425
+ */
10426
+ sourceInfo: SourceInfoSchema.optional()
10427
+ });
10415
10428
  /**
10416
- * Error types for the safe expression engine. Two distinct classes so callers
10417
- * can tell a compile-time (grammar) failure from a runtime (evaluation)
10418
- * failure both are non-fatal to the host: read paths degrade to "skip link".
10429
+ * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
10430
+ * Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
10431
+ * tRPC layer can pass it through without reshaping.
10419
10432
  */
10420
- /** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
10421
- * the failure is anchored to a character (author-facing inline feedback). */
10422
- var ExpressionParseError = class extends Error {
10423
- position;
10424
- constructor(message, position) {
10425
- super(message);
10426
- this.name = "ExpressionParseError";
10427
- this.position = position;
10428
- }
10429
- };
10430
- /** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
10431
- * result, unknown builtin, step-budget exceeded). */
10432
- var ExpressionEvalError = class extends Error {
10433
- constructor(message) {
10434
- super(message);
10435
- this.name = "ExpressionEvalError";
10436
- }
10437
- };
10433
+ var DeviceSummarySchema = object({
10434
+ id: number(),
10435
+ stableId: string(),
10436
+ addonId: string(),
10437
+ type: string(),
10438
+ name: string(),
10439
+ parentDeviceId: number().nullable(),
10440
+ online: boolean(),
10441
+ features: array(string()),
10442
+ config: record(string(), unknown()),
10443
+ /** Optional upstream-system identity (dispatch key + system tag).
10444
+ * See `SourceInfo`. Present when the device has a non-synthetic
10445
+ * source identifier (HA entities, vendor MAC, …); omitted when the
10446
+ * synthetic backfill is in effect. */
10447
+ sourceInfo: SourceInfoSchema.optional()
10448
+ });
10438
10449
  /**
10439
- * Frozen, null-prototype builtin function table for the expression engine
10440
- * (spec §4 rule 4). The table is the SOLE surface of callable functions: the
10441
- * parser rejects any callee not in it, and the evaluator gates each call on an
10442
- * own-property check against it.
10450
+ * Result of a live field test (e.g. probing an RTSP URL during device
10451
+ * creation). Matches the UI-side `FieldProbeResult` in
10452
+ * `interfaces/config-ui.ts` the admin `FormBuilder` renders the
10453
+ * returned `labels` as chips next to the input.
10454
+ */
10455
+ var FieldProbeResultSchema = object({
10456
+ status: _enum(["ok", "error"]),
10457
+ labels: array(string()).optional(),
10458
+ error: string().optional()
10459
+ });
10460
+ /**
10461
+ * The output of `getChildCreationSchema` is a UI schema tree. We store
10462
+ * it as `unknown` at the capability layer — the router just passes it
10463
+ * through and the admin UI renders it via `FormBuilder`. The actual
10464
+ * type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
10465
+ * but we deliberately avoid a Zod mirror because the union is large and
10466
+ * not meant for runtime validation at this seam.
10467
+ */
10468
+ var CreationSchemaOutputSchema = unknown();
10469
+ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), ProviderStatusSchema), method(_void(), array(object({
10470
+ id: string(),
10471
+ name: string(),
10472
+ type: string()
10473
+ }))), method(object({}), boolean()), method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
10474
+ kind: "mutation",
10475
+ auth: "admin"
10476
+ }), method(object({}), CreationSchemaOutputSchema), method(object({}), object({ deviceType: _enum(DeviceType).nullable() })), method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
10477
+ kind: "mutation",
10478
+ auth: "admin"
10479
+ }), method(object({}), boolean()), method(object({ type: _enum(DeviceType) }), CreationSchemaOutputSchema), method(object({
10480
+ type: _enum(DeviceType),
10481
+ config: record(string(), unknown())
10482
+ }), DeviceSummarySchema, {
10483
+ kind: "mutation",
10484
+ auth: "admin"
10485
+ }), method(object({
10486
+ type: _enum(DeviceType),
10487
+ key: string(),
10488
+ value: unknown(),
10489
+ formValues: record(string(), unknown()).optional()
10490
+ }), FieldProbeResultSchema, {
10491
+ kind: "mutation",
10492
+ auth: "admin"
10493
+ });
10494
+ /**
10495
+ * Device Manager capability — hub-side singleton that unifies device persistence,
10496
+ * live registry access, and all management operations into a single tRPC surface.
10443
10497
  *
10444
- * Because the object has a NULL prototype AND is `Object.freeze`d:
10445
- * - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
10446
- * - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
10447
- * (there is no `Object.prototype` in the chain), so those names are not
10448
- * callable — they are simply "unknown function" at parse time.
10498
+ * Replaces:
10499
+ * - `device-persistence` capability (persistence methods absorbed here)
10500
+ * - `device-management.router.ts` (deleted in Phase 2)
10501
+ * - `device-ops.router.ts` (compat layer deleted; device-provider ops absorbed here)
10449
10502
  *
10450
- * Every numeric argument is validated as a finite number and every numeric
10451
- * RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
10452
- * (`pow(10,400)` Infinity) all raise `ExpressionEvalError` and fail the link
10453
- * closed rather than emitting a garbage value.
10503
+ * All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
10504
+ * fork into separate processes but never run on remote cluster agents. Therefore:
10505
+ * - No nodeId routing needed this is a pure hub singleton.
10506
+ * - The hub's DeviceRegistry is the single source of truth for all live devices.
10507
+ * - No shadow registry or cross-node aggregation required.
10508
+ *
10509
+ * Forked workers register devices back to the hub via `ctx.devices`
10510
+ * (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
10454
10511
  */
10455
- function asFiniteNumber(value, name, index) {
10456
- if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
10457
- return value;
10458
- }
10459
- function asString$1(value, name, index) {
10460
- if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
10461
- return value;
10462
- }
10463
- function finiteResult(value, name) {
10464
- if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
10465
- return value;
10466
- }
10467
- function allFiniteNumbers(args, name) {
10468
- return args.map((a, idx) => asFiniteNumber(a, name, idx));
10469
- }
10470
- var INF = Number.POSITIVE_INFINITY;
10471
- var table = {
10472
- min: {
10473
- minArgs: 1,
10474
- maxArgs: INF,
10475
- apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
10476
- },
10477
- max: {
10478
- minArgs: 1,
10479
- maxArgs: INF,
10480
- apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
10481
- },
10482
- abs: {
10483
- minArgs: 1,
10484
- maxArgs: 1,
10485
- apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
10486
- },
10487
- floor: {
10488
- minArgs: 1,
10489
- maxArgs: 1,
10490
- apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
10491
- },
10492
- ceil: {
10493
- minArgs: 1,
10494
- maxArgs: 1,
10495
- apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
10496
- },
10497
- sqrt: {
10498
- minArgs: 1,
10499
- maxArgs: 1,
10500
- apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
10501
- },
10502
- round: {
10503
- minArgs: 1,
10504
- maxArgs: 2,
10505
- apply: (args) => {
10506
- const x = asFiniteNumber(args[0], "round", 0);
10507
- const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
10508
- if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
10509
- const factor = 10 ** digits;
10510
- return finiteResult(Math.round(x * factor) / factor, "round");
10511
- }
10512
- },
10513
- pow: {
10514
- minArgs: 2,
10515
- maxArgs: 2,
10516
- apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
10517
- },
10518
- clamp: {
10519
- minArgs: 3,
10520
- maxArgs: 3,
10521
- apply: (args) => {
10522
- const x = asFiniteNumber(args[0], "clamp", 0);
10523
- const lo = asFiniteNumber(args[1], "clamp", 1);
10524
- const hi = asFiniteNumber(args[2], "clamp", 2);
10525
- if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
10526
- return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
10527
- }
10528
- },
10529
- avg: {
10530
- minArgs: 1,
10531
- maxArgs: INF,
10532
- apply: (args) => {
10533
- const nums = allFiniteNumbers(args, "avg");
10534
- return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
10535
- }
10536
- },
10537
- sum: {
10538
- minArgs: 1,
10539
- maxArgs: INF,
10540
- apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
10541
- },
10542
- coalesce: {
10543
- minArgs: 1,
10544
- maxArgs: INF,
10545
- apply: (args) => {
10546
- for (const a of args) if (a !== null) return a;
10547
- return null;
10548
- }
10549
- },
10550
- age: {
10551
- minArgs: 2,
10552
- maxArgs: 2,
10553
- apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
10554
- },
10555
- convert: {
10556
- minArgs: 3,
10557
- maxArgs: 3,
10558
- apply: (args, hooks) => {
10559
- const x = asFiniteNumber(args[0], "convert", 0);
10560
- const from = asString$1(args[1], "convert", 1).trim();
10561
- const to = asString$1(args[2], "convert", 2).trim();
10562
- if (hooks.convert) {
10563
- const out = hooks.convert(x, from, to);
10564
- if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
10565
- return finiteResult(out, "convert");
10566
- }
10567
- if (from === to) return x;
10568
- throw new ExpressionEvalError("convert: unit conversion table not installed");
10569
- }
10570
- }
10571
- };
10572
- Object.freeze(Object.assign(Object.create(null), table));
10573
- /** The set of valid builtin names — used by the parser to reject unknown
10574
- * callees at parse time (immediate author feedback). */
10575
- var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
10576
- /**
10577
- * Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
10578
- * zero-dependency. The grammar is deliberately boring: decimal numbers,
10579
- * single/double-quoted strings with a tiny escape set, identifiers, the three
10580
- * value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
10581
- * outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
10582
- * is a parse error with a source position, so member access / assignment /
10583
- * template literals are lexically impossible.
10584
- */
10585
- var KEYWORDS = new Set([
10586
- "true",
10587
- "false",
10588
- "null"
10589
- ]);
10590
- function isDigit(ch) {
10591
- return ch >= "0" && ch <= "9";
10592
- }
10593
- function isIdentStart(ch) {
10594
- return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
10595
- }
10596
- function isIdentPart(ch) {
10597
- return isIdentStart(ch) || isDigit(ch);
10598
- }
10599
- function isWhitespace(ch) {
10600
- return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
10601
- }
10602
- /** Tokenize `source` into a flat token list ending with a single `eof` token.
10603
- * Throws `ExpressionParseError` on any illegal character or unterminated
10604
- * string. */
10605
- function tokenize(source) {
10606
- if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
10607
- const tokens = [];
10608
- let i = 0;
10609
- const n = source.length;
10610
- while (i < n) {
10611
- const ch = source[i];
10612
- if (isWhitespace(ch)) {
10613
- i += 1;
10614
- continue;
10615
- }
10616
- if (isDigit(ch)) {
10617
- const start = i;
10618
- while (i < n && isDigit(source[i])) i += 1;
10619
- if (i < n && source[i] === ".") {
10620
- if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
10621
- i += 1;
10622
- while (i < n && isDigit(source[i])) i += 1;
10623
- }
10624
- const text = source.slice(start, i);
10625
- const value = Number(text);
10626
- if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
10627
- tokens.push({
10628
- type: "number",
10629
- value,
10630
- pos: start
10631
- });
10632
- continue;
10633
- }
10634
- if (ch === "'" || ch === "\"") {
10635
- const quote = ch;
10636
- const start = i;
10637
- i += 1;
10638
- let out = "";
10639
- let closed = false;
10640
- while (i < n) {
10641
- const c = source[i];
10642
- if (c === "\\") {
10643
- const next = i + 1 < n ? source[i + 1] : "";
10644
- if (next === "\\" || next === "'" || next === "\"") {
10645
- out += next;
10646
- i += 2;
10647
- continue;
10648
- }
10649
- throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
10650
- }
10651
- if (c === quote) {
10652
- closed = true;
10653
- i += 1;
10654
- break;
10655
- }
10656
- out += c;
10657
- i += 1;
10658
- }
10659
- if (!closed) throw new ExpressionParseError("unterminated string literal", start);
10660
- tokens.push({
10661
- type: "string",
10662
- value: out,
10663
- pos: start
10664
- });
10665
- continue;
10666
- }
10667
- if (isIdentStart(ch)) {
10668
- const start = i;
10669
- while (i < n && isIdentPart(source[i])) i += 1;
10670
- const text = source.slice(start, i);
10671
- if (KEYWORDS.has(text)) tokens.push({
10672
- type: "keyword",
10673
- keyword: keywordOf(text),
10674
- pos: start
10675
- });
10676
- else tokens.push({
10677
- type: "identifier",
10678
- name: text,
10679
- pos: start
10680
- });
10681
- continue;
10682
- }
10683
- const two = i + 1 < n ? source.slice(i, i + 2) : "";
10684
- if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
10685
- tokens.push({
10686
- type: "punct",
10687
- punct: two,
10688
- pos: i
10689
- });
10690
- i += 2;
10691
- continue;
10692
- }
10693
- if (isSinglePunct(ch)) {
10694
- tokens.push({
10695
- type: "punct",
10696
- punct: ch,
10697
- pos: i
10698
- });
10699
- i += 1;
10700
- continue;
10701
- }
10702
- throw new ExpressionParseError(`unexpected character '${ch}'`, i);
10703
- }
10704
- tokens.push({
10705
- type: "eof",
10706
- pos: n
10707
- });
10708
- return tokens;
10709
- }
10710
- function keywordOf(text) {
10711
- if (text === "true") return "true";
10712
- if (text === "false") return "false";
10713
- return "null";
10714
- }
10715
- function isSinglePunct(ch) {
10716
- return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
10717
- }
10512
+ /** One child-placement directive on a container's `childLayout`. Structurally
10513
+ * identical to `ChildLayoutEntry` in `device-management.ts` the cap wire
10514
+ * shape for the same field. The child is identified by its re-sync-stable
10515
+ * accessory `stableIdSuffix` (`childKey`); listed children are grouped into
10516
+ * named accordion sections (with optional intra-section order). */
10517
+ var ChildLayoutEntrySchema = object({
10518
+ childKey: string(),
10519
+ section: string(),
10520
+ order: number().optional(),
10521
+ collapsed: boolean().optional()
10522
+ });
10523
+ /** Cap-wire shape of a per-cap display refinement — mirrors
10524
+ * `DeviceCapDisplayOverride` in `device-management.ts`. */
10525
+ var DeviceCapDisplayOverrideSchema = object({
10526
+ unit: string().min(1).optional(),
10527
+ precision: number().int().min(0).max(10).optional()
10528
+ });
10529
+ /** Cap-wire shape of an operator-authored per-device display override —
10530
+ * mirrors `DeviceDisplayOverride` in `device-management.ts`. `precision`
10531
+ * bounds mirror `numeric-sensor.cap.ts` (`int 0-10`). */
10532
+ var DeviceDisplayOverrideSchema = object({
10533
+ icon: string().min(1).optional(),
10534
+ label: string().min(1).optional(),
10535
+ unit: string().min(1).optional(),
10536
+ precision: number().int().min(0).max(10).optional(),
10537
+ hidden: boolean().optional(),
10538
+ perCap: record(string(), DeviceCapDisplayOverrideSchema).optional()
10539
+ });
10540
+ /** Cap-wire shape of a per-role display default — mirrors `RoleDisplayDefault`
10541
+ * in `device-management.ts`. Keyed by `DeviceRole` string (role strings cross
10542
+ * the wire as plain strings everywhere else — cf. `DeviceInfoSchema.role`). */
10543
+ var RoleDisplayDefaultSchema = object({
10544
+ unit: string().min(1).optional(),
10545
+ precision: number().int().min(0).max(10).optional(),
10546
+ icon: string().min(1).optional()
10547
+ });
10718
10548
  /**
10719
- * Pratt (precedence-climbing) parser for the safe expression mini-language.
10720
- *
10721
- * Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
10722
- * → relational → additive → multiplicative → unary `! -` → call / primary.
10723
- * Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
10724
- * string validated against the builtin table at parse time, so an unknown
10725
- * function is rejected immediately (author feedback) and a persisted expression
10726
- * that references a since-removed builtin degrades at read.
10727
- *
10728
- * A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
10729
- * arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
10549
+ * Serializable projection of a live IDevice.
10550
+ * Returned by listAll, getDevice, getChildren.
10551
+ * Live methods (getStreamSources, getConfigSchema) are separate calls.
10730
10552
  */
10731
- /** Binary/logical operator precedence (higher binds tighter). */
10732
- var BINARY_PRECEDENCE = {
10733
- "||": 1,
10734
- "&&": 2,
10735
- "==": 3,
10736
- "!=": 3,
10737
- "<": 4,
10738
- "<=": 4,
10739
- ">": 4,
10740
- ">=": 4,
10741
- "+": 5,
10742
- "-": 5,
10743
- "*": 6,
10744
- "/": 6,
10745
- "%": 6
10746
- };
10747
- function isLogicalOp(op) {
10748
- return op === "&&" || op === "||";
10749
- }
10750
- function isBinaryOp(op) {
10751
- return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
10752
- }
10753
- var Parser = class {
10754
- tokens;
10755
- pos = 0;
10756
- nodeCount = 0;
10757
- identifiers = /* @__PURE__ */ new Set();
10758
- callees = /* @__PURE__ */ new Set();
10759
- constructor(tokens) {
10760
- this.tokens = tokens;
10761
- }
10762
- parse() {
10763
- const ast = this.parseTernary();
10764
- const tok = this.peek();
10765
- if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
10766
- return {
10767
- ast,
10768
- identifiers: this.identifiers,
10769
- callees: this.callees,
10770
- nodeCount: this.nodeCount
10771
- };
10772
- }
10773
- peek() {
10774
- return this.tokens[this.pos];
10775
- }
10776
- next() {
10777
- return this.tokens[this.pos++];
10778
- }
10779
- /** Consume a punctuator token, erroring if the next token isn't it. */
10780
- expectPunct(punct) {
10781
- const tok = this.peek();
10782
- if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
10783
- this.pos += 1;
10784
- }
10785
- matchPunct(punct) {
10786
- const tok = this.peek();
10787
- if (tok.type === "punct" && tok.punct === punct) {
10788
- this.pos += 1;
10789
- return true;
10790
- }
10791
- return false;
10792
- }
10793
- countNode() {
10794
- this.nodeCount += 1;
10795
- if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
10796
- }
10797
- parseTernary() {
10798
- const test = this.parseBinary(1);
10799
- if (this.matchPunct("?")) {
10800
- const consequent = this.parseTernary();
10801
- this.expectPunct(":");
10802
- const alternate = this.parseTernary();
10803
- this.countNode();
10804
- return {
10805
- kind: "conditional",
10806
- test,
10807
- consequent,
10808
- alternate
10809
- };
10810
- }
10811
- return test;
10812
- }
10813
- parseBinary(minPrec) {
10814
- let left = this.parseUnary();
10815
- for (;;) {
10816
- const tok = this.peek();
10817
- if (tok.type !== "punct") break;
10818
- const prec = BINARY_PRECEDENCE[tok.punct];
10819
- if (prec === void 0 || prec < minPrec) break;
10820
- const op = tok.punct;
10821
- this.pos += 1;
10822
- const right = this.parseBinary(prec + 1);
10823
- this.countNode();
10824
- if (isLogicalOp(op)) left = {
10825
- kind: "logical",
10826
- op,
10827
- left,
10828
- right
10829
- };
10830
- else if (isBinaryOp(op)) left = {
10831
- kind: "binary",
10832
- op,
10833
- left,
10834
- right
10835
- };
10836
- else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
10837
- }
10838
- return left;
10839
- }
10840
- parseUnary() {
10841
- const tok = this.peek();
10842
- if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
10843
- const op = tok.punct;
10844
- this.pos += 1;
10845
- const operand = this.parseUnary();
10846
- this.countNode();
10847
- return {
10848
- kind: "unary",
10849
- op,
10850
- operand
10851
- };
10852
- }
10853
- return this.parsePrimary();
10854
- }
10855
- parsePrimary() {
10856
- const tok = this.next();
10857
- switch (tok.type) {
10858
- case "number":
10859
- this.countNode();
10860
- return {
10861
- kind: "literal",
10862
- value: tok.value
10863
- };
10864
- case "string":
10865
- this.countNode();
10866
- return {
10867
- kind: "literal",
10868
- value: tok.value
10869
- };
10870
- case "keyword":
10871
- this.countNode();
10872
- return {
10873
- kind: "literal",
10874
- value: tok.keyword === "null" ? null : tok.keyword === "true"
10875
- };
10876
- case "identifier": {
10877
- const nextTok = this.peek();
10878
- if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
10879
- this.identifiers.add(tok.name);
10880
- this.countNode();
10881
- return {
10882
- kind: "identifier",
10883
- name: tok.name
10884
- };
10885
- }
10886
- case "punct":
10887
- if (tok.punct === "(") {
10888
- const inner = this.parseTernary();
10889
- this.expectPunct(")");
10890
- return inner;
10891
- }
10892
- throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
10893
- case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
10894
- }
10895
- }
10896
- parseCall(callee, pos) {
10897
- if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
10898
- this.expectPunct("(");
10899
- const args = [];
10900
- if (!this.matchPunct(")")) for (;;) {
10901
- args.push(this.parseTernary());
10902
- if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
10903
- if (this.matchPunct(",")) continue;
10904
- this.expectPunct(")");
10905
- break;
10906
- }
10907
- this.callees.add(callee);
10908
- this.countNode();
10909
- return {
10910
- kind: "call",
10911
- callee,
10912
- args
10913
- };
10914
- }
10915
- };
10916
- /** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
10917
- * `ExpressionParseError` on any lexical or grammatical failure. */
10918
- function parseExpression(source) {
10919
- return new Parser(tokenize(source)).parse();
10920
- }
10921
- /**
10922
- * LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
10923
- * by expr"). The cache stores BOTH successes and failures (negative caching),
10924
- * so a corrupt persisted string costs exactly one tokenize+parse total — not
10925
- * one per read on a hot resolve path.
10926
- *
10927
- * The cache is a module-level singleton: entries are pure, content-addressed
10928
- * ASTs keyed by the raw source string, so sharing one instance across all
10929
- * callers is safe and maximises hit rate.
10930
- */
10931
- var cache = /* @__PURE__ */ new Map();
10932
- function getCached(source) {
10933
- const hit = cache.get(source);
10934
- if (hit !== void 0) {
10935
- cache.delete(source);
10936
- cache.set(source, hit);
10937
- return hit;
10938
- }
10939
- let result;
10940
- try {
10941
- result = {
10942
- ok: true,
10943
- parsed: parseExpression(source)
10944
- };
10945
- } catch (err) {
10946
- result = {
10947
- ok: false,
10948
- error: err instanceof ExpressionParseError ? err.message : String(err)
10949
- };
10950
- }
10951
- cache.set(source, result);
10952
- if (cache.size > 256) {
10953
- const oldest = cache.keys().next().value;
10954
- if (oldest !== void 0) cache.delete(oldest);
10955
- }
10956
- return result;
10957
- }
10958
- /** Compile `source`, returning a discriminated result instead of throwing.
10959
- * Used by read paths that must degrade rather than raise. LRU/negative-cached. */
10960
- function compileExpressionSafe(source) {
10961
- return getCached(source);
10962
- }
10963
- Object.freeze({});
10964
- /**
10965
- * Author-time validation. Returns `null` when the source is valid, else a
10966
- * human-readable error message. Checks: the expression compiles; binding count
10967
- * is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
10968
- * is not reserved (`now`/keywords) and does not shadow a builtin; and every
10969
- * FREE identifier of the AST is covered by a binding or the injected `now`.
10970
- */
10971
- function validateExpressionSource(src) {
10972
- const names = Object.keys(src.bindings);
10973
- if (names.length > 32) return `too many bindings (${names.length} > 32)`;
10974
- for (const name of names) {
10975
- if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
10976
- if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
10977
- if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
10978
- }
10979
- const compiled = compileExpressionSafe(src.expr);
10980
- if (!compiled.ok) return compiled.error;
10981
- const bound = new Set(names);
10982
- for (const id of compiled.parsed.identifiers) {
10983
- if (id === "now") continue;
10984
- if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
10985
- }
10986
- return null;
10987
- }
10988
- var ProviderStatusSchema = object({
10989
- connected: boolean(),
10990
- deviceCount: number(),
10991
- error: string().optional()
10992
- });
10993
- object({
10994
- externalId: string(),
10995
- name: string(),
10996
- type: string(),
10997
- metadata: record(string(), unknown()).optional()
10998
- });
10999
- /**
11000
- * Candidate handed back from discovery and accepted by
11001
- * `adoptDiscoveredDevice`. Shape mirrors the in-process
11002
- * `DiscoveredDevice` interface used by `DeviceDiscovery`.
11003
- */
11004
- var DiscoveryCandidateSchema = object({
10553
+ var DeviceInfoSchema = object({
10554
+ /** Progressive, system-wide unique number. Allocated synchronously by
10555
+ * `device-manager.allocateDeviceId` BEFORE the owning `IDevice` is
10556
+ * constructed, so every live device exposes an `id` — no transient
10557
+ * null window. Distinct from `stableId`, which is unique per
10558
+ * integration. Ids are monotonic and never reissued on removal. */
10559
+ id: number(),
11005
10560
  stableId: string(),
11006
- type: _enum(DeviceType),
11007
- suggestedName: string(),
11008
- prefilledConfig: record(string(), unknown()),
11009
- /**
11010
- * Optional upstream-system identity (HA entity_id, vendor MAC, …).
11011
- * Discovery pre-populates this for systems that know the upstream
11012
- * identity ahead of adoption. Rendering metadata (unit, precision)
11013
- * flows live through the cap STATUS SLICE after adoption.
11014
- */
11015
- sourceInfo: SourceInfoSchema.optional()
11016
- });
11017
- /**
11018
- * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
11019
- * Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
11020
- * tRPC layer can pass it through without reshaping.
11021
- */
11022
- var DeviceSummarySchema = object({
11023
- id: number(),
11024
- stableId: string(),
11025
- addonId: string(),
11026
- type: string(),
11027
- name: string(),
11028
- parentDeviceId: number().nullable(),
11029
- online: boolean(),
11030
- features: array(string()),
11031
- config: record(string(), unknown()),
11032
- /** Optional upstream-system identity (dispatch key + system tag).
11033
- * See `SourceInfo`. Present when the device has a non-synthetic
11034
- * source identifier (HA entities, vendor MAC, …); omitted when the
11035
- * synthetic backfill is in effect. */
11036
- sourceInfo: SourceInfoSchema.optional()
11037
- });
11038
- /**
11039
- * Result of a live field test (e.g. probing an RTSP URL during device
11040
- * creation). Matches the UI-side `FieldProbeResult` in
11041
- * `interfaces/config-ui.ts` — the admin `FormBuilder` renders the
11042
- * returned `labels` as chips next to the input.
11043
- */
11044
- var FieldProbeResultSchema = object({
11045
- status: _enum(["ok", "error"]),
11046
- labels: array(string()).optional(),
11047
- error: string().optional()
11048
- });
11049
- /**
11050
- * The output of `getChildCreationSchema` is a UI schema tree. We store
11051
- * it as `unknown` at the capability layer — the router just passes it
11052
- * through and the admin UI renders it via `FormBuilder`. The actual
11053
- * type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
11054
- * but we deliberately avoid a Zod mirror because the union is large and
11055
- * not meant for runtime validation at this seam.
11056
- */
11057
- var CreationSchemaOutputSchema = unknown();
11058
- method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), ProviderStatusSchema), method(_void(), array(object({
11059
- id: string(),
11060
- name: string(),
11061
- type: string()
11062
- }))), method(object({}), boolean()), method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
11063
- kind: "mutation",
11064
- auth: "admin"
11065
- }), method(object({}), CreationSchemaOutputSchema), method(object({}), object({ deviceType: _enum(DeviceType).nullable() })), method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
11066
- kind: "mutation",
11067
- auth: "admin"
11068
- }), method(object({}), boolean()), method(object({ type: _enum(DeviceType) }), CreationSchemaOutputSchema), method(object({
11069
- type: _enum(DeviceType),
11070
- config: record(string(), unknown())
11071
- }), DeviceSummarySchema, {
11072
- kind: "mutation",
11073
- auth: "admin"
11074
- }), method(object({
11075
- type: _enum(DeviceType),
11076
- key: string(),
11077
- value: unknown(),
11078
- formValues: record(string(), unknown()).optional()
11079
- }), FieldProbeResultSchema, {
11080
- kind: "mutation",
11081
- auth: "admin"
11082
- });
11083
- /**
11084
- * Device Manager capability — hub-side singleton that unifies device persistence,
11085
- * live registry access, and all management operations into a single tRPC surface.
11086
- *
11087
- * Replaces:
11088
- * - `device-persistence` capability (persistence methods absorbed here)
11089
- * - `device-management.router.ts` (deleted in Phase 2)
11090
- * - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
11091
- *
11092
- * All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
11093
- * fork into separate processes but never run on remote cluster agents. Therefore:
11094
- * - No nodeId routing needed — this is a pure hub singleton.
11095
- * - The hub's DeviceRegistry is the single source of truth for all live devices.
11096
- * - No shadow registry or cross-node aggregation required.
11097
- *
11098
- * Forked workers register devices back to the hub via `ctx.devices`
11099
- * (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
11100
- */
11101
- /** One child-placement directive on a container's `childLayout`. Structurally
11102
- * identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
11103
- * shape for the same field. The child is identified by its re-sync-stable
11104
- * accessory `stableIdSuffix` (`childKey`); listed children are grouped into
11105
- * named accordion sections (with optional intra-section order). */
11106
- var ChildLayoutEntrySchema = object({
11107
- childKey: string(),
11108
- section: string(),
11109
- order: number().optional(),
11110
- collapsed: boolean().optional()
11111
- });
11112
- /** Cap-wire shape of a DeviceLink — structurally mirrors `DeviceLink` in
11113
- * `device-management.ts`. Source is a union: a FIELD source copies a sibling
11114
- * accessory's status field (`kind` optional/absent for wire compat); a
11115
- * LITERAL source carries a per-device constant (no sibling is read); a
11116
- * GLOBAL source (P2e) copies ANY device's status field, addressed by the
11117
- * source device's full re-sync-stable `stableId`. */
11118
- var DeviceLinkFieldSourceSchema = object({
11119
- kind: literal("field").optional(),
11120
- sourceKey: string(),
11121
- cap: string(),
11122
- fieldPath: string()
11123
- });
11124
- var DeviceLinkLiteralSourceSchema = object({
11125
- kind: literal("literal"),
11126
- value: union([
11127
- string(),
11128
- number(),
11129
- boolean(),
11130
- _null()
11131
- ])
11132
- });
11133
- var DeviceLinkGlobalSourceSchema = object({
11134
- kind: literal("global"),
11135
- sourceStableId: string(),
11136
- cap: string(),
11137
- fieldPath: string()
11138
- });
11139
- /** Expression source (Stage X): compute the target field from N named bindings
11140
- * via the safe expression engine. Bindings are field | literal | global — never
11141
- * another expression (no nesting). The `superRefine` runs the SAME author-time
11142
- * validation as `validateExpressionSource` (compiles the expr, checks binding
11143
- * names + identifier coverage) so every wire boundary that parses a DeviceLink
11144
- * (tRPC mount, kernel create pre-seed, projection output) validates-at-write.
11145
- * Compiles are LRU-cached, so repeated validation of the same expr is a hit. */
11146
- var DeviceLinkExpressionSourceSchema = object({
11147
- kind: literal("expression"),
11148
- expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
11149
- bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), union([
11150
- DeviceLinkFieldSourceSchema,
11151
- DeviceLinkLiteralSourceSchema,
11152
- DeviceLinkGlobalSourceSchema
11153
- ]))
11154
- }).superRefine((src, ctx) => {
11155
- const err = validateExpressionSource(src);
11156
- if (err !== null) ctx.addIssue({
11157
- code: "custom",
11158
- message: err,
11159
- path: ["expr"]
11160
- });
11161
- });
11162
- var DeviceLinkSchema = object({
11163
- id: string(),
11164
- source: union([
11165
- DeviceLinkFieldSourceSchema,
11166
- DeviceLinkLiteralSourceSchema,
11167
- DeviceLinkGlobalSourceSchema,
11168
- DeviceLinkExpressionSourceSchema
11169
- ]),
11170
- target: object({
11171
- cap: string(),
11172
- fieldPath: string(),
11173
- itemKey: string().optional()
11174
- }),
11175
- transform: discriminatedUnion("kind", [
11176
- object({ kind: literal("identity") }),
11177
- object({
11178
- kind: literal("enum-map"),
11179
- mapping: record(string(), union([
11180
- string(),
11181
- number(),
11182
- boolean()
11183
- ])),
11184
- fallback: union([
11185
- string(),
11186
- number(),
11187
- boolean()
11188
- ]).optional()
11189
- }),
11190
- object({
11191
- kind: literal("linear"),
11192
- scale: number(),
11193
- offset: number(),
11194
- clamp: tuple([number(), number()]).readonly().optional()
11195
- })
11196
- ]).optional()
11197
- });
11198
- /** Cap-wire shape of a per-cap display refinement — mirrors
11199
- * `DeviceCapDisplayOverride` in `device-management.ts`. */
11200
- var DeviceCapDisplayOverrideSchema = object({
11201
- unit: string().min(1).optional(),
11202
- precision: number().int().min(0).max(10).optional()
11203
- });
11204
- /** Cap-wire shape of an operator-authored per-device display override —
11205
- * mirrors `DeviceDisplayOverride` in `device-management.ts`. `precision`
11206
- * bounds mirror `numeric-sensor.cap.ts` (`int 0-10`). */
11207
- var DeviceDisplayOverrideSchema = object({
11208
- icon: string().min(1).optional(),
11209
- label: string().min(1).optional(),
11210
- unit: string().min(1).optional(),
11211
- precision: number().int().min(0).max(10).optional(),
11212
- hidden: boolean().optional(),
11213
- perCap: record(string(), DeviceCapDisplayOverrideSchema).optional()
11214
- });
11215
- /** Cap-wire shape of a per-role display default — mirrors `RoleDisplayDefault`
11216
- * in `device-management.ts`. Keyed by `DeviceRole` string (role strings cross
11217
- * the wire as plain strings everywhere else — cf. `DeviceInfoSchema.role`). */
11218
- var RoleDisplayDefaultSchema = object({
11219
- unit: string().min(1).optional(),
11220
- precision: number().int().min(0).max(10).optional(),
11221
- icon: string().min(1).optional()
11222
- });
11223
- /**
11224
- * Serializable projection of a live IDevice.
11225
- * Returned by listAll, getDevice, getChildren.
11226
- * Live methods (getStreamSources, getConfigSchema) are separate calls.
11227
- */
11228
- var DeviceInfoSchema = object({
11229
- /** Progressive, system-wide unique number. Allocated synchronously by
11230
- * `device-manager.allocateDeviceId` BEFORE the owning `IDevice` is
11231
- * constructed, so every live device exposes an `id` — no transient
11232
- * null window. Distinct from `stableId`, which is unique per
11233
- * integration. Ids are monotonic and never reissued on removal. */
11234
- id: number(),
11235
- stableId: string(),
11236
- addonId: string(),
10561
+ addonId: string(),
11237
10562
  type: _enum(DeviceType),
11238
10563
  name: string(),
11239
10564
  /** Operator-organisational location label. `null` when unset. */
@@ -11274,8 +10599,6 @@ var DeviceInfoSchema = object({
11274
10599
  * named accordion sections (with optional intra-section order). See
11275
10600
  * `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
11276
10601
  childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
11277
- /** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
11278
- deviceLinks: array(DeviceLinkSchema).readonly().optional(),
11279
10602
  /** Operator-authored per-device display override. See `DeviceMeta.display`. */
11280
10603
  display: DeviceDisplayOverrideSchema.optional()
11281
10604
  });
@@ -11284,7 +10607,7 @@ var ConfigEntrySchema = object({
11284
10607
  value: unknown(),
11285
10608
  description: string().optional()
11286
10609
  });
11287
- var DeviceLinkModeSchema = _enum(["auto", "manual"]);
10610
+ var LinkedDevicesModeSchema = _enum(["auto", "manual"]);
11288
10611
  /** One resolved linked device — the compact projection consumers need. */
11289
10612
  var LinkedDeviceSchema = object({
11290
10613
  deviceId: number(),
@@ -11347,8 +10670,6 @@ var DeviceMetaSchema = object({
11347
10670
  * accordion sections (with optional intra-section order). See
11348
10671
  * `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
11349
10672
  childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
11350
- /** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
11351
- deviceLinks: array(DeviceLinkSchema).readonly().optional(),
11352
10673
  /** Semantic role string (`DeviceRole`) — propagated from the spawn pre-seed.
11353
10674
  * Optional: only present for accessory children that carry a known role. */
11354
10675
  role: string().nullable().optional(),
@@ -11441,12 +10762,6 @@ method(object({
11441
10762
  }), _void(), {
11442
10763
  kind: "mutation",
11443
10764
  auth: "admin"
11444
- }), method(object({
11445
- deviceId: number(),
11446
- deviceLinks: array(DeviceLinkSchema).readonly()
11447
- }), _void(), {
11448
- kind: "mutation",
11449
- auth: "admin"
11450
10765
  }), method(object({
11451
10766
  deviceId: number(),
11452
10767
  display: DeviceDisplayOverrideSchema.nullable()
@@ -11528,7 +10843,7 @@ method(object({
11528
10843
  * shipping 293 rows to find 12. */
11529
10844
  isCamera: boolean().optional()
11530
10845
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
11531
- mode: DeviceLinkModeSchema,
10846
+ mode: LinkedDevicesModeSchema,
11532
10847
  devices: array(LinkedDeviceSchema)
11533
10848
  })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
11534
10849
  deviceId: number(),
@@ -11561,11 +10876,7 @@ method(object({
11561
10876
  deviceId: number(),
11562
10877
  entries: array(object({
11563
10878
  capName: string(),
11564
- kind: _enum([
11565
- "native",
11566
- "wrapped",
11567
- "linked"
11568
- ]),
10879
+ kind: _enum(["native", "wrapped"]),
11569
10880
  providerAddonId: string(),
11570
10881
  providerNodeId: string(),
11571
10882
  nativeAddonId: string()
@@ -11574,11 +10885,7 @@ method(object({
11574
10885
  deviceId: number(),
11575
10886
  entries: array(object({
11576
10887
  capName: string(),
11577
- kind: _enum([
11578
- "native",
11579
- "wrapped",
11580
- "linked"
11581
- ]),
10888
+ kind: _enum(["native", "wrapped"]),
11582
10889
  providerAddonId: string(),
11583
10890
  providerNodeId: string(),
11584
10891
  nativeAddonId: string()
@@ -12381,7 +11688,7 @@ var MotionAnalysisResultSchema = object({
12381
11688
  frameHeight: number(),
12382
11689
  analysisMs: number()
12383
11690
  });
12384
- method(object({
11691
+ DeviceType.Camera, method(object({
12385
11692
  deviceId: number(),
12386
11693
  frame: FrameInputSchema.optional(),
12387
11694
  frameHandle: FrameHandleSchema.optional()
@@ -14570,6 +13877,18 @@ var OauthIntegrationDescriptorSchema = object({
14570
13877
  * redirect_uri that does not start with one of these. Required —
14571
13878
  * an empty list means the integration can never complete linking. */
14572
13879
  allowedRedirectPrefixes: array(string()).min(1),
13880
+ /** Paths accepted as a `redirect_uri` when the host is PRIVATE — loopback,
13881
+ * RFC1918, CGNAT (100.64/10, Tailscale), link-local, IPv6 ULA, or an
13882
+ * `.local` / `.internal` / `.ts.net` name. Exists for self-hosted clients
13883
+ * whose address the hub cannot know in advance (a Home Assistant at
13884
+ * `http://<lan-ip>:8123/auth/external/callback`). The PATH must match
13885
+ * exactly; a public host never satisfies this branch, so it is not a
13886
+ * wildcard prefix by another name. */
13887
+ allowedPrivateHostPaths: array(string()).optional(),
13888
+ /** When true this is a PUBLIC client (source is published, no secret can be
13889
+ * protected) and PKCE is mandatory: `/authorize` refuses without an S256
13890
+ * `code_challenge`, `/token` refuses without the matching `code_verifier`. */
13891
+ requiresPkce: boolean().optional(),
14573
13892
  /** Optional public origin (no trailing slash) that this integration's
14574
13893
  * issued codes/tokens should carry as the `hubUrl` claim — typically the
14575
13894
  * operator-selected external-access endpoint resolved by the addon. When
@@ -14720,7 +14039,7 @@ var TrackEnvelopeSchema = object({
14720
14039
  * `snapshots[]` references — megabytes across a page of tracks. `slim`
14721
14040
  * keeps every scalar the list surfaces actually render (ids, class(es),
14722
14041
  * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
14723
- * zonesVisited, bestEventId, envelope) and returns `positions` /
14042
+ * zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
14724
14043
  * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
14725
14044
  * `getTrack`. Mirrors the event-store `projection` convention
14726
14045
  * (`getObjectEvents` et al.).
@@ -14957,6 +14276,24 @@ var TrackSchema = object({
14957
14276
  * Populated from the persisted envelope columns on historical reads;
14958
14277
  * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
14959
14278
  envelope: TrackEnvelopeSchema.optional(),
14279
+ /**
14280
+ * A face DETECTOR found a face on this track — nothing more. It says the
14281
+ * detail plane produced a `face` detail; it does NOT say the face was
14282
+ * embedded, matched, above `minFacePx`, or that the recognizer was even
14283
+ * enabled. Set once and never cleared.
14284
+ *
14285
+ * **This exists so "face present but not recognised" is expressible.** A
14286
+ * recognised identity lands in `subLabel` (attributed to the face chain via
14287
+ * `subLabelMeta.stepId`), so before this field a track with an unmatched face
14288
+ * and a track with no face at all were byte-identical on the wire and no
14289
+ * surface could tell them apart. The read is `hasFace === true && subLabel
14290
+ * === undefined`.
14291
+ *
14292
+ * **Absent ≠ false.** Every row written before the column existed omits it,
14293
+ * and so does every server that predates the field — a consumer must test
14294
+ * `=== true` and render nothing otherwise, never infer "no face".
14295
+ */
14296
+ hasFace: boolean().optional(),
14960
14297
  ...TrackFlagFields,
14961
14298
  ...TrackRetrainFields
14962
14299
  });
@@ -17571,6 +16908,10 @@ var SsoBridgeClaimsSchema = object({
17571
16908
  integrationId: string().optional(),
17572
16909
  /** JWT ID — unique per issued code; consumed-set enforces single-use. */
17573
16910
  jti: string().optional(),
16911
+ /** PKCE S256 challenge — set only on `oauth-code` tokens issued to a public
16912
+ * client. Its PRESENCE is what makes the verifier mandatory at exchange,
16913
+ * so the requirement travels with the code and not with mutable config. */
16914
+ codeChallenge: string().optional(),
17574
16915
  /** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
17575
16916
  * tokens so the verify path can check the session is not revoked. */
17576
16917
  sessionId: string().optional()
@@ -18123,7 +17464,7 @@ var ClipPlaybackSchema = object({
18123
17464
  playbackEndpoints: array(string()).optional(),
18124
17465
  token: string().optional()
18125
17466
  });
18126
- method(object({
17467
+ DeviceType.Camera, method(object({
18127
17468
  deviceId: number(),
18128
17469
  since: number(),
18129
17470
  until: number(),
@@ -23199,13 +22540,18 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
23199
22540
  username: string(),
23200
22541
  scopes: array(TokenScopeSchema),
23201
22542
  redirectUri: string(),
23202
- hubUrl: string()
22543
+ hubUrl: string(),
22544
+ /** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
22545
+ * that carries one can ONLY be exchanged with the matching verifier. */
22546
+ codeChallenge: string().optional()
23203
22547
  }), object({ code: string() }), {
23204
22548
  kind: "mutation",
23205
22549
  access: "create"
23206
22550
  }), method(object({
23207
22551
  code: string(),
23208
- redirectUri: string()
22552
+ redirectUri: string(),
22553
+ /** PKCE verifier. REQUIRED when the code carries a challenge. */
22554
+ codeVerifier: string().optional()
23209
22555
  }), object({
23210
22556
  accessToken: string(),
23211
22557
  refreshToken: string(),
@@ -23432,189 +22778,818 @@ object({
23432
22778
  });
23433
22779
  DeviceType.Weather;
23434
22780
  /**
23435
- * Per-zone occupancy aggregation produced by the analytics frame
23436
- * processor on every inference result. Covers the full combinatorial
23437
- * matrix the operator UI needs: total objects everywhere, total
23438
- * objects per zone, single class everywhere, single class per zone,
23439
- * objects outside any zone.
22781
+ * Per-zone occupancy aggregation produced by the analytics frame
22782
+ * processor on every inference result. Covers the full combinatorial
22783
+ * matrix the operator UI needs: total objects everywhere, total
22784
+ * objects per zone, single class everywhere, single class per zone,
22785
+ * objects outside any zone.
22786
+ *
22787
+ * Counts are derived from the analytics tracker (tracked detections
22788
+ * with stable trackIds), not raw detector hits — this filters out
22789
+ * one-off detector flickers and gives counts that match what the user
22790
+ * sees on the live overlay.
22791
+ *
22792
+ * Overlap policy: a detection that intersects two zones counts in
22793
+ * BOTH zones' `byClass` and `totalObjects` (count-in-each). The
22794
+ * `frame` aggregate de-duplicates trivially since it's frame-wide.
22795
+ * `unzoned` counts only detections that landed in zero zones.
22796
+ */
22797
+ var PerScopeBreakdownSchema = object({
22798
+ /** Total tracked objects in this scope (frame / zone / unzoned). */
22799
+ totalObjects: number().int().nonnegative(),
22800
+ /** Per-class count. Keys are macro class names (e.g. `person`, `car`). */
22801
+ byClass: record(string(), number().int().nonnegative())
22802
+ });
22803
+ var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
22804
+ zoneId: string(),
22805
+ zoneName: string(),
22806
+ /** TrackIds of objects currently inside this zone — for cross-reference
22807
+ * with the per-track detail panel and live overlay. */
22808
+ trackIds: array(string()).readonly()
22809
+ });
22810
+ /**
22811
+ * A parked ("stationary") object surfaced alongside occupancy — an object that
22812
+ * settled and stopped moving. It is NO LONGER a tracked object (the tracker was
22813
+ * told to forget it so it stops re-spawning tracks/events), but it IS still
22814
+ * physically present, so it keeps counting toward `frame` occupancy and is
22815
+ * listed here so the UI can show it in a dedicated "Stationary" section instead
22816
+ * of flooding the live event feed.
22817
+ */
22818
+ var StationaryObjectSchema = object({
22819
+ id: string(),
22820
+ className: string(),
22821
+ bbox: object({
22822
+ x: number(),
22823
+ y: number(),
22824
+ w: number(),
22825
+ h: number()
22826
+ }),
22827
+ frameWidth: number().int().nonnegative(),
22828
+ frameHeight: number().int().nonnegative(),
22829
+ /** When the source track was first seen. */
22830
+ firstSeenAt: number().int(),
22831
+ /** When the object was recognised as parked (promotion time). */
22832
+ becameStationaryAt: number().int(),
22833
+ /** Last frame a detection confirmed the object is still there. */
22834
+ lastConfirmedAt: number().int(),
22835
+ /** Enrichment label carried from the source track (identity / plate). */
22836
+ label: string().optional(),
22837
+ /** Native-resolution key-frame media key for the parked object's best image. */
22838
+ keyFrameMediaKey: string().optional()
22839
+ });
22840
+ var CameraOccupancySnapshotSchema = object({
22841
+ /** Frame timestamp of the inference result that produced this snapshot. */
22842
+ ts: number().int(),
22843
+ /** Frame width/height in pixels — let the UI normalize bbox coords. */
22844
+ frameWidth: number().int().nonnegative(),
22845
+ frameHeight: number().int().nonnegative(),
22846
+ /** Per-zone breakdown — one entry per defined zone (user + onboard). */
22847
+ zones: array(ZoneScopeBreakdownSchema).readonly(),
22848
+ /** Frame-wide aggregate (everywhere, regardless of zone membership).
22849
+ * INCLUDES currently-confirmed stationary objects (they are still present). */
22850
+ frame: PerScopeBreakdownSchema,
22851
+ /** Detections that landed outside every zone. Empty when no zones defined. */
22852
+ unzoned: PerScopeBreakdownSchema,
22853
+ /** Parked objects on this camera (additive — absent on legacy snapshots).
22854
+ * Surfaced separately so the UI shows them in a dedicated section rather
22855
+ * than as repeated tracks/events. */
22856
+ stationaryObjects: array(StationaryObjectSchema).readonly().optional()
22857
+ });
22858
+ /**
22859
+ * Time-series resolution. The history methods return one bucket per
22860
+ * step over the requested range. Smaller resolutions cost more
22861
+ * memory + bandwidth; bound to discrete steps so caller cannot ask
22862
+ * for arbitrary fractional buckets.
22863
+ */
22864
+ var HistoryResolutionEnum = _enum([
22865
+ "minute",
22866
+ "5min",
22867
+ "hour"
22868
+ ]);
22869
+ var HistoryRangeSchema = object({
22870
+ /** Range start (epoch ms, inclusive). */
22871
+ from: number().int(),
22872
+ /** Range end (epoch ms, inclusive). Defaults to "now" at query time. */
22873
+ to: number().int(),
22874
+ resolution: HistoryResolutionEnum
22875
+ });
22876
+ var HistoryPointSchema = object({
22877
+ /** Bucket midpoint (epoch ms). */
22878
+ ts: number().int(),
22879
+ /** Object count averaged over the bucket (rounded to nearest integer). */
22880
+ count: number().int().nonnegative()
22881
+ });
22882
+ DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
22883
+ deviceId: number(),
22884
+ zoneId: string(),
22885
+ className: string().optional()
22886
+ }).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
22887
+ deviceId: number(),
22888
+ className: string().optional()
22889
+ }).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
22890
+ deviceId: number(),
22891
+ className: string().optional()
22892
+ }).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly());
22893
+ /**
22894
+ * Stages a {@link ZoneRule} can apply to. Discriminator on the rules
22895
+ * cap so a single CRUD surface backs every consumer; each stage has
22896
+ * its own dev-state mirror slice (`motion-zone-rules`,
22897
+ * `detection-zone-rules`, …) so consumer addons subscribe independently.
22898
+ *
22899
+ * Extend the enum here when a new gating consumer comes online (audio
22900
+ * gating, alert filtering, …) — no other surface needs to change.
22901
+ */
22902
+ var ZoneRuleStageEnum = _enum([
22903
+ "motion",
22904
+ "detection",
22905
+ "package"
22906
+ ]);
22907
+ DeviceType.Camera, method(object({
22908
+ deviceId: number(),
22909
+ stage: ZoneRuleStageEnum
22910
+ }), array(ZoneRuleSchema).readonly()), method(object({
22911
+ deviceId: number(),
22912
+ stage: ZoneRuleStageEnum,
22913
+ rules: array(ZoneRuleSchema).readonly()
22914
+ }), _void(), {
22915
+ kind: "mutation",
22916
+ auth: "admin"
22917
+ }), object({
22918
+ motion: array(ZoneRuleSchema).readonly(),
22919
+ detection: array(ZoneRuleSchema).readonly(),
22920
+ package: array(ZoneRuleSchema).readonly()
22921
+ });
22922
+ /**
22923
+ * Accessory device helpers — shared across drivers.
22924
+ *
22925
+ * Many vendor-specific drivers register accessory child devices on
22926
+ * top of a parent (Reolink: siren / floodlight / PIR / autotrack /
22927
+ * chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
22928
+ * driver picks the right `DeviceType` + `DeviceRole` explicitly when
22929
+ * spawning, builds a name derived from the parent, and produces a
22930
+ * stableId tied to the parent so boot-restore can reconstruct the
22931
+ * relationship.
22932
+ *
22933
+ * Centralised `(kind → DeviceType)` mapping was dropped on purpose:
22934
+ * drivers may reasonably disagree on the right type for an accessory
22935
+ * (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
22936
+ * read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
22937
+ * one canonical mapping was over-prescriptive and added a layer of
22938
+ * indirection without saving meaningful code at call sites — the
22939
+ * driver knows its own hardware best.
22940
+ */
22941
+ /**
22942
+ * Subset of `DeviceRole` values that drivers register as child
22943
+ * accessories of a parent device. Sourced verbatim from `DeviceRole`
22944
+ * — `AccessoryKind` is the alias drivers use when building accessory
22945
+ * children, so the call site reads as
22946
+ * `accessoryStableId(parent, AccessoryKind.Siren)` rather than
22947
+ * `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
22948
+ * any role works, including non-accessory ones like Doorbell).
22949
+ */
22950
+ var AccessoryKind = {
22951
+ Siren: DeviceRole.Siren,
22952
+ Floodlight: DeviceRole.Floodlight,
22953
+ Spotlight: DeviceRole.Spotlight,
22954
+ PirSensor: DeviceRole.PirSensor,
22955
+ Chime: DeviceRole.Chime,
22956
+ Autotrack: DeviceRole.Autotrack,
22957
+ Nightvision: DeviceRole.Nightvision,
22958
+ PrivacyMask: DeviceRole.PrivacyMask
22959
+ };
22960
+ AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
22961
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
22962
+ new Set(Object.values(DeviceType));
22963
+ DeviceFeature.BatteryOperated;
22964
+ /**
22965
+ * Error types for the safe expression engine. Two distinct classes so callers
22966
+ * can tell a compile-time (grammar) failure from a runtime (evaluation)
22967
+ * failure — both are non-fatal to the host: read paths degrade to "skip link".
22968
+ */
22969
+ /** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
22970
+ * the failure is anchored to a character (author-facing inline feedback). */
22971
+ var ExpressionParseError = class extends Error {
22972
+ position;
22973
+ constructor(message, position) {
22974
+ super(message);
22975
+ this.name = "ExpressionParseError";
22976
+ this.position = position;
22977
+ }
22978
+ };
22979
+ /** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
22980
+ * result, unknown builtin, step-budget exceeded). */
22981
+ var ExpressionEvalError = class extends Error {
22982
+ constructor(message) {
22983
+ super(message);
22984
+ this.name = "ExpressionEvalError";
22985
+ }
22986
+ };
22987
+ /**
22988
+ * Frozen, null-prototype builtin function table for the expression engine
22989
+ * (spec §4 rule 4). The table is the SOLE surface of callable functions: the
22990
+ * parser rejects any callee not in it, and the evaluator gates each call on an
22991
+ * own-property check against it.
22992
+ *
22993
+ * Because the object has a NULL prototype AND is `Object.freeze`d:
22994
+ * - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
22995
+ * - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
22996
+ * (there is no `Object.prototype` in the chain), so those names are not
22997
+ * callable — they are simply "unknown function" at parse time.
22998
+ *
22999
+ * Every numeric argument is validated as a finite number and every numeric
23000
+ * RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
23001
+ * (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
23002
+ * closed rather than emitting a garbage value.
23003
+ */
23004
+ function asFiniteNumber(value, name, index) {
23005
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
23006
+ return value;
23007
+ }
23008
+ function asString$1(value, name, index) {
23009
+ if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
23010
+ return value;
23011
+ }
23012
+ function finiteResult(value, name) {
23013
+ if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
23014
+ return value;
23015
+ }
23016
+ function allFiniteNumbers(args, name) {
23017
+ return args.map((a, idx) => asFiniteNumber(a, name, idx));
23018
+ }
23019
+ var INF = Number.POSITIVE_INFINITY;
23020
+ var table = {
23021
+ min: {
23022
+ minArgs: 1,
23023
+ maxArgs: INF,
23024
+ apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
23025
+ },
23026
+ max: {
23027
+ minArgs: 1,
23028
+ maxArgs: INF,
23029
+ apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
23030
+ },
23031
+ abs: {
23032
+ minArgs: 1,
23033
+ maxArgs: 1,
23034
+ apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
23035
+ },
23036
+ floor: {
23037
+ minArgs: 1,
23038
+ maxArgs: 1,
23039
+ apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
23040
+ },
23041
+ ceil: {
23042
+ minArgs: 1,
23043
+ maxArgs: 1,
23044
+ apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
23045
+ },
23046
+ sqrt: {
23047
+ minArgs: 1,
23048
+ maxArgs: 1,
23049
+ apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
23050
+ },
23051
+ round: {
23052
+ minArgs: 1,
23053
+ maxArgs: 2,
23054
+ apply: (args) => {
23055
+ const x = asFiniteNumber(args[0], "round", 0);
23056
+ const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
23057
+ if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
23058
+ const factor = 10 ** digits;
23059
+ return finiteResult(Math.round(x * factor) / factor, "round");
23060
+ }
23061
+ },
23062
+ pow: {
23063
+ minArgs: 2,
23064
+ maxArgs: 2,
23065
+ apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
23066
+ },
23067
+ clamp: {
23068
+ minArgs: 3,
23069
+ maxArgs: 3,
23070
+ apply: (args) => {
23071
+ const x = asFiniteNumber(args[0], "clamp", 0);
23072
+ const lo = asFiniteNumber(args[1], "clamp", 1);
23073
+ const hi = asFiniteNumber(args[2], "clamp", 2);
23074
+ if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
23075
+ return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
23076
+ }
23077
+ },
23078
+ avg: {
23079
+ minArgs: 1,
23080
+ maxArgs: INF,
23081
+ apply: (args) => {
23082
+ const nums = allFiniteNumbers(args, "avg");
23083
+ return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
23084
+ }
23085
+ },
23086
+ sum: {
23087
+ minArgs: 1,
23088
+ maxArgs: INF,
23089
+ apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
23090
+ },
23091
+ coalesce: {
23092
+ minArgs: 1,
23093
+ maxArgs: INF,
23094
+ apply: (args) => {
23095
+ for (const a of args) if (a !== null) return a;
23096
+ return null;
23097
+ }
23098
+ },
23099
+ age: {
23100
+ minArgs: 2,
23101
+ maxArgs: 2,
23102
+ apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
23103
+ },
23104
+ convert: {
23105
+ minArgs: 3,
23106
+ maxArgs: 3,
23107
+ apply: (args, hooks) => {
23108
+ const x = asFiniteNumber(args[0], "convert", 0);
23109
+ const from = asString$1(args[1], "convert", 1).trim();
23110
+ const to = asString$1(args[2], "convert", 2).trim();
23111
+ if (hooks.convert) {
23112
+ const out = hooks.convert(x, from, to);
23113
+ if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
23114
+ return finiteResult(out, "convert");
23115
+ }
23116
+ if (from === to) return x;
23117
+ throw new ExpressionEvalError("convert: unit conversion table not installed");
23118
+ }
23119
+ }
23120
+ };
23121
+ Object.freeze(Object.assign(Object.create(null), table));
23122
+ /** The set of valid builtin names — used by the parser to reject unknown
23123
+ * callees at parse time (immediate author feedback). */
23124
+ var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
23125
+ /**
23126
+ * Resource-bound constants for the safe expression engine.
23127
+ *
23128
+ * Every bound is defense-in-depth: the grammar is non-Turing-complete (no
23129
+ * loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
23130
+ * O(nodeCount) by construction. These caps merely put a hard ceiling on the
23131
+ * work a single author-supplied expression can request, so a hostile or
23132
+ * accidental pathological string can never spend unbounded CPU/memory.
23133
+ */
23134
+ /** Max source length (chars) — checked BEFORE tokenizing so a huge string is
23135
+ * rejected without allocation. */
23136
+ var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
23137
+ /** A legal binding / identifier name. */
23138
+ var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
23139
+ /** Binding names an author may NOT use: `now` is auto-injected; the literal
23140
+ * keywords lex as values, not identifiers, so binding to them is meaningless. */
23141
+ var RESERVED_BINDING_NAMES = new Set([
23142
+ "now",
23143
+ "true",
23144
+ "false",
23145
+ "null"
23146
+ ]);
23147
+ /**
23148
+ * Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
23149
+ * zero-dependency. The grammar is deliberately boring: decimal numbers,
23150
+ * single/double-quoted strings with a tiny escape set, identifiers, the three
23151
+ * value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
23152
+ * outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
23153
+ * is a parse error with a source position, so member access / assignment /
23154
+ * template literals are lexically impossible.
23155
+ */
23156
+ var KEYWORDS = new Set([
23157
+ "true",
23158
+ "false",
23159
+ "null"
23160
+ ]);
23161
+ function isDigit(ch) {
23162
+ return ch >= "0" && ch <= "9";
23163
+ }
23164
+ function isIdentStart(ch) {
23165
+ return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
23166
+ }
23167
+ function isIdentPart(ch) {
23168
+ return isIdentStart(ch) || isDigit(ch);
23169
+ }
23170
+ function isWhitespace(ch) {
23171
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
23172
+ }
23173
+ /** Tokenize `source` into a flat token list ending with a single `eof` token.
23174
+ * Throws `ExpressionParseError` on any illegal character or unterminated
23175
+ * string. */
23176
+ function tokenize(source) {
23177
+ if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
23178
+ const tokens = [];
23179
+ let i = 0;
23180
+ const n = source.length;
23181
+ while (i < n) {
23182
+ const ch = source[i];
23183
+ if (isWhitespace(ch)) {
23184
+ i += 1;
23185
+ continue;
23186
+ }
23187
+ if (isDigit(ch)) {
23188
+ const start = i;
23189
+ while (i < n && isDigit(source[i])) i += 1;
23190
+ if (i < n && source[i] === ".") {
23191
+ if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
23192
+ i += 1;
23193
+ while (i < n && isDigit(source[i])) i += 1;
23194
+ }
23195
+ const text = source.slice(start, i);
23196
+ const value = Number(text);
23197
+ if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
23198
+ tokens.push({
23199
+ type: "number",
23200
+ value,
23201
+ pos: start
23202
+ });
23203
+ continue;
23204
+ }
23205
+ if (ch === "'" || ch === "\"") {
23206
+ const quote = ch;
23207
+ const start = i;
23208
+ i += 1;
23209
+ let out = "";
23210
+ let closed = false;
23211
+ while (i < n) {
23212
+ const c = source[i];
23213
+ if (c === "\\") {
23214
+ const next = i + 1 < n ? source[i + 1] : "";
23215
+ if (next === "\\" || next === "'" || next === "\"") {
23216
+ out += next;
23217
+ i += 2;
23218
+ continue;
23219
+ }
23220
+ throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
23221
+ }
23222
+ if (c === quote) {
23223
+ closed = true;
23224
+ i += 1;
23225
+ break;
23226
+ }
23227
+ out += c;
23228
+ i += 1;
23229
+ }
23230
+ if (!closed) throw new ExpressionParseError("unterminated string literal", start);
23231
+ tokens.push({
23232
+ type: "string",
23233
+ value: out,
23234
+ pos: start
23235
+ });
23236
+ continue;
23237
+ }
23238
+ if (isIdentStart(ch)) {
23239
+ const start = i;
23240
+ while (i < n && isIdentPart(source[i])) i += 1;
23241
+ const text = source.slice(start, i);
23242
+ if (KEYWORDS.has(text)) tokens.push({
23243
+ type: "keyword",
23244
+ keyword: keywordOf(text),
23245
+ pos: start
23246
+ });
23247
+ else tokens.push({
23248
+ type: "identifier",
23249
+ name: text,
23250
+ pos: start
23251
+ });
23252
+ continue;
23253
+ }
23254
+ const two = i + 1 < n ? source.slice(i, i + 2) : "";
23255
+ if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
23256
+ tokens.push({
23257
+ type: "punct",
23258
+ punct: two,
23259
+ pos: i
23260
+ });
23261
+ i += 2;
23262
+ continue;
23263
+ }
23264
+ if (isSinglePunct(ch)) {
23265
+ tokens.push({
23266
+ type: "punct",
23267
+ punct: ch,
23268
+ pos: i
23269
+ });
23270
+ i += 1;
23271
+ continue;
23272
+ }
23273
+ throw new ExpressionParseError(`unexpected character '${ch}'`, i);
23274
+ }
23275
+ tokens.push({
23276
+ type: "eof",
23277
+ pos: n
23278
+ });
23279
+ return tokens;
23280
+ }
23281
+ function keywordOf(text) {
23282
+ if (text === "true") return "true";
23283
+ if (text === "false") return "false";
23284
+ return "null";
23285
+ }
23286
+ function isSinglePunct(ch) {
23287
+ return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
23288
+ }
23289
+ /**
23290
+ * Pratt (precedence-climbing) parser for the safe expression mini-language.
23440
23291
  *
23441
- * Counts are derived from the analytics tracker (tracked detections
23442
- * with stable trackIds), not raw detector hits this filters out
23443
- * one-off detector flickers and gives counts that match what the user
23444
- * sees on the live overlay.
23292
+ * Precedence (low high): ternary `?:` (right-assoc) → `||` → `&&` → equality
23293
+ * relational additive multiplicative unary `! -` → call / primary.
23294
+ * Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
23295
+ * string validated against the builtin table at parse time, so an unknown
23296
+ * function is rejected immediately (author feedback) and a persisted expression
23297
+ * that references a since-removed builtin degrades at read.
23298
+ *
23299
+ * A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
23300
+ * arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
23301
+ */
23302
+ /** Binary/logical operator precedence (higher binds tighter). */
23303
+ var BINARY_PRECEDENCE = {
23304
+ "||": 1,
23305
+ "&&": 2,
23306
+ "==": 3,
23307
+ "!=": 3,
23308
+ "<": 4,
23309
+ "<=": 4,
23310
+ ">": 4,
23311
+ ">=": 4,
23312
+ "+": 5,
23313
+ "-": 5,
23314
+ "*": 6,
23315
+ "/": 6,
23316
+ "%": 6
23317
+ };
23318
+ function isLogicalOp(op) {
23319
+ return op === "&&" || op === "||";
23320
+ }
23321
+ function isBinaryOp(op) {
23322
+ return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
23323
+ }
23324
+ var Parser = class {
23325
+ tokens;
23326
+ pos = 0;
23327
+ nodeCount = 0;
23328
+ identifiers = /* @__PURE__ */ new Set();
23329
+ callees = /* @__PURE__ */ new Set();
23330
+ constructor(tokens) {
23331
+ this.tokens = tokens;
23332
+ }
23333
+ parse() {
23334
+ const ast = this.parseTernary();
23335
+ const tok = this.peek();
23336
+ if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
23337
+ return {
23338
+ ast,
23339
+ identifiers: this.identifiers,
23340
+ callees: this.callees,
23341
+ nodeCount: this.nodeCount
23342
+ };
23343
+ }
23344
+ peek() {
23345
+ return this.tokens[this.pos];
23346
+ }
23347
+ next() {
23348
+ return this.tokens[this.pos++];
23349
+ }
23350
+ /** Consume a punctuator token, erroring if the next token isn't it. */
23351
+ expectPunct(punct) {
23352
+ const tok = this.peek();
23353
+ if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
23354
+ this.pos += 1;
23355
+ }
23356
+ matchPunct(punct) {
23357
+ const tok = this.peek();
23358
+ if (tok.type === "punct" && tok.punct === punct) {
23359
+ this.pos += 1;
23360
+ return true;
23361
+ }
23362
+ return false;
23363
+ }
23364
+ countNode() {
23365
+ this.nodeCount += 1;
23366
+ if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
23367
+ }
23368
+ parseTernary() {
23369
+ const test = this.parseBinary(1);
23370
+ if (this.matchPunct("?")) {
23371
+ const consequent = this.parseTernary();
23372
+ this.expectPunct(":");
23373
+ const alternate = this.parseTernary();
23374
+ this.countNode();
23375
+ return {
23376
+ kind: "conditional",
23377
+ test,
23378
+ consequent,
23379
+ alternate
23380
+ };
23381
+ }
23382
+ return test;
23383
+ }
23384
+ parseBinary(minPrec) {
23385
+ let left = this.parseUnary();
23386
+ for (;;) {
23387
+ const tok = this.peek();
23388
+ if (tok.type !== "punct") break;
23389
+ const prec = BINARY_PRECEDENCE[tok.punct];
23390
+ if (prec === void 0 || prec < minPrec) break;
23391
+ const op = tok.punct;
23392
+ this.pos += 1;
23393
+ const right = this.parseBinary(prec + 1);
23394
+ this.countNode();
23395
+ if (isLogicalOp(op)) left = {
23396
+ kind: "logical",
23397
+ op,
23398
+ left,
23399
+ right
23400
+ };
23401
+ else if (isBinaryOp(op)) left = {
23402
+ kind: "binary",
23403
+ op,
23404
+ left,
23405
+ right
23406
+ };
23407
+ else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
23408
+ }
23409
+ return left;
23410
+ }
23411
+ parseUnary() {
23412
+ const tok = this.peek();
23413
+ if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
23414
+ const op = tok.punct;
23415
+ this.pos += 1;
23416
+ const operand = this.parseUnary();
23417
+ this.countNode();
23418
+ return {
23419
+ kind: "unary",
23420
+ op,
23421
+ operand
23422
+ };
23423
+ }
23424
+ return this.parsePrimary();
23425
+ }
23426
+ parsePrimary() {
23427
+ const tok = this.next();
23428
+ switch (tok.type) {
23429
+ case "number":
23430
+ this.countNode();
23431
+ return {
23432
+ kind: "literal",
23433
+ value: tok.value
23434
+ };
23435
+ case "string":
23436
+ this.countNode();
23437
+ return {
23438
+ kind: "literal",
23439
+ value: tok.value
23440
+ };
23441
+ case "keyword":
23442
+ this.countNode();
23443
+ return {
23444
+ kind: "literal",
23445
+ value: tok.keyword === "null" ? null : tok.keyword === "true"
23446
+ };
23447
+ case "identifier": {
23448
+ const nextTok = this.peek();
23449
+ if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
23450
+ this.identifiers.add(tok.name);
23451
+ this.countNode();
23452
+ return {
23453
+ kind: "identifier",
23454
+ name: tok.name
23455
+ };
23456
+ }
23457
+ case "punct":
23458
+ if (tok.punct === "(") {
23459
+ const inner = this.parseTernary();
23460
+ this.expectPunct(")");
23461
+ return inner;
23462
+ }
23463
+ throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
23464
+ case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
23465
+ }
23466
+ }
23467
+ parseCall(callee, pos) {
23468
+ if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
23469
+ this.expectPunct("(");
23470
+ const args = [];
23471
+ if (!this.matchPunct(")")) for (;;) {
23472
+ args.push(this.parseTernary());
23473
+ if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
23474
+ if (this.matchPunct(",")) continue;
23475
+ this.expectPunct(")");
23476
+ break;
23477
+ }
23478
+ this.callees.add(callee);
23479
+ this.countNode();
23480
+ return {
23481
+ kind: "call",
23482
+ callee,
23483
+ args
23484
+ };
23485
+ }
23486
+ };
23487
+ /** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
23488
+ * `ExpressionParseError` on any lexical or grammatical failure. */
23489
+ function parseExpression(source) {
23490
+ return new Parser(tokenize(source)).parse();
23491
+ }
23492
+ /**
23493
+ * LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
23494
+ * by expr"). The cache stores BOTH successes and failures (negative caching),
23495
+ * so a corrupt persisted string costs exactly one tokenize+parse total — not
23496
+ * one per read on a hot resolve path.
23445
23497
  *
23446
- * Overlap policy: a detection that intersects two zones counts in
23447
- * BOTH zones' `byClass` and `totalObjects` (count-in-each). The
23448
- * `frame` aggregate de-duplicates trivially since it's frame-wide.
23449
- * `unzoned` counts only detections that landed in zero zones.
23498
+ * The cache is a module-level singleton: entries are pure, content-addressed
23499
+ * ASTs keyed by the raw source string, so sharing one instance across all
23500
+ * callers is safe and maximises hit rate.
23450
23501
  */
23451
- var PerScopeBreakdownSchema = object({
23452
- /** Total tracked objects in this scope (frame / zone / unzoned). */
23453
- totalObjects: number().int().nonnegative(),
23454
- /** Per-class count. Keys are macro class names (e.g. `person`, `car`). */
23455
- byClass: record(string(), number().int().nonnegative())
23456
- });
23457
- var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
23458
- zoneId: string(),
23459
- zoneName: string(),
23460
- /** TrackIds of objects currently inside this zone — for cross-reference
23461
- * with the per-track detail panel and live overlay. */
23462
- trackIds: array(string()).readonly()
23463
- });
23502
+ var cache = /* @__PURE__ */ new Map();
23503
+ function getCached(source) {
23504
+ const hit = cache.get(source);
23505
+ if (hit !== void 0) {
23506
+ cache.delete(source);
23507
+ cache.set(source, hit);
23508
+ return hit;
23509
+ }
23510
+ let result;
23511
+ try {
23512
+ result = {
23513
+ ok: true,
23514
+ parsed: parseExpression(source)
23515
+ };
23516
+ } catch (err) {
23517
+ result = {
23518
+ ok: false,
23519
+ error: err instanceof ExpressionParseError ? err.message : String(err)
23520
+ };
23521
+ }
23522
+ cache.set(source, result);
23523
+ if (cache.size > 256) {
23524
+ const oldest = cache.keys().next().value;
23525
+ if (oldest !== void 0) cache.delete(oldest);
23526
+ }
23527
+ return result;
23528
+ }
23529
+ /** Compile `source`, returning a discriminated result instead of throwing.
23530
+ * Used by read paths that must degrade rather than raise. LRU/negative-cached. */
23531
+ function compileExpressionSafe(source) {
23532
+ return getCached(source);
23533
+ }
23534
+ Object.freeze({});
23464
23535
  /**
23465
- * A parked ("stationary") object surfaced alongside occupancy an object that
23466
- * settled and stopped moving. It is NO LONGER a tracked object (the tracker was
23467
- * told to forget it so it stops re-spawning tracks/events), but it IS still
23468
- * physically present, so it keeps counting toward `frame` occupancy and is
23469
- * listed here so the UI can show it in a dedicated "Stationary" section instead
23470
- * of flooding the live event feed.
23536
+ * Author-time validation. Returns `null` when the source is valid, else a
23537
+ * human-readable error message. Checks: the expression compiles; binding count
23538
+ * is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
23539
+ * is not reserved (`now`/keywords) and does not shadow a builtin; and every
23540
+ * FREE identifier of the AST is covered by a binding or the injected `now`.
23471
23541
  */
23472
- var StationaryObjectSchema = object({
23473
- id: string(),
23474
- className: string(),
23475
- bbox: object({
23476
- x: number(),
23477
- y: number(),
23478
- w: number(),
23479
- h: number()
23542
+ function validateExpressionSource(src) {
23543
+ const names = Object.keys(src.bindings);
23544
+ if (names.length > 32) return `too many bindings (${names.length} > 32)`;
23545
+ for (const name of names) {
23546
+ if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
23547
+ if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
23548
+ if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
23549
+ }
23550
+ const compiled = compileExpressionSafe(src.expr);
23551
+ if (!compiled.ok) return compiled.error;
23552
+ const bound = new Set(names);
23553
+ for (const id of compiled.parsed.identifiers) {
23554
+ if (id === "now") continue;
23555
+ if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
23556
+ }
23557
+ return null;
23558
+ }
23559
+ var ExpressionBindingSourceSchema = union([
23560
+ object({
23561
+ kind: literal("field").optional(),
23562
+ sourceKey: string(),
23563
+ cap: string(),
23564
+ fieldPath: string()
23480
23565
  }),
23481
- frameWidth: number().int().nonnegative(),
23482
- frameHeight: number().int().nonnegative(),
23483
- /** When the source track was first seen. */
23484
- firstSeenAt: number().int(),
23485
- /** When the object was recognised as parked (promotion time). */
23486
- becameStationaryAt: number().int(),
23487
- /** Last frame a detection confirmed the object is still there. */
23488
- lastConfirmedAt: number().int(),
23489
- /** Enrichment label carried from the source track (identity / plate). */
23490
- label: string().optional(),
23491
- /** Native-resolution key-frame media key for the parked object's best image. */
23492
- keyFrameMediaKey: string().optional()
23493
- });
23494
- var CameraOccupancySnapshotSchema = object({
23495
- /** Frame timestamp of the inference result that produced this snapshot. */
23496
- ts: number().int(),
23497
- /** Frame width/height in pixels — let the UI normalize bbox coords. */
23498
- frameWidth: number().int().nonnegative(),
23499
- frameHeight: number().int().nonnegative(),
23500
- /** Per-zone breakdown — one entry per defined zone (user + onboard). */
23501
- zones: array(ZoneScopeBreakdownSchema).readonly(),
23502
- /** Frame-wide aggregate (everywhere, regardless of zone membership).
23503
- * INCLUDES currently-confirmed stationary objects (they are still present). */
23504
- frame: PerScopeBreakdownSchema,
23505
- /** Detections that landed outside every zone. Empty when no zones defined. */
23506
- unzoned: PerScopeBreakdownSchema,
23507
- /** Parked objects on this camera (additive — absent on legacy snapshots).
23508
- * Surfaced separately so the UI shows them in a dedicated section rather
23509
- * than as repeated tracks/events. */
23510
- stationaryObjects: array(StationaryObjectSchema).readonly().optional()
23511
- });
23512
- /**
23513
- * Time-series resolution. The history methods return one bucket per
23514
- * step over the requested range. Smaller resolutions cost more
23515
- * memory + bandwidth; bound to discrete steps so caller cannot ask
23516
- * for arbitrary fractional buckets.
23517
- */
23518
- var HistoryResolutionEnum = _enum([
23519
- "minute",
23520
- "5min",
23521
- "hour"
23522
- ]);
23523
- var HistoryRangeSchema = object({
23524
- /** Range start (epoch ms, inclusive). */
23525
- from: number().int(),
23526
- /** Range end (epoch ms, inclusive). Defaults to "now" at query time. */
23527
- to: number().int(),
23528
- resolution: HistoryResolutionEnum
23529
- });
23530
- var HistoryPointSchema = object({
23531
- /** Bucket midpoint (epoch ms). */
23532
- ts: number().int(),
23533
- /** Object count averaged over the bucket (rounded to nearest integer). */
23534
- count: number().int().nonnegative()
23535
- });
23536
- DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
23537
- deviceId: number(),
23538
- zoneId: string(),
23539
- className: string().optional()
23540
- }).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
23541
- deviceId: number(),
23542
- className: string().optional()
23543
- }).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
23544
- deviceId: number(),
23545
- className: string().optional()
23546
- }).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly());
23547
- /**
23548
- * Stages a {@link ZoneRule} can apply to. Discriminator on the rules
23549
- * cap so a single CRUD surface backs every consumer; each stage has
23550
- * its own dev-state mirror slice (`motion-zone-rules`,
23551
- * `detection-zone-rules`, …) so consumer addons subscribe independently.
23552
- *
23553
- * Extend the enum here when a new gating consumer comes online (audio
23554
- * gating, alert filtering, …) — no other surface needs to change.
23555
- */
23556
- var ZoneRuleStageEnum = _enum([
23557
- "motion",
23558
- "detection",
23559
- "package"
23566
+ object({
23567
+ kind: literal("literal"),
23568
+ value: union([
23569
+ string(),
23570
+ number(),
23571
+ boolean(),
23572
+ _null()
23573
+ ])
23574
+ }),
23575
+ object({
23576
+ kind: literal("global"),
23577
+ sourceStableId: string(),
23578
+ cap: string(),
23579
+ fieldPath: string()
23580
+ })
23560
23581
  ]);
23561
- DeviceType.Camera, method(object({
23562
- deviceId: number(),
23563
- stage: ZoneRuleStageEnum
23564
- }), array(ZoneRuleSchema).readonly()), method(object({
23565
- deviceId: number(),
23566
- stage: ZoneRuleStageEnum,
23567
- rules: array(ZoneRuleSchema).readonly()
23568
- }), _void(), {
23569
- kind: "mutation",
23570
- auth: "admin"
23571
- }), object({
23572
- motion: array(ZoneRuleSchema).readonly(),
23573
- detection: array(ZoneRuleSchema).readonly(),
23574
- package: array(ZoneRuleSchema).readonly()
23582
+ object({
23583
+ expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
23584
+ bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
23585
+ }).superRefine((src, ctx) => {
23586
+ const err = validateExpressionSource(src);
23587
+ if (err !== null) ctx.addIssue({
23588
+ code: "custom",
23589
+ message: err,
23590
+ path: ["expr"]
23591
+ });
23575
23592
  });
23576
- /**
23577
- * Accessory device helpers — shared across drivers.
23578
- *
23579
- * Many vendor-specific drivers register accessory child devices on
23580
- * top of a parent (Reolink: siren / floodlight / PIR / autotrack /
23581
- * chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
23582
- * driver picks the right `DeviceType` + `DeviceRole` explicitly when
23583
- * spawning, builds a name derived from the parent, and produces a
23584
- * stableId tied to the parent so boot-restore can reconstruct the
23585
- * relationship.
23586
- *
23587
- * Centralised `(kind → DeviceType)` mapping was dropped on purpose:
23588
- * drivers may reasonably disagree on the right type for an accessory
23589
- * (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
23590
- * read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
23591
- * one canonical mapping was over-prescriptive and added a layer of
23592
- * indirection without saving meaningful code at call sites — the
23593
- * driver knows its own hardware best.
23594
- */
23595
- /**
23596
- * Subset of `DeviceRole` values that drivers register as child
23597
- * accessories of a parent device. Sourced verbatim from `DeviceRole`
23598
- * — `AccessoryKind` is the alias drivers use when building accessory
23599
- * children, so the call site reads as
23600
- * `accessoryStableId(parent, AccessoryKind.Siren)` rather than
23601
- * `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
23602
- * any role works, including non-accessory ones like Doorbell).
23603
- */
23604
- var AccessoryKind = {
23605
- Siren: DeviceRole.Siren,
23606
- Floodlight: DeviceRole.Floodlight,
23607
- Spotlight: DeviceRole.Spotlight,
23608
- PirSensor: DeviceRole.PirSensor,
23609
- Chime: DeviceRole.Chime,
23610
- Autotrack: DeviceRole.Autotrack,
23611
- Nightvision: DeviceRole.Nightvision,
23612
- PrivacyMask: DeviceRole.PrivacyMask
23613
- };
23614
- AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
23615
- DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
23616
- new Set(Object.values(DeviceType));
23617
- DeviceFeature.BatteryOperated;
23618
23593
  Object.freeze({
23619
23594
  "accessories.setChildHidden": {
23620
23595
  capName: "accessories",
@@ -24438,6 +24413,12 @@ Object.freeze({
24438
24413
  addonId: null,
24439
24414
  access: "view"
24440
24415
  },
24416
+ "coreBlocks.restart": {
24417
+ capName: "core-blocks",
24418
+ capScope: "system",
24419
+ addonId: null,
24420
+ access: "create"
24421
+ },
24441
24422
  "coreBlocks.setEnabled": {
24442
24423
  capName: "core-blocks",
24443
24424
  capScope: "system",
@@ -25074,12 +25055,6 @@ Object.freeze({
25074
25055
  addonId: null,
25075
25056
  access: "create"
25076
25057
  },
25077
- "deviceManager.setDeviceLinks": {
25078
- capName: "device-manager",
25079
- capScope: "system",
25080
- addonId: null,
25081
- access: "create"
25082
- },
25083
25058
  "deviceManager.setDisabled": {
25084
25059
  capName: "device-manager",
25085
25060
  capScope: "system",
@@ -29241,7 +29216,7 @@ var AdminUIAddon = class extends BaseAddon {
29241
29216
  capability: adminUiCapability,
29242
29217
  provider: {
29243
29218
  getStaticDir: async () => ({ staticDir: resolveAdminUiDistDir() }),
29244
- getVersion: async () => ({ version: "1.2.36" })
29219
+ getVersion: async () => ({ version: "1.2.37" })
29245
29220
  }
29246
29221
  }];
29247
29222
  }