@camstack/addon-admin-ui 1.2.32 → 1.2.34

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 (90) hide show
  1. package/dist/assets/{AddLocationWizard-Bqn51iVc.js → AddLocationWizard-DzP-CYOi.js} +1 -1
  2. package/dist/assets/{AddonCollectionPage-D9EE6p80.js → AddonCollectionPage-B5t3SHZE.js} +1 -1
  3. package/dist/assets/{Addons-Bp55KXZR.js → Addons-DCR8DMgD.js} +1 -1
  4. package/dist/assets/{AdminTabs-DB9TozBh.js → AdminTabs-DTECFIAe.js} +1 -1
  5. package/dist/assets/{AiProfiles-C4ji0awJ.js → AiProfiles-D0jpUF4v.js} +1 -1
  6. package/dist/assets/{Automation-BmDGNoTh.js → Automation-BX0bNbpp.js} +1 -1
  7. package/dist/assets/{BrokerForm-CtjETVv8.js → BrokerForm-CROZPAB-.js} +1 -1
  8. package/dist/assets/{Capabilities-BlEUfcGN.js → Capabilities-DIGQ9fiv.js} +1 -1
  9. package/dist/assets/{CapabilityBadges-CTZ7zqWM.js → CapabilityBadges-B4vHw0qw.js} +1 -1
  10. package/dist/assets/{Cluster-CkGq5KVh.js → Cluster-BnTT5Wsb.js} +1 -1
  11. package/dist/assets/{ClusterRecordingTab-nszIK7qG.js → ClusterRecordingTab-BVkyOEdI.js} +1 -1
  12. package/dist/assets/{Dashboard-CO5PTZ9n.js → Dashboard-BlkR2NIh.js} +1 -1
  13. package/dist/assets/{Data-CYwE4-K2.js → Data-CECcPfPj.js} +1 -1
  14. package/dist/assets/{DetectionIntelligence-MaxZ_sn8.js → DetectionIntelligence-CzMadCA-.js} +1 -1
  15. package/dist/assets/{DeviceAggregateSection-C6sscd95.js → DeviceAggregateSection-CbCIksD2.js} +1 -1
  16. package/dist/assets/DeviceDetail-oWNRSfva.js +2 -0
  17. package/dist/assets/{Devices-xlmwhKxI.js → Devices-COMVrlGU.js} +1 -1
  18. package/dist/assets/{EmbedPlayerPage-CLGFNg5C.js → EmbedPlayerPage-B4Y1MxZ9.js} +1 -1
  19. package/dist/assets/Events-BIZWrXMY.js +1 -0
  20. package/dist/assets/{FormBuilder-ZuE9B0w9.js → FormBuilder-Cogt0U0A.js} +1 -1
  21. package/dist/assets/{Identity-CKR7bmrK.js → Identity-i_LhUVHo.js} +1 -1
  22. package/dist/assets/{IntegrationDetail-DUkC3dFN.js → IntegrationDetail-CPE4VRwU.js} +1 -1
  23. package/dist/assets/{Integrations-t8-3dqN4.js → Integrations-DhMaG2V3.js} +1 -1
  24. package/dist/assets/{Integrations-BuhF2y9p.js → Integrations-qNIS4emE.js} +1 -1
  25. package/dist/assets/{Logs-D1sMKNHu.js → Logs-rH1aVfpt.js} +1 -1
  26. package/dist/assets/{MotionZonesSettings-JUP-5dky.js → MotionZonesSettings-DDSnsNc9.js} +1 -1
  27. package/dist/assets/{MyAccess-BSkJTbz8.js → MyAccess-J8p1dKF3.js} +1 -1
  28. package/dist/assets/{Network-CXKyrgFS.js → Network-C6bXHvxd.js} +1 -1
  29. package/dist/assets/{NodeAddonsSettingsPanel-1C9rZfoa.js → NodeAddonsSettingsPanel-6Dzcc5in.js} +1 -1
  30. package/dist/assets/{NodeInferenceDevices-D1yIrxri.js → NodeInferenceDevices-DraQNeS_.js} +1 -1
  31. package/dist/assets/{Notifications-gurR5fDh.js → Notifications-BL4Cp1b2.js} +1 -1
  32. package/dist/assets/{Pipeline-DTKomVZV.js → Pipeline-Bf8P_Drx.js} +1 -1
  33. package/dist/assets/{Plates-CQ4aoncO.js → Plates-BkYxLI16.js} +1 -1
  34. package/dist/assets/{PrivacyMaskSettings-CfvJZ3tP.js → PrivacyMaskSettings-ByZ6t3aH.js} +1 -1
  35. package/dist/assets/{ProviderIcon-ClXY1rL4.js → ProviderIcon-9BELU-vo.js} +1 -1
  36. package/dist/assets/{Recordings-DglOV8NW.js → Recordings-OoJn-xbL.js} +1 -1
  37. package/dist/assets/{ScanNetworkModal-DmZyDQIW.js → ScanNetworkModal-DGnLpsYA.js} +1 -1
  38. package/dist/assets/{Settings-BVw2pnr7.js → Settings-DhIPBQcW.js} +1 -1
  39. package/dist/assets/{Showroom-9BtpR6nD.js → Showroom-C7QnFCQp.js} +1 -1
  40. package/dist/assets/{Terminal-C-v8B3we.js → Terminal-DVjWHgq3.js} +1 -1
  41. package/dist/assets/{_virtual_mf-localSharedImportMap___mfe_internal__admin_ui_host-BtA4eogJ.js → _virtual_mf-localSharedImportMap___mfe_internal__admin_ui_host-DkbcXBBJ.js} +1 -1
  42. package/dist/assets/{build-cluster-graph-6UMcPt27.js → build-cluster-graph-BDFGSfk8.js} +1 -1
  43. package/dist/assets/circle-check-big-DX7P42Tw.js +1 -0
  44. package/dist/assets/clock-DMJpZrGI.js +1 -0
  45. package/dist/assets/{copy-BObtPXKC.js → copy-oDpB_CYv.js} +1 -1
  46. package/dist/assets/{debounced-invalidate-Sygznr_T.js → debounced-invalidate-CPxCM-Lt.js} +1 -1
  47. package/dist/assets/dist-CekX4Hra.js +7 -0
  48. package/dist/assets/{dist-W1UImz79.js → dist-Dx0ACB1O.js} +1 -1
  49. package/dist/assets/{download-DsIysR_y.js → download-c60BtWes.js} +1 -1
  50. package/dist/assets/{face-bbox-DjqvdhYZ.js → face-bbox-nkg8OmFa.js} +1 -1
  51. package/dist/assets/{hostInit-BUHMjqwF.js → hostInit-BpyXzSDR.js} +1 -1
  52. package/dist/assets/{index-BqCgqp9V.js → index-2QX2zxyn.js} +3 -3
  53. package/dist/assets/index-BIqRUg3I.css +2 -0
  54. package/dist/assets/{key-round-DF1dcL0w.js → key-round-BlSZq_ro.js} +1 -1
  55. package/dist/assets/{mf-entry-bootstrap-0-62a13374.js → mf-entry-bootstrap-0-3845b4a0.js} +2 -2
  56. package/dist/assets/{pencil-CQwfEybQ.js → pencil-DUgcFYZU.js} +1 -1
  57. package/dist/assets/{player-overlays-CJ2EDXmI.js → player-overlays-CzjKRHin.js} +1 -1
  58. package/dist/assets/plus-C7qYd7DF.js +1 -0
  59. package/dist/assets/{radio-C754NrLM.js → radio-eY0YHItP.js} +1 -1
  60. package/dist/assets/{refresh-cw-DAm7QoFj.js → refresh-cw-D_fKxMIe.js} +1 -1
  61. package/dist/assets/remoteEntry-CFMi51wk.js +1 -0
  62. package/dist/assets/{rotate-ccw-ildFCltM.js → rotate-ccw-DU4_rBQq.js} +1 -1
  63. package/dist/assets/rotate-cw-CktlNgLW.js +1 -0
  64. package/dist/assets/search-CY5Hv7Yv.js +1 -0
  65. package/dist/assets/{send-BMpnpOJi.js → send-B9r8Lohp.js} +1 -1
  66. package/dist/assets/{server-2ReeYjUz.js → server-BhdxNAPC.js} +1 -1
  67. package/dist/assets/square-S_5NH0Gl.js +1 -0
  68. package/dist/assets/{square-check-big-DUv4JqM3.js → square-check-big-DMenPwwp.js} +1 -1
  69. package/dist/assets/{src-DCsdXZpS.js → src-BBe6PrXe.js} +10 -10
  70. package/dist/assets/src-DSZBcu6j.js +7 -0
  71. package/dist/assets/{upload-Dvv16Zg3.js → upload-AwB-upJP.js} +1 -1
  72. package/dist/assets/{useEventInvalidation-BI2Lc7mp.js → useEventInvalidation-qhbuBjhg.js} +1 -1
  73. package/dist/assets/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__admin_ui_host__remoteEntry-_hash_-D3wOFHTg.js → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__admin_ui_host__remoteEntry-_hash_-CPcVkZQ0.js} +2 -2
  74. package/dist/assets/{wifi-cBAahSiZ.js → wifi-DyeEKGI-.js} +1 -1
  75. package/dist/assets/{zap-C86yuB6j.js → zap-C32Ix0E1.js} +1 -1
  76. package/dist/index.html +6 -6
  77. package/dist/server/addon.js +642 -32
  78. package/package.json +1 -1
  79. package/dist/assets/DeviceDetail-KHB2dWaR.js +0 -2
  80. package/dist/assets/Events-EfdVWQGS.js +0 -1
  81. package/dist/assets/circle-check-big-5J4vhHcH.js +0 -1
  82. package/dist/assets/clock-D19QkJv2.js +0 -1
  83. package/dist/assets/dist-Bso6V1TI.js +0 -7
  84. package/dist/assets/index-BH_HqZVJ.css +0 -2
  85. package/dist/assets/plus-BIR1e9Y7.js +0 -1
  86. package/dist/assets/remoteEntry-EVtyXnE6.js +0 -1
  87. package/dist/assets/rotate-cw-DyrhB96Y.js +0 -1
  88. package/dist/assets/search-BFU3RCdH.js +0 -1
  89. package/dist/assets/square-CjeJk_qb.js +0 -1
  90. package/dist/assets/src-YgttllUb.js +0 -7
@@ -6462,7 +6462,20 @@ var BrokerStatsSchema = object({
6462
6462
  sampleRate: number(),
6463
6463
  channels: number(),
6464
6464
  supported: boolean()
6465
- }).nullable().optional()
6465
+ }).nullable().optional(),
6466
+ /**
6467
+ * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
6468
+ * distributing none of the device's audio, on live or recording.
6469
+ *
6470
+ * Present so a silent camera can be told apart from a broken one on the
6471
+ * stream panel itself, without cross-referencing the switch group: a
6472
+ * broker holding an `audio` track descriptor while `audioMuted` is true is
6473
+ * working exactly as asked. `audioMutedDropped` counts the audio units
6474
+ * thrown away since the current dial — it is how you confirm from stats
6475
+ * alone that the mute is on the packet path and not merely persisted.
6476
+ */
6477
+ audioMuted: boolean().optional(),
6478
+ audioMutedDropped: number().optional()
6466
6479
  });
6467
6480
  /**
6468
6481
  * Exporter-facing "profile restream" entry. Returned by
@@ -7087,7 +7100,7 @@ object({
7087
7100
  * ## This file adds no state
7088
7101
  *
7089
7102
  * Every switch here is a VIEW onto an authority that already existed
7090
- * ([D61](../../../../docs/decisions/adr-0062.md)). The whole point of the
7103
+ * ([D62](../../../../docs/decisions/adr-0062.md)). The whole point of the
7091
7104
  * group is that there is exactly one place each function is turned off, and
7092
7105
  * the group routes to it:
7093
7106
  *
@@ -7098,6 +7111,53 @@ object({
7098
7111
  * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
7099
7112
  * | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
7100
7113
  * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
7114
+ * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
7115
+ * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
7116
+ * | `broker-audio` | `streamBroker.setDeviceAudioMute` → `DeviceOverride.audioMuted` | `StreamBroker.setAudioMuted` drops the audio plane at the source: no `type:'audio'` packet leaves `fanOutEncoded`, no RTP reaches the restreamer, and the restreamer serves the video-only SDP |
7117
+ *
7118
+ * ## `device-audio` and `broker-audio` are two functions, not two knobs
7119
+ *
7120
+ * They look adjacent and they are not the same control ([D83](../../../../docs/decisions/adr-0083.md)):
7121
+ * `device-audio` writes the CAMERA, so it is hardware privacy — the microphone
7122
+ * genuinely stops, it survives CamStack entirely, and it costs a multi-second
7123
+ * encoder restart on every flip. `broker-audio` writes THIS server, so it is
7124
+ * instant, vendor-independent and reversible without touching the camera, and
7125
+ * a camera that ignores or lacks the ISAPI/Reolink control is still silenced.
7126
+ * D62 forbids a second switch that *disagrees* with the first; these two
7127
+ * cannot disagree, because neither reads the other's store — the camera holds
7128
+ * one, the broker holds the other, and each reports its own fact.
7129
+ *
7130
+ * ## The two switches whose authority is not on this server
7131
+ *
7132
+ * `privacy-mask` and `device-audio` write the CAMERA. That is not a loophole
7133
+ * in "the group stores nothing" — it is the purest form of it: the camera
7134
+ * holds the fact, every read is a read-through, and there is no server-side
7135
+ * copy that could drift. Their availability therefore cannot come from
7136
+ * `listBindableCapsForDeviceType` (a device-NATIVE cap carries no wrappers and
7137
+ * is filtered out there); it comes from the cap's own camera-probed
7138
+ * `privacyMask.getOptions()`, which is strictly more honest — it answers for
7139
+ * THIS camera rather than for the device type
7140
+ * ([D74](../../../../docs/decisions/adr-0074.md)).
7141
+ *
7142
+ * ## `privacy-mask` is the one row whose ON is not "the function is working"
7143
+ *
7144
+ * Every other switch means *this camera's function is doing its job*, so
7145
+ * `enabled: false` is a thing an operator took away. `privacy-mask` means **the
7146
+ * MASK is active** — `enabled: true` is video deliberately obscured. The
7147
+ * polarity is not a choice made here: `addon-export-hap`'s privacy `Switch`
7148
+ * (`builders/privacy-switch.ts`) already mirrors `patch.enabled` verbatim, and
7149
+ * a HomeKit toggle that disagreed with the app's toggle for the same camera is
7150
+ * worse than either surface not having one.
7151
+ *
7152
+ * Two consequences follow and both are load-bearing:
7153
+ *
7154
+ * - **It never counts as `switchedOff`.** `countsAsSwitchedOff` is `false` for
7155
+ * exactly this row. With the polarity above, every camera that has NOT drawn
7156
+ * a privacy mask would otherwise report `switchedOff: ['privacy-mask']` — the
7157
+ * normal, healthy state of most cameras rendered as an operator disablement.
7158
+ * - **Its cost line names BOTH directions.** `costWhenOff` is rendered
7159
+ * unconditionally by both clients, so for this row it has to read correctly
7160
+ * whichever way the switch is sitting.
7101
7161
  *
7102
7162
  * The wrapper-binding pair is not a new idea: `legacy-migrations.ts` already
7103
7163
  * migrated the legacy `audioEnabled` / `pipelineEnabled` /
@@ -7116,14 +7176,18 @@ object({
7116
7176
  * `CameraStatus.switchedOff`.
7117
7177
  */
7118
7178
  /**
7119
- * The five functions the operator named (2026-08-05). Deliberately NOT one id
7120
- * per pipeline step: face recognition and plate/LPR are per-step toggles on
7179
+ * The functions the operator named — five on 2026-08-05, plus the camera's own
7180
+ * microphone on 2026-08-07. Deliberately NOT one id per pipeline step: face
7181
+ * recognition and plate/LPR are per-step toggles on
7121
7182
  * `pipelineOrchestrator.setCameraStepToggle` and belong in the pipeline
7122
- * editor, not in a five-button safety group.
7183
+ * editor, not in a safety group.
7123
7184
  */
7124
7185
  var CameraSwitchIdSchema = _enum([
7125
7186
  "stream-broker",
7126
7187
  "object-detection",
7188
+ "privacy-mask",
7189
+ "device-audio",
7190
+ "broker-audio",
7127
7191
  "audio-analysis",
7128
7192
  "recording",
7129
7193
  "notifications"
@@ -7141,14 +7205,27 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7141
7205
  capName: string()
7142
7206
  }),
7143
7207
  object({ kind: literal("recording-config") }),
7144
- object({ kind: literal("notification-mute") })
7208
+ object({ kind: literal("notification-mute") }),
7209
+ object({
7210
+ kind: literal("camera-audio"),
7211
+ capName: string()
7212
+ }),
7213
+ object({
7214
+ kind: literal("camera-mask"),
7215
+ capName: string()
7216
+ }),
7217
+ object({ kind: literal("broker-audio-mute") })
7145
7218
  ]);
7146
7219
  /**
7147
7220
  * Why a switch is not offered for this camera. Rendered instead of the
7148
7221
  * control, never as a dead control — an absent function and a broken one must
7149
7222
  * not look the same.
7150
7223
  */
7151
- var CameraSwitchUnavailableReasonSchema = _enum(["no-provider", "source-unreachable"]);
7224
+ var CameraSwitchUnavailableReasonSchema = _enum([
7225
+ "no-provider",
7226
+ "source-unreachable",
7227
+ "not-configured"
7228
+ ]);
7152
7229
  /**
7153
7230
  * One switch, resolved for one camera.
7154
7231
  *
@@ -9116,6 +9193,26 @@ var EgressTranscodeRequestSchema = object({
9116
9193
  "h264_mp4toannexb",
9117
9194
  "hevc_mp4toannexb"
9118
9195
  ]).optional(),
9196
+ /**
9197
+ * Publish the transcode as a LOCAL push cam stream, instead of leaving the
9198
+ * consumer to dial the returned url. The broker picks the id and returns it
9199
+ * as `camStreamId` — a caller-supplied one would be circular, since the
9200
+ * sharing key is computed FROM this request.
9201
+ *
9202
+ * The url is still returned and still the contract for a transcode pinned to
9203
+ * another node. But dialling it locally costs an RTSP round trip that changes
9204
+ * the transport underneath the consumer: a dialled stream is an RTP source,
9205
+ * so `isRtpSource()` is true and the session takes the RTP-passthrough +
9206
+ * repacketizer branch. The push branch — the one the derived mechanism has
9207
+ * live hours on — is never reached. Measured on Alexa: broker registered, RTP
9208
+ * arriving, key frame arriving, black screen, on a chain healthy at every
9209
+ * other point.
9210
+ *
9211
+ * Same idea the transport already applies to CALLS, where `classifyCapRoute`
9212
+ * gives priority to `hub-in-process` so a local call never leaves the node.
9213
+ * This is that rule for media.
9214
+ */
9215
+ publishLocally: boolean().optional(),
9119
9216
  pixelFormat: _enum(["yuv420p", "nv12"]).optional(),
9120
9217
  /**
9121
9218
  * Operator/consumer override for decode hardware. ABSENT is the normal case
@@ -9160,7 +9257,13 @@ var EgressTranscodeSchema = object({
9160
9257
  * Returned rather than assumed: a consumer that asked for hardware and got
9161
9258
  * software needs to be able to see that without reading the broker's logs.
9162
9259
  */
9163
- decodeHwAccel: string().nullable()
9260
+ decodeHwAccel: string().nullable(),
9261
+ /**
9262
+ * Set when `publishLocally` was honoured: attach to THIS instead of dialling
9263
+ * `url`, and the session takes the push/deframe transport rather than the
9264
+ * RTP-passthrough one. `null` means the consumer must dial.
9265
+ */
9266
+ camStreamId: string().nullable()
9164
9267
  });
9165
9268
  method(object({
9166
9269
  deviceId: number().int().nonnegative(),
@@ -9308,7 +9411,25 @@ method(object({
9308
9411
  }), _void(), {
9309
9412
  kind: "mutation",
9310
9413
  auth: "admin"
9311
- }), method(object({ brokerId: string() }), boolean()), object({
9414
+ }), method(object({ brokerId: string() }), boolean()), method(object({ deviceId: number().int() }), object({
9415
+ muted: boolean(),
9416
+ /**
9417
+ * How many live non-derived brokers currently hold the mute. Purely
9418
+ * diagnostic: `muted` is the policy and is authoritative on its own
9419
+ * (it applies to brokers that do not exist yet), while this says
9420
+ * whether anything is presently being silenced.
9421
+ */
9422
+ appliedBrokers: number().int().nonnegative()
9423
+ })), method(object({
9424
+ deviceId: number().int(),
9425
+ muted: boolean()
9426
+ }), object({
9427
+ muted: boolean(),
9428
+ appliedBrokers: number().int().nonnegative()
9429
+ }), {
9430
+ kind: "mutation",
9431
+ auth: "admin"
9432
+ }), object({
9312
9433
  deviceId: number().int().nonnegative(),
9313
9434
  camStreamId: string(),
9314
9435
  profile: CamProfileSchema
@@ -14550,6 +14671,30 @@ var TrackSourceSchema = _enum([
14550
14671
  "audio"
14551
14672
  ]);
14552
14673
  /**
14674
+ * Where a track sits in the RETRAIN lifecycle (D81).
14675
+ *
14676
+ * - `none` — never marked, or un-marked. Evictable.
14677
+ * - `staging` — the operator wants this track as training material and has not
14678
+ * finished with it. **This is the only state retention holds**: the track and
14679
+ * everything it owns (object events, crops, keyframes, CLIP vector) survive
14680
+ * the device's age window.
14681
+ * - `trained` — the retrain page has taken what it needed. The frames it chose
14682
+ * were COPIED into the retrain dataset at selection time, so the dataset no
14683
+ * longer depends on the track's media and the track becomes EVICTABLE again.
14684
+ * Terminal for the plain `markForTrain` toggle: returning it to `staging` is
14685
+ * a deliberate action of the retrain page, not a side effect of a checkbox.
14686
+ *
14687
+ * There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` because
14688
+ * the store's filter language has only positive equality and `whereIn` — no
14689
+ * negation, no IS NULL — so a NULL would be unselectable by ANY predicate and
14690
+ * would make the entire pre-column history immortal in one deploy.
14691
+ */
14692
+ var RetrainStatusSchema = _enum([
14693
+ "none",
14694
+ "staging",
14695
+ "trained"
14696
+ ]);
14697
+ /**
14553
14698
  * Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
14554
14699
  * by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
14555
14700
  * so the two surfaces cannot drift.
@@ -14559,18 +14704,31 @@ var TrackSourceSchema = _enum([
14559
14704
  * columns existed read as absent, and a consumer that needs a boolean should say
14560
14705
  * `flag === true`, not `flag !== false`.
14561
14706
  *
14562
- * What the flags DO is deliberately UNDEFINED at the time of writing: they are
14563
- * operator curation, and the behaviour they drive will be specified separately.
14564
- * In particular a `markForTrain` track is NOT pinned against retention — see
14565
- * `docs/decisions/adr-0059.md` for why that is a store-level change, not a flag.
14707
+ * `markForTrain` is the WIRE FACE of {@link RetrainStatusSchema}, not a column:
14708
+ * it is exactly `retrainStatus === 'staging'`, in both directions. Writing
14709
+ * `true` moves `none → staging`, writing `false` moves `staging none`, and a
14710
+ * `trained` track reports `false` while refusing both writes. The boolean is
14711
+ * kept because three surfaces drive a toggle off it; anything that needs to tell
14712
+ * "never marked" from "already trained" must read `retrainStatus`.
14713
+ *
14714
+ * `debug` does NOT pin; it is attention, not durability.
14566
14715
  */
14567
14716
  var TrackFlagFields = {
14568
- /** Operator marked this track as training material. */
14717
+ /** Operator marked this track as training material — i.e. `retrainStatus` is
14718
+ * `'staging'`. */
14569
14719
  markForTrain: boolean().optional(),
14570
14720
  /** Operator marked this track for diagnostic attention. */
14571
14721
  debug: boolean().optional()
14572
14722
  };
14573
14723
  /**
14724
+ * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
14725
+ * Deliberately NOT part of {@link TrackFlagFields}: that group also builds the
14726
+ * write patch, and the status is not something the toggle sets — it is what the
14727
+ * toggle's boolean is derived from. Absent on an in-RAM track never touched;
14728
+ * always present on a persisted row (the column default materialises `'none'`).
14729
+ */
14730
+ var TrackRetrainFields = { retrainStatus: RetrainStatusSchema.optional() };
14731
+ /**
14574
14732
  * The write half: a PARTIAL patch. An omitted key is left untouched, so setting
14575
14733
  * one flag can never clear the other — the toggles are independent and are
14576
14734
  * driven from three surfaces that do not know about each other.
@@ -14584,7 +14742,32 @@ var TrackFlagsPatchSchema = object(TrackFlagFields);
14584
14742
  var TrackFlagsSchema = object({
14585
14743
  trackId: string(),
14586
14744
  markForTrain: boolean(),
14587
- debug: boolean()
14745
+ debug: boolean(),
14746
+ /** The lifecycle state the boolean was derived from. Required here (unlike on
14747
+ * a track row) because this shape is only ever produced by the write body,
14748
+ * which always knows it — and a surface that has just written needs to render
14749
+ * `trained` without a re-fetch. */
14750
+ retrainStatus: RetrainStatusSchema
14751
+ });
14752
+ /** Per-camera slice of a training-export estimate. */
14753
+ var TrainingExportDeviceTotalsSchema = object({
14754
+ deviceId: number(),
14755
+ tracks: number().int(),
14756
+ files: number().int(),
14757
+ bytes: number().int()
14758
+ });
14759
+ /**
14760
+ * What a training export WOULD contain. Computed from media index rows only —
14761
+ * no blob is read to produce this.
14762
+ */
14763
+ var TrainingExportSummarySchema = object({
14764
+ generatedAt: number(),
14765
+ trackCount: number().int(),
14766
+ fileCount: number().int(),
14767
+ byteCount: number().int(),
14768
+ /** More marked tracks exist than a single pass carries. */
14769
+ truncated: boolean(),
14770
+ devices: array(TrainingExportDeviceTotalsSchema).readonly()
14588
14771
  });
14589
14772
  var TrackSchema = object({
14590
14773
  trackId: string(),
@@ -14629,7 +14812,8 @@ var TrackSchema = object({
14629
14812
  * Populated from the persisted envelope columns on historical reads;
14630
14813
  * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
14631
14814
  envelope: TrackEnvelopeSchema.optional(),
14632
- ...TrackFlagFields
14815
+ ...TrackFlagFields,
14816
+ ...TrackRetrainFields
14633
14817
  });
14634
14818
  var BaseEventFields = {
14635
14819
  id: string(),
@@ -14843,7 +15027,8 @@ var KeyEventSchema = object({
14843
15027
  bestEventId: string(),
14844
15028
  /** Track lifetime in ms (lastSeen - firstSeen). */
14845
15029
  windowMs: number().optional(),
14846
- ...TrackFlagFields
15030
+ ...TrackFlagFields,
15031
+ ...TrackRetrainFields
14847
15032
  });
14848
15033
  object({
14849
15034
  trackId: string(),
@@ -15104,6 +15289,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
15104
15289
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
15105
15290
  kind: "query",
15106
15291
  auth: "admin"
15292
+ }), method(object({ deviceIds: array(number()).optional() }), TrainingExportSummarySchema, {
15293
+ kind: "query",
15294
+ auth: "admin"
15295
+ }), method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
15296
+ kind: "query",
15297
+ auth: "admin"
15107
15298
  }), method(object({
15108
15299
  eventId: string(),
15109
15300
  kind: MediaFileKindEnum.optional()
@@ -16921,9 +17112,15 @@ DeviceType.Camera, method(object({
16921
17112
  * Bypass the cache freshness check and fetch directly from the
16922
17113
  * native (or stream-broker fallback). Triggered by the UI's
16923
17114
  * "refresh" button so an operator can force a fresh frame
16924
- * even when the cache is well within `snapshotMaxAgeMs`.
16925
- * On battery cams this WILL wake the camera — accept the
16926
- * cost only when the user explicitly asks for it.
17115
+ * even when the cache is well within the device's
17116
+ * `snapshotMaxAgeS` window.
17117
+ *
17118
+ * **`force` is an OPERATOR signal, not a freshness preference.** On a
17119
+ * battery camera it is the one thing that walks past the wrapper's
17120
+ * sleep gate and wakes the camera, so a background caller — a poller,
17121
+ * an event handler, a thumbnail — must NEVER set it. Every such caller
17122
+ * gets the cached frame, which on a sleeping battery camera is the
17123
+ * correct answer: stale but honest beats woken.
16927
17124
  */
16928
17125
  force: boolean().optional()
16929
17126
  }), SnapshotImageSchema.nullable()), method(object({ deviceId: number() }), _void(), {
@@ -20758,6 +20955,173 @@ DeviceType.Camera, method(object({
20758
20955
  status: OsdStatusSchema
20759
20956
  });
20760
20957
  /**
20958
+ * `osd-manager` — the ORCHESTRATOR over the device-scope `osd` cap.
20959
+ *
20960
+ * The `osd` cap is the firmware contract: it probes a camera's overlay
20961
+ * SLOTS and writes literal text into one. It has no idea WHERE that text
20962
+ * comes from, and it must not — a driver that grew a "show the temperature
20963
+ * here" feature would grow it once per vendor.
20964
+ *
20965
+ * This cap owns the other half: a per-(camera, slot) BINDING that says
20966
+ * which value feeds the slot, how it is formatted, and under which
20967
+ * conditions it is shown at all. One addon renders every binding on every
20968
+ * camera, so a new source costs zero driver code.
20969
+ *
20970
+ * Three deliberate choices, each with a rejected alternative:
20971
+ *
20972
+ * 1. A source is `(capName, valuePath)` over the kernel's device
20973
+ * runtime-state mirror — NOT a closed enum of source kinds. Every
20974
+ * cap-keyed slice a device publishes is bindable the day the cap
20975
+ * ships. The rejected alternative (one enum member per source, with
20976
+ * a resolver branch each) is what makes "add the humidity too" a
20977
+ * code change.
20978
+ * 2. The display gate reuses `NcConditionsSchema` verbatim — the
20979
+ * notification centre's condition vocabulary — rather than a parallel
20980
+ * model. An operator who has learned one condition editor has learned
20981
+ * both.
20982
+ * 3. Because the renderer's facts are device STATE and not a detection
20983
+ * record, only a SUBSET of that vocabulary can be answered here.
20984
+ * `setSlotBinding` REJECTS the rest at write time (see
20985
+ * `getConditionSupport`). It does not accept-then-fail-closed: a
20986
+ * condition that can never be true renders a permanently blank
20987
+ * overlay, and a blank overlay looks exactly like a broken camera.
20988
+ */
20989
+ /** Where a slot's value comes from. */
20990
+ var OsdSourceSchema = discriminatedUnion("kind", [
20991
+ object({
20992
+ kind: literal("static"),
20993
+ text: string().max(64)
20994
+ }),
20995
+ object({
20996
+ kind: literal("clock"),
20997
+ /** Token pattern: `YYYY MM DD HH mm ss`. Everything else is literal. */
20998
+ pattern: string().min(1).max(32).default("HH:mm"),
20999
+ /** IANA zone. Omitted = the server's zone. */
21000
+ timezone: string().min(1).max(64).optional()
21001
+ }),
21002
+ object({
21003
+ kind: literal("device-state"),
21004
+ deviceId: number().int().optional(),
21005
+ capName: string().min(1).max(64),
21006
+ /** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
21007
+ valuePath: string().min(1).max(64)
21008
+ })
21009
+ ]);
21010
+ var OsdSlotBindingSchema = object({
21011
+ /** Off = the manager stops driving this slot. It does NOT clear it. */
21012
+ enabled: boolean().default(true),
21013
+ source: OsdSourceSchema,
21014
+ /** `${value}` and `${unit}` are substituted; every occurrence. */
21015
+ template: string().max(96).default("${value}"),
21016
+ /** Truncate with an ellipsis past this length. Absent = no limit. */
21017
+ maxCharacters: number().int().min(4).max(64).optional(),
21018
+ /**
21019
+ * Decimal places for a numeric value. `0` yields an integer — the
21020
+ * documented workaround for firmwares that reject `.` in overlay text.
21021
+ */
21022
+ maxDecimals: number().int().min(0).max(4).default(1),
21023
+ /** Appended via `${unit}`. The state mirror does not carry units. */
21024
+ unitLabel: string().max(8).optional(),
21025
+ /** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
21026
+ valueMap: record(string(), string()).optional(),
21027
+ /** Time windows in which the slot is shown. Absent = always. */
21028
+ schedule: NcScheduleSchema.optional(),
21029
+ /**
21030
+ * Display gate, in the notification centre's condition vocabulary.
21031
+ * Only the keys reported by `getConditionSupport` are accepted.
21032
+ */
21033
+ conditions: NcConditionsSchema.optional(),
21034
+ /** Rendered when the gate is closed or the value unreadable. Empty = hide. */
21035
+ fallbackText: string().max(64).default("")
21036
+ });
21037
+ /** One camera slot, as the operator sees it: firmware truth + our binding. */
21038
+ var OsdSlotViewSchema = object({
21039
+ slotId: string(),
21040
+ kind: OsdOverlayKindEnum,
21041
+ /** Firmware refuses text edits (a timestamp, the channel name). */
21042
+ readOnly: boolean(),
21043
+ cameraEnabled: boolean(),
21044
+ cameraText: string().optional(),
21045
+ binding: OsdSlotBindingSchema.nullable()
21046
+ });
21047
+ /**
21048
+ * What happened to one slot on one render pass. `unchanged` exists so the
21049
+ * operator can tell "we are driving this and the value is steady" from
21050
+ * "we never got there" — and so the loop can prove it is not rewriting
21051
+ * identical text to the camera every tick.
21052
+ */
21053
+ var OsdRenderOutcomeEnum = _enum([
21054
+ "written",
21055
+ "unchanged",
21056
+ "gated",
21057
+ "unreadable",
21058
+ "disabled",
21059
+ "unbound",
21060
+ "failed"
21061
+ ]);
21062
+ var OsdRenderResultSchema = object({
21063
+ slotId: string(),
21064
+ outcome: OsdRenderOutcomeEnum,
21065
+ /** The text the slot should carry. Empty = the slot is switched off. */
21066
+ text: string(),
21067
+ /** Why, whenever the outcome is not a plain write. Never silent. */
21068
+ reason: string().optional()
21069
+ });
21070
+ var OsdSourceValueTypeEnum = _enum([
21071
+ "number",
21072
+ "boolean",
21073
+ "string",
21074
+ "enum"
21075
+ ]);
21076
+ /**
21077
+ * One bindable value, derived from a cap's `runtimeState` schema — never
21078
+ * hand-listed. The editor renders from this, so a cap that ships a new
21079
+ * state field becomes bindable with no UI change.
21080
+ */
21081
+ var OsdSourceOptionSchema = object({
21082
+ deviceId: number().int(),
21083
+ deviceName: string(),
21084
+ capName: string(),
21085
+ valuePath: string(),
21086
+ label: string(),
21087
+ valueType: OsdSourceValueTypeEnum,
21088
+ /** Present for `enum`; the editor offers these as `valueMap` keys. */
21089
+ enumValues: array(string()).readonly().optional()
21090
+ });
21091
+ method(object({ deviceId: number().int() }), object({
21092
+ supported: boolean(),
21093
+ slots: array(OsdSlotViewSchema)
21094
+ }), { auth: "admin" }), method(object({ deviceId: number().int() }), object({ sources: array(OsdSourceOptionSchema) }), { auth: "admin" }), method(object({}), object({
21095
+ supported: array(string()),
21096
+ catalog: array(NcConditionDescriptorSchema)
21097
+ }), { auth: "admin" }), method(object({
21098
+ deviceId: number().int(),
21099
+ slotId: string().min(1),
21100
+ binding: OsdSlotBindingSchema
21101
+ }), object({
21102
+ slot: OsdSlotViewSchema,
21103
+ render: OsdRenderResultSchema
21104
+ }), {
21105
+ kind: "mutation",
21106
+ auth: "admin"
21107
+ }), method(object({
21108
+ deviceId: number().int(),
21109
+ slotId: string().min(1)
21110
+ }), object({ success: literal(true) }), {
21111
+ kind: "mutation",
21112
+ auth: "admin"
21113
+ }), method(object({
21114
+ deviceId: number().int(),
21115
+ slotId: string().min(1),
21116
+ binding: OsdSlotBindingSchema.optional()
21117
+ }), OsdRenderResultSchema, {
21118
+ kind: "mutation",
21119
+ auth: "admin"
21120
+ }), method(object({ deviceId: number().int() }), object({ results: array(OsdRenderResultSchema) }), {
21121
+ kind: "mutation",
21122
+ auth: "admin"
21123
+ });
21124
+ /**
20761
21125
  * Feeder connectivity / power status — mirrors the HA petkit device-status
20762
21126
  * enum: `normal` (online, mains), `offline` (not reaching PetKit cloud),
20763
21127
  * `on_batteries` (running on battery backup). `null` until first reported.
@@ -21205,12 +21569,30 @@ object({
21205
21569
  });
21206
21570
  DeviceType.Sensor;
21207
21571
  /**
21208
- * Privacy mask = up to `maxRegions` SHAPES the camera blanks out (NOT a
21209
- * cell grid). Reolink `<shelterList>` zones are rectangles; Hikvision
21210
- * ISAPI `<RegionCoordinatesList>` zones are free polygons (this camera:
21211
- * exactly 4 vertices, not necessarily axis-aligned). The cap composes the
21212
- * shared rect|polygon subset of the MaskShape vocabulary. All coords are
21213
- * normalized 0..1 (top-left origin).
21572
+ * PRIVACY what the camera deliberately does not capture. Two planes:
21573
+ *
21574
+ * - **video**: up to `maxRegions` SHAPES the camera blanks out (NOT a cell
21575
+ * grid). Reolink `<shelterList>` zones are rectangles; Hikvision ISAPI
21576
+ * `<RegionCoordinatesList>` zones are free polygons (this camera: exactly
21577
+ * 4 vertices, not necessarily axis-aligned). The cap composes the shared
21578
+ * rect|polygon subset of the MaskShape vocabulary. All coords are
21579
+ * normalized 0..1 (top-left origin).
21580
+ * - **audio**: the camera's microphone. `setAudioEnabled(false)` stops the
21581
+ * camera encoding an audio track at all, so EVERY consumer — live view,
21582
+ * recording, the audio analyzer, an export — sees silent video. There is
21583
+ * no server-side copy of this fact; the camera is the store and every read
21584
+ * is a read-through, which is why a switch over it cannot drift
21585
+ * ([D62](../../../../docs/decisions/adr-0062.md)).
21586
+ *
21587
+ * Both belong here for one reason: they are the two things an operator turns
21588
+ * off when the answer to "what is this camera allowed to record" changes, and
21589
+ * both are applied ON the device, before anything leaves it.
21590
+ *
21591
+ * **The audio flag has exactly one writer.** `stream-params` used to carry a
21592
+ * per-profile `audio` in its patch schema — reachable from no UI and honoured
21593
+ * by one provider — and it was removed when this landed. A second writer onto
21594
+ * one device register is the shape of every knob this repo has shipped that
21595
+ * disagreed with the one the reader read.
21214
21596
  */
21215
21597
  /** A privacy-mask region's geometry — rectangle or free polygon. */
21216
21598
  var PrivacyMaskShapeSchema = discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
@@ -21226,16 +21608,40 @@ object({
21226
21608
  enabled: boolean(),
21227
21609
  /** Active zones (normalized 0..1). Length ≤ maxRegions. */
21228
21610
  regions: array(PrivacyMaskRegionSchema),
21611
+ /**
21612
+ * Is the camera capturing sound right now? Read from the camera, never from
21613
+ * a server-side mirror.
21614
+ *
21615
+ * `null` means "no answer" — either this camera exposes no controllable
21616
+ * microphone (`getOptions().supportsAudioMute === false`) or the read
21617
+ * failed. A consumer must render `null` as UNKNOWN and never as `false`:
21618
+ * "the microphone is off" and "we could not ask" look identical to an
21619
+ * operator only until one of them is wrong.
21620
+ *
21621
+ * On a camera whose profiles carry the flag independently (Reolink writes
21622
+ * it per stream), `true` means AT LEAST ONE profile still carries audio —
21623
+ * privacy is only satisfied when every one of them is silent.
21624
+ */
21625
+ audioEnabled: boolean().nullable(),
21229
21626
  lastFetchedAt: number()
21230
21627
  });
21231
- /** Per-camera availability. */
21628
+ /** Per-camera availability. Probed, never assumed from the model name. */
21232
21629
  var PrivacyMaskOptionsSchema = object({
21233
21630
  /** Maximum number of supported zones. */
21234
21631
  maxRegions: number(),
21235
21632
  /** Shape kinds this camera accepts — Reolink: ['rect']; Hikvision: ['rect','polygon']. */
21236
21633
  supportedShapes: array(MaskShapeKindSchema),
21237
21634
  /** Polygon vertex bounds when 'polygon' is supported (Hikvision: {min:4,max:4}). */
21238
- polygonVertices: MaskPolygonVerticesSchema.optional()
21635
+ polygonVertices: MaskPolygonVerticesSchema.optional(),
21636
+ /**
21637
+ * Does this camera expose a microphone switch we can actually write?
21638
+ *
21639
+ * Camera-probed: `true` only when the firmware answered with an audio flag
21640
+ * we know how to patch. A camera that never answered is `false` — a control
21641
+ * the operator can press that changes nothing is worse than no control, and
21642
+ * the switch group renders "not available" instead.
21643
+ */
21644
+ supportsAudioMute: boolean()
21239
21645
  });
21240
21646
  /** Partial change — every field optional. */
21241
21647
  var PrivacyMaskPatchSchema = object({
@@ -21248,6 +21654,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), PrivacyMaskOptionsSche
21248
21654
  }), _void(), {
21249
21655
  kind: "mutation",
21250
21656
  auth: "admin"
21657
+ }), method(object({
21658
+ deviceId: number(),
21659
+ enabled: boolean()
21660
+ }), _void(), {
21661
+ kind: "mutation",
21662
+ auth: "admin"
21251
21663
  });
21252
21664
  var PtzPresetSchema = object({
21253
21665
  id: string(),
@@ -21457,6 +21869,21 @@ var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
21457
21869
  })]);
21458
21870
  /** Raw bytes of one finalized footage segment (read off disk on the recording node). */
21459
21871
  var ReadSegmentBytesResultSchema = object({ data: _instanceof(Uint8Array) });
21872
+ /**
21873
+ * One GOP of a finalized segment, cut by byte range through the segment's own
21874
+ * `mfra` (D31 on the D42 feeder path). `data` is the `ftyp`+`moov` head plus
21875
+ * the single `moof`+`mdat` covering the requested instant — standalone-
21876
+ * demuxable, never the whole file. When the segment's index cannot be parsed
21877
+ * the provider degrades INSIDE the mechanism to the whole segment (still one
21878
+ * `data`, `gopStartMs` = the segment start) — a worse read, not another path.
21879
+ */
21880
+ var ReadGopBytesResultSchema = object({
21881
+ data: _instanceof(Uint8Array),
21882
+ /** Absolute epoch ms of the returned fragment's first sample. */
21883
+ gopStartMs: number(),
21884
+ /** Media ms the returned fragment covers. */
21885
+ gopDurMs: number()
21886
+ });
21460
21887
  method(object({
21461
21888
  deviceId: number(),
21462
21889
  fromMs: number(),
@@ -21499,6 +21926,14 @@ method(object({
21499
21926
  }), ReadSegmentBytesResultSchema, {
21500
21927
  kind: "query",
21501
21928
  auth: "admin"
21929
+ }), method(object({
21930
+ deviceId: number(),
21931
+ profile: string(),
21932
+ startMs: number(),
21933
+ epochMs: number()
21934
+ }), ReadGopBytesResultSchema, {
21935
+ kind: "query",
21936
+ auth: "admin"
21502
21937
  }), method(object({
21503
21938
  deviceId: number(),
21504
21939
  config: RecordingConfigSchema
@@ -21979,6 +22414,16 @@ var StreamProfileConfigSchema = object({
21979
22414
  "baseline"
21980
22415
  ]).optional(),
21981
22416
  gop: number().optional(),
22417
+ /**
22418
+ * Whether THIS profile currently carries an audio track. READ-ONLY here.
22419
+ *
22420
+ * There is no matching field on {@link StreamProfilePatchSchema}: the
22421
+ * camera's microphone is owned by `privacy-mask` (`setAudioEnabled`), which
22422
+ * writes every profile at once so "audio off" means silent everywhere. A
22423
+ * per-profile writer beside it would let a camera be half-muted and would be
22424
+ * a second knob onto one device register — the failure D62 exists to
22425
+ * prevent. Absent when the firmware does not report the flag.
22426
+ */
21982
22427
  audio: boolean().optional()
21983
22428
  });
21984
22429
  object({
@@ -22019,7 +22464,13 @@ var StreamParamsOptionsSchema = object({
22019
22464
  ext: StreamProfileOptionsSchema.optional()
22020
22465
  });
22021
22466
  /** A partial change to one profile — every field optional; a provider
22022
- * ignores fields it doesn't support. */
22467
+ * ignores fields it doesn't support.
22468
+ *
22469
+ * There is deliberately NO `audio` here. It existed until 2026-08-07,
22470
+ * reachable from no form and honoured by exactly one provider, while the
22471
+ * camera's microphone is a whole-device fact. It now has one writer,
22472
+ * `privacyMask.setAudioEnabled`, which writes every profile — see
22473
+ * `privacy-mask.cap.ts`. */
22023
22474
  var StreamProfilePatchSchema = object({
22024
22475
  width: number().optional(),
22025
22476
  height: number().optional(),
@@ -22032,8 +22483,7 @@ var StreamProfilePatchSchema = object({
22032
22483
  "main",
22033
22484
  "baseline"
22034
22485
  ]).optional(),
22035
- gop: number().optional(),
22036
- audio: boolean().optional()
22486
+ gop: number().optional()
22037
22487
  });
22038
22488
  DeviceType.Camera, method(object({ deviceId: number() }), StreamParamsOptionsSchema), method(object({
22039
22489
  deviceId: number(),
@@ -25627,6 +26077,48 @@ Object.freeze({
25627
26077
  addonId: null,
25628
26078
  access: "create"
25629
26079
  },
26080
+ "osdManager.clearSlotBinding": {
26081
+ capName: "osd-manager",
26082
+ capScope: "system",
26083
+ addonId: null,
26084
+ access: "delete"
26085
+ },
26086
+ "osdManager.getConditionSupport": {
26087
+ capName: "osd-manager",
26088
+ capScope: "system",
26089
+ addonId: null,
26090
+ access: "view"
26091
+ },
26092
+ "osdManager.getDeviceOsd": {
26093
+ capName: "osd-manager",
26094
+ capScope: "system",
26095
+ addonId: null,
26096
+ access: "view"
26097
+ },
26098
+ "osdManager.getSourceCatalog": {
26099
+ capName: "osd-manager",
26100
+ capScope: "system",
26101
+ addonId: null,
26102
+ access: "view"
26103
+ },
26104
+ "osdManager.previewSlot": {
26105
+ capName: "osd-manager",
26106
+ capScope: "system",
26107
+ addonId: null,
26108
+ access: "create"
26109
+ },
26110
+ "osdManager.renderDevice": {
26111
+ capName: "osd-manager",
26112
+ capScope: "system",
26113
+ addonId: null,
26114
+ access: "create"
26115
+ },
26116
+ "osdManager.setSlotBinding": {
26117
+ capName: "osd-manager",
26118
+ capScope: "system",
26119
+ addonId: null,
26120
+ access: "create"
26121
+ },
25630
26122
  "petFeeder.callPet": {
25631
26123
  capName: "pet-feeder",
25632
26124
  capScope: "device",
@@ -25789,6 +26281,18 @@ Object.freeze({
25789
26281
  addonId: null,
25790
26282
  access: "view"
25791
26283
  },
26284
+ "pipelineAnalytics.getTrainingExportSummary": {
26285
+ capName: "pipeline-analytics",
26286
+ capScope: "device",
26287
+ addonId: null,
26288
+ access: "view"
26289
+ },
26290
+ "pipelineAnalytics.getTrainingExportUrl": {
26291
+ capName: "pipeline-analytics",
26292
+ capScope: "device",
26293
+ addonId: null,
26294
+ access: "view"
26295
+ },
25792
26296
  "pipelineAnalytics.listEventKinds": {
25793
26297
  capName: "pipeline-analytics",
25794
26298
  capScope: "device",
@@ -26545,6 +27049,12 @@ Object.freeze({
26545
27049
  addonId: null,
26546
27050
  access: "view"
26547
27051
  },
27052
+ "privacyMask.setAudioEnabled": {
27053
+ capName: "privacy-mask",
27054
+ capScope: "device",
27055
+ addonId: null,
27056
+ access: "create"
27057
+ },
26548
27058
  "privacyMask.setMask": {
26549
27059
  capName: "privacy-mask",
26550
27060
  capScope: "device",
@@ -26713,6 +27223,12 @@ Object.freeze({
26713
27223
  addonId: null,
26714
27224
  access: "create"
26715
27225
  },
27226
+ "recording.readGopBytes": {
27227
+ capName: "recording",
27228
+ capScope: "system",
27229
+ addonId: null,
27230
+ access: "view"
27231
+ },
26716
27232
  "recording.readSegmentBytes": {
26717
27233
  capName: "recording",
26718
27234
  capScope: "system",
@@ -27253,6 +27769,12 @@ Object.freeze({
27253
27769
  addonId: null,
27254
27770
  access: "view"
27255
27771
  },
27772
+ "streamBroker.getDeviceAudioMute": {
27773
+ capName: "stream-broker",
27774
+ capScope: "system",
27775
+ addonId: null,
27776
+ access: "view"
27777
+ },
27256
27778
  "streamBroker.getPreBufferInfo": {
27257
27779
  capName: "stream-broker",
27258
27780
  capScope: "system",
@@ -27373,6 +27895,12 @@ Object.freeze({
27373
27895
  addonId: null,
27374
27896
  access: "create"
27375
27897
  },
27898
+ "streamBroker.setDeviceAudioMute": {
27899
+ capName: "stream-broker",
27900
+ capScope: "system",
27901
+ addonId: null,
27902
+ access: "create"
27903
+ },
27376
27904
  "streamBroker.setPreBufferDuration": {
27377
27905
  capName: "stream-broker",
27378
27906
  capScope: "system",
@@ -28133,6 +28661,88 @@ object({
28133
28661
  paddingRatio: .15,
28134
28662
  square: false
28135
28663
  }).paddingRatio;
28664
+ /**
28665
+ * WHICH delivered frames the decode worker retains a native copy of.
28666
+ *
28667
+ * - `all` — every frame the worker delivered to the runner. The shipped
28668
+ * behaviour, and the only correct one if something can ask for a crop of a
28669
+ * frame the runner never sent to inference.
28670
+ * - `inferred` — only the frames the runner ADMITTED to its detection queue.
28671
+ * A native-crop request always names a `frameId` that rode an inference
28672
+ * result, so that is the only set a request can name. How much it drops is
28673
+ * the two-plane governor's admit ratio and nothing else: measured at ~50% on
28674
+ * this cluster, not the ~80% the design sketch assumed, because the governor
28675
+ * was not throttling as hard as the sketch supposed. Read
28676
+ * `leaseAdmitted`/`leaseOffered` off the metrics line for the camera in front
28677
+ * of you rather than quoting a number from here. The newest delivered frame is
28678
+ * croppable regardless — it is still the worker's reserved slot, not a lease —
28679
+ * which covers the one-frame race between a mark and the supersede that
28680
+ * consumes it.
28681
+ */
28682
+ var NativeLeaseAdmissionSchema = _enum(["all", "inferred"]);
28683
+ object({
28684
+ /**
28685
+ * How long a retained native frame is served before it counts as a miss.
28686
+ *
28687
+ * Must cover the FULL late-crop horizon: detection inference + the
28688
+ * cross-process inference-result hop to hub post-analysis + tracking + the
28689
+ * tRPC crop round-trip back. Below ~500 ms the busiest cameras' subject crops
28690
+ * outrun it and fall back to the ≤640 detection frame; above ~3 s the resident
28691
+ * RAM per busy camera grows linearly with no measured hit-rate gain.
28692
+ */
28693
+ ttlMs: number().int().min(250).max(1e4),
28694
+ /**
28695
+ * Hard per-decode-worker RAM ceiling for retained native frames, in MB.
28696
+ *
28697
+ * Intended as a SAFETY ceiling with the TTL as the effective cap — but check
28698
+ * which one is actually binding before reasoning from that. At the shipped
28699
+ * 1024 MB and a 2 800 ms TTL, a 4K camera hits the CEILING first (~43 frames
28700
+ * at ~24 MB each) and the TTL never gets to expire anything; `leaseMb` /
28701
+ * `leaseFrames` on the metrics line say which. When the ceiling binds, a
28702
+ * change that admits fewer frames buys retention WINDOW at constant RAM
28703
+ * rather than giving RAM back — lower this knob if RAM is what you wanted.
28704
+ * `0` DISABLES the lease entirely and falls the worker back to the tiny
28705
+ * leak-prone GPU surface ring (~85% crop miss; that is what the lease exists
28706
+ * to replace).
28707
+ */
28708
+ budgetMb: number().int().min(0).max(4096),
28709
+ /**
28710
+ * Demand window: eager per-frame native retention runs only within this many
28711
+ * ms of the last native-crop request (or of the dial starting).
28712
+ *
28713
+ * `0` means ALWAYS ON — it disables the gate, it does not disable retention.
28714
+ * That is the legacy behaviour that saturated an N100 (24 native-4K downloads
28715
+ * per second on a camera with zero crop demand), so leave it non-zero unless
28716
+ * you are reproducing that.
28717
+ */
28718
+ activityMs: number().int().min(0).max(12e4),
28719
+ /**
28720
+ * Which delivered frames are retained at all — see
28721
+ * {@link NativeLeaseAdmissionSchema}. This is the only knob of the four that
28722
+ * changes WHAT is kept rather than for how long, so it is also the only one
28723
+ * that can turn a crop that used to hit into a miss. The worker counts every
28724
+ * crop request naming a frame it did NOT see marked
28725
+ * (`leaseUnmarkedCrops` on the session-decode metrics line): a non-zero value
28726
+ * there is the signal that some caller names frames outside the inference set
28727
+ * and that this must go back to `all`.
28728
+ */
28729
+ admission: NativeLeaseAdmissionSchema
28730
+ });
28731
+ /**
28732
+ * The values in force when the operator has set nothing — byte-for-byte the
28733
+ * constants the decode worker shipped with as env-var defaults, so making these
28734
+ * settings changed no behaviour on the day it landed.
28735
+ */
28736
+ var DEFAULT_NATIVE_LEASE_SETTINGS = {
28737
+ ttlMs: 1200,
28738
+ budgetMb: 1024,
28739
+ activityMs: 15e3,
28740
+ admission: "inferred"
28741
+ };
28742
+ DEFAULT_NATIVE_LEASE_SETTINGS.ttlMs;
28743
+ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
28744
+ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
28745
+ DEFAULT_NATIVE_LEASE_SETTINGS.admission;
28136
28746
  //#endregion
28137
28747
  //#region src/server/addon.ts
28138
28748
  var __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -28152,7 +28762,7 @@ var AdminUIAddon = class extends BaseAddon {
28152
28762
  capability: adminUiCapability,
28153
28763
  provider: {
28154
28764
  getStaticDir: async () => ({ staticDir: resolveAdminUiDistDir() }),
28155
- getVersion: async () => ({ version: "1.2.32" })
28765
+ getVersion: async () => ({ version: "1.2.34" })
28156
28766
  }
28157
28767
  }];
28158
28768
  }