@camstack/addon-admin-ui 1.2.32 → 1.2.33

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 (89) hide show
  1. package/dist/assets/{AddLocationWizard-Bqn51iVc.js → AddLocationWizard-BNLzjRkS.js} +1 -1
  2. package/dist/assets/{AddonCollectionPage-D9EE6p80.js → AddonCollectionPage-CvyviITl.js} +1 -1
  3. package/dist/assets/{Addons-Bp55KXZR.js → Addons-B0nG62pB.js} +1 -1
  4. package/dist/assets/{AdminTabs-DB9TozBh.js → AdminTabs-V94RuaJP.js} +1 -1
  5. package/dist/assets/{AiProfiles-C4ji0awJ.js → AiProfiles-CNda-xrz.js} +1 -1
  6. package/dist/assets/{Automation-BmDGNoTh.js → Automation-DYV0gbiV.js} +1 -1
  7. package/dist/assets/{BrokerForm-CtjETVv8.js → BrokerForm-DX_RUCNg.js} +1 -1
  8. package/dist/assets/{Capabilities-BlEUfcGN.js → Capabilities-Cud2I1p1.js} +1 -1
  9. package/dist/assets/{CapabilityBadges-CTZ7zqWM.js → CapabilityBadges-CaKz6Row.js} +1 -1
  10. package/dist/assets/{Cluster-CkGq5KVh.js → Cluster-Cx5dtvDp.js} +1 -1
  11. package/dist/assets/{ClusterRecordingTab-nszIK7qG.js → ClusterRecordingTab-BShu9AFE.js} +1 -1
  12. package/dist/assets/{Dashboard-CO5PTZ9n.js → Dashboard-CN0qzHHd.js} +1 -1
  13. package/dist/assets/{Data-CYwE4-K2.js → Data-Cddnp6Uz.js} +1 -1
  14. package/dist/assets/{DetectionIntelligence-MaxZ_sn8.js → DetectionIntelligence-C4g5DaCG.js} +1 -1
  15. package/dist/assets/{DeviceAggregateSection-C6sscd95.js → DeviceAggregateSection-H5AVeNJ0.js} +1 -1
  16. package/dist/assets/DeviceDetail-CLQTZBsf.js +2 -0
  17. package/dist/assets/{Devices-xlmwhKxI.js → Devices-Bp2S4_jq.js} +1 -1
  18. package/dist/assets/{EmbedPlayerPage-CLGFNg5C.js → EmbedPlayerPage-DVIbQ9_P.js} +1 -1
  19. package/dist/assets/{Events-EfdVWQGS.js → Events-C_TE39Tm.js} +1 -1
  20. package/dist/assets/{FormBuilder-ZuE9B0w9.js → FormBuilder-B3G6L5El.js} +1 -1
  21. package/dist/assets/{Identity-CKR7bmrK.js → Identity-D5YIw7nG.js} +1 -1
  22. package/dist/assets/{IntegrationDetail-DUkC3dFN.js → IntegrationDetail-Dj2OdD1K.js} +1 -1
  23. package/dist/assets/{Integrations-t8-3dqN4.js → Integrations-DZKddVhD.js} +1 -1
  24. package/dist/assets/{Integrations-BuhF2y9p.js → Integrations-fadRG4Km.js} +1 -1
  25. package/dist/assets/{Logs-D1sMKNHu.js → Logs-zJNzZ183.js} +1 -1
  26. package/dist/assets/{MotionZonesSettings-JUP-5dky.js → MotionZonesSettings-4IW50CRy.js} +1 -1
  27. package/dist/assets/{MyAccess-BSkJTbz8.js → MyAccess-CBfHhwSU.js} +1 -1
  28. package/dist/assets/{Network-CXKyrgFS.js → Network-Dx9MFUR0.js} +1 -1
  29. package/dist/assets/{NodeAddonsSettingsPanel-1C9rZfoa.js → NodeAddonsSettingsPanel-C2NjwPws.js} +1 -1
  30. package/dist/assets/{NodeInferenceDevices-D1yIrxri.js → NodeInferenceDevices-uPCEdvuj.js} +1 -1
  31. package/dist/assets/{Notifications-gurR5fDh.js → Notifications-C3Ng_pEz.js} +1 -1
  32. package/dist/assets/{Pipeline-DTKomVZV.js → Pipeline-Daj8GJqM.js} +1 -1
  33. package/dist/assets/{Plates-CQ4aoncO.js → Plates-CdBLHSjv.js} +1 -1
  34. package/dist/assets/{PrivacyMaskSettings-CfvJZ3tP.js → PrivacyMaskSettings-DOgtt7Eb.js} +1 -1
  35. package/dist/assets/{ProviderIcon-ClXY1rL4.js → ProviderIcon-DpSmN8_h.js} +1 -1
  36. package/dist/assets/{Recordings-DglOV8NW.js → Recordings-5kVeLC44.js} +1 -1
  37. package/dist/assets/{ScanNetworkModal-DmZyDQIW.js → ScanNetworkModal-CVNem53G.js} +1 -1
  38. package/dist/assets/{Settings-BVw2pnr7.js → Settings-Dot1aUpP.js} +1 -1
  39. package/dist/assets/{Showroom-9BtpR6nD.js → Showroom-CE0pD_JA.js} +1 -1
  40. package/dist/assets/{Terminal-C-v8B3we.js → Terminal-Jn61QHqq.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-DfmS2X5b.js} +1 -1
  42. package/dist/assets/{build-cluster-graph-6UMcPt27.js → build-cluster-graph-oxCzI_GT.js} +1 -1
  43. package/dist/assets/circle-check-big-bqZqQn_F.js +1 -0
  44. package/dist/assets/clock-BxBK96nq.js +1 -0
  45. package/dist/assets/{copy-BObtPXKC.js → copy-BrMo-O0l.js} +1 -1
  46. package/dist/assets/{debounced-invalidate-Sygznr_T.js → debounced-invalidate-DCfaNC1r.js} +1 -1
  47. package/dist/assets/dist-DPJBx5gf.js +7 -0
  48. package/dist/assets/{dist-W1UImz79.js → dist-w7XvHdqJ.js} +1 -1
  49. package/dist/assets/{download-DsIysR_y.js → download-Bd71YsuA.js} +1 -1
  50. package/dist/assets/{face-bbox-DjqvdhYZ.js → face-bbox-ByAacIlM.js} +1 -1
  51. package/dist/assets/{hostInit-BUHMjqwF.js → hostInit-DhSUBWsx.js} +1 -1
  52. package/dist/assets/index-BIqRUg3I.css +2 -0
  53. package/dist/assets/{index-BqCgqp9V.js → index-BZA0u2Kc.js} +3 -3
  54. package/dist/assets/{key-round-DF1dcL0w.js → key-round-DZL9RdNY.js} +1 -1
  55. package/dist/assets/{mf-entry-bootstrap-0-62a13374.js → mf-entry-bootstrap-0-28bb7ad2.js} +2 -2
  56. package/dist/assets/{pencil-CQwfEybQ.js → pencil-TjwM9NpS.js} +1 -1
  57. package/dist/assets/{player-overlays-CJ2EDXmI.js → player-overlays-CkAkhPbQ.js} +1 -1
  58. package/dist/assets/plus-BgUAuoLj.js +1 -0
  59. package/dist/assets/{radio-C754NrLM.js → radio-DOrncLmg.js} +1 -1
  60. package/dist/assets/{refresh-cw-DAm7QoFj.js → refresh-cw-BYCB135v.js} +1 -1
  61. package/dist/assets/remoteEntry-2xQrocPD.js +1 -0
  62. package/dist/assets/{rotate-ccw-ildFCltM.js → rotate-ccw-Db_qQ-Qx.js} +1 -1
  63. package/dist/assets/rotate-cw-vsmRF4Nv.js +1 -0
  64. package/dist/assets/search-BopXcdUz.js +1 -0
  65. package/dist/assets/{send-BMpnpOJi.js → send-CosR7D5R.js} +1 -1
  66. package/dist/assets/{server-2ReeYjUz.js → server-CUNj6w6P.js} +1 -1
  67. package/dist/assets/square-C0Uc1Hub.js +1 -0
  68. package/dist/assets/{square-check-big-DUv4JqM3.js → square-check-big-yxLWkS9v.js} +1 -1
  69. package/dist/assets/src-Bh5CQXm7.js +7 -0
  70. package/dist/assets/{src-DCsdXZpS.js → src-DOXkuCPk.js} +10 -10
  71. package/dist/assets/{upload-Dvv16Zg3.js → upload-D0orNbud.js} +1 -1
  72. package/dist/assets/{useEventInvalidation-BI2Lc7mp.js → useEventInvalidation-DJ5h5Wvp.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_-Cg_QybrP.js} +2 -2
  74. package/dist/assets/{wifi-cBAahSiZ.js → wifi-D0ItCC1K.js} +1 -1
  75. package/dist/assets/{zap-C86yuB6j.js → zap-CIK4k9g5.js} +1 -1
  76. package/dist/index.html +6 -6
  77. package/dist/server/addon.js +283 -22
  78. package/package.json +1 -1
  79. package/dist/assets/DeviceDetail-KHB2dWaR.js +0 -2
  80. package/dist/assets/circle-check-big-5J4vhHcH.js +0 -1
  81. package/dist/assets/clock-D19QkJv2.js +0 -1
  82. package/dist/assets/dist-Bso6V1TI.js +0 -7
  83. package/dist/assets/index-BH_HqZVJ.css +0 -2
  84. package/dist/assets/plus-BIR1e9Y7.js +0 -1
  85. package/dist/assets/remoteEntry-EVtyXnE6.js +0 -1
  86. package/dist/assets/rotate-cw-DyrhB96Y.js +0 -1
  87. package/dist/assets/search-BFU3RCdH.js +0 -1
  88. package/dist/assets/square-CjeJk_qb.js +0 -1
  89. package/dist/assets/src-YgttllUb.js +0 -7
@@ -7087,7 +7087,7 @@ object({
7087
7087
  * ## This file adds no state
7088
7088
  *
7089
7089
  * Every switch here is a VIEW onto an authority that already existed
7090
- * ([D61](../../../../docs/decisions/adr-0062.md)). The whole point of the
7090
+ * ([D62](../../../../docs/decisions/adr-0062.md)). The whole point of the
7091
7091
  * group is that there is exactly one place each function is turned off, and
7092
7092
  * the group routes to it:
7093
7093
  *
@@ -7098,6 +7098,40 @@ object({
7098
7098
  * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
7099
7099
  * | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
7100
7100
  * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
7101
+ * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
7102
+ * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
7103
+ *
7104
+ * ## The two switches whose authority is not on this server
7105
+ *
7106
+ * `privacy-mask` and `device-audio` write the CAMERA. That is not a loophole
7107
+ * in "the group stores nothing" — it is the purest form of it: the camera
7108
+ * holds the fact, every read is a read-through, and there is no server-side
7109
+ * copy that could drift. Their availability therefore cannot come from
7110
+ * `listBindableCapsForDeviceType` (a device-NATIVE cap carries no wrappers and
7111
+ * is filtered out there); it comes from the cap's own camera-probed
7112
+ * `privacyMask.getOptions()`, which is strictly more honest — it answers for
7113
+ * THIS camera rather than for the device type
7114
+ * ([D74](../../../../docs/decisions/adr-0074.md)).
7115
+ *
7116
+ * ## `privacy-mask` is the one row whose ON is not "the function is working"
7117
+ *
7118
+ * Every other switch means *this camera's function is doing its job*, so
7119
+ * `enabled: false` is a thing an operator took away. `privacy-mask` means **the
7120
+ * MASK is active** — `enabled: true` is video deliberately obscured. The
7121
+ * polarity is not a choice made here: `addon-export-hap`'s privacy `Switch`
7122
+ * (`builders/privacy-switch.ts`) already mirrors `patch.enabled` verbatim, and
7123
+ * a HomeKit toggle that disagreed with the app's toggle for the same camera is
7124
+ * worse than either surface not having one.
7125
+ *
7126
+ * Two consequences follow and both are load-bearing:
7127
+ *
7128
+ * - **It never counts as `switchedOff`.** `countsAsSwitchedOff` is `false` for
7129
+ * exactly this row. With the polarity above, every camera that has NOT drawn
7130
+ * a privacy mask would otherwise report `switchedOff: ['privacy-mask']` — the
7131
+ * normal, healthy state of most cameras rendered as an operator disablement.
7132
+ * - **Its cost line names BOTH directions.** `costWhenOff` is rendered
7133
+ * unconditionally by both clients, so for this row it has to read correctly
7134
+ * whichever way the switch is sitting.
7101
7135
  *
7102
7136
  * The wrapper-binding pair is not a new idea: `legacy-migrations.ts` already
7103
7137
  * migrated the legacy `audioEnabled` / `pipelineEnabled` /
@@ -7116,14 +7150,17 @@ object({
7116
7150
  * `CameraStatus.switchedOff`.
7117
7151
  */
7118
7152
  /**
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
7153
+ * The functions the operator named — five on 2026-08-05, plus the camera's own
7154
+ * microphone on 2026-08-07. Deliberately NOT one id per pipeline step: face
7155
+ * recognition and plate/LPR are per-step toggles on
7121
7156
  * `pipelineOrchestrator.setCameraStepToggle` and belong in the pipeline
7122
- * editor, not in a five-button safety group.
7157
+ * editor, not in a safety group.
7123
7158
  */
7124
7159
  var CameraSwitchIdSchema = _enum([
7125
7160
  "stream-broker",
7126
7161
  "object-detection",
7162
+ "privacy-mask",
7163
+ "device-audio",
7127
7164
  "audio-analysis",
7128
7165
  "recording",
7129
7166
  "notifications"
@@ -7141,14 +7178,26 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7141
7178
  capName: string()
7142
7179
  }),
7143
7180
  object({ kind: literal("recording-config") }),
7144
- object({ kind: literal("notification-mute") })
7181
+ object({ kind: literal("notification-mute") }),
7182
+ object({
7183
+ kind: literal("camera-audio"),
7184
+ capName: string()
7185
+ }),
7186
+ object({
7187
+ kind: literal("camera-mask"),
7188
+ capName: string()
7189
+ })
7145
7190
  ]);
7146
7191
  /**
7147
7192
  * Why a switch is not offered for this camera. Rendered instead of the
7148
7193
  * control, never as a dead control — an absent function and a broken one must
7149
7194
  * not look the same.
7150
7195
  */
7151
- var CameraSwitchUnavailableReasonSchema = _enum(["no-provider", "source-unreachable"]);
7196
+ var CameraSwitchUnavailableReasonSchema = _enum([
7197
+ "no-provider",
7198
+ "source-unreachable",
7199
+ "not-configured"
7200
+ ]);
7152
7201
  /**
7153
7202
  * One switch, resolved for one camera.
7154
7203
  *
@@ -9116,6 +9165,26 @@ var EgressTranscodeRequestSchema = object({
9116
9165
  "h264_mp4toannexb",
9117
9166
  "hevc_mp4toannexb"
9118
9167
  ]).optional(),
9168
+ /**
9169
+ * Publish the transcode as a LOCAL push cam stream, instead of leaving the
9170
+ * consumer to dial the returned url. The broker picks the id and returns it
9171
+ * as `camStreamId` — a caller-supplied one would be circular, since the
9172
+ * sharing key is computed FROM this request.
9173
+ *
9174
+ * The url is still returned and still the contract for a transcode pinned to
9175
+ * another node. But dialling it locally costs an RTSP round trip that changes
9176
+ * the transport underneath the consumer: a dialled stream is an RTP source,
9177
+ * so `isRtpSource()` is true and the session takes the RTP-passthrough +
9178
+ * repacketizer branch. The push branch — the one the derived mechanism has
9179
+ * live hours on — is never reached. Measured on Alexa: broker registered, RTP
9180
+ * arriving, key frame arriving, black screen, on a chain healthy at every
9181
+ * other point.
9182
+ *
9183
+ * Same idea the transport already applies to CALLS, where `classifyCapRoute`
9184
+ * gives priority to `hub-in-process` so a local call never leaves the node.
9185
+ * This is that rule for media.
9186
+ */
9187
+ publishLocally: boolean().optional(),
9119
9188
  pixelFormat: _enum(["yuv420p", "nv12"]).optional(),
9120
9189
  /**
9121
9190
  * Operator/consumer override for decode hardware. ABSENT is the normal case
@@ -9160,7 +9229,13 @@ var EgressTranscodeSchema = object({
9160
9229
  * Returned rather than assumed: a consumer that asked for hardware and got
9161
9230
  * software needs to be able to see that without reading the broker's logs.
9162
9231
  */
9163
- decodeHwAccel: string().nullable()
9232
+ decodeHwAccel: string().nullable(),
9233
+ /**
9234
+ * Set when `publishLocally` was honoured: attach to THIS instead of dialling
9235
+ * `url`, and the session takes the push/deframe transport rather than the
9236
+ * RTP-passthrough one. `null` means the consumer must dial.
9237
+ */
9238
+ camStreamId: string().nullable()
9164
9239
  });
9165
9240
  method(object({
9166
9241
  deviceId: number().int().nonnegative(),
@@ -16921,9 +16996,15 @@ DeviceType.Camera, method(object({
16921
16996
  * Bypass the cache freshness check and fetch directly from the
16922
16997
  * native (or stream-broker fallback). Triggered by the UI's
16923
16998
  * "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.
16999
+ * even when the cache is well within the device's
17000
+ * `snapshotMaxAgeS` window.
17001
+ *
17002
+ * **`force` is an OPERATOR signal, not a freshness preference.** On a
17003
+ * battery camera it is the one thing that walks past the wrapper's
17004
+ * sleep gate and wakes the camera, so a background caller — a poller,
17005
+ * an event handler, a thumbnail — must NEVER set it. Every such caller
17006
+ * gets the cached frame, which on a sleeping battery camera is the
17007
+ * correct answer: stale but honest beats woken.
16927
17008
  */
16928
17009
  force: boolean().optional()
16929
17010
  }), SnapshotImageSchema.nullable()), method(object({ deviceId: number() }), _void(), {
@@ -21205,12 +21286,30 @@ object({
21205
21286
  });
21206
21287
  DeviceType.Sensor;
21207
21288
  /**
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).
21289
+ * PRIVACY what the camera deliberately does not capture. Two planes:
21290
+ *
21291
+ * - **video**: up to `maxRegions` SHAPES the camera blanks out (NOT a cell
21292
+ * grid). Reolink `<shelterList>` zones are rectangles; Hikvision ISAPI
21293
+ * `<RegionCoordinatesList>` zones are free polygons (this camera: exactly
21294
+ * 4 vertices, not necessarily axis-aligned). The cap composes the shared
21295
+ * rect|polygon subset of the MaskShape vocabulary. All coords are
21296
+ * normalized 0..1 (top-left origin).
21297
+ * - **audio**: the camera's microphone. `setAudioEnabled(false)` stops the
21298
+ * camera encoding an audio track at all, so EVERY consumer — live view,
21299
+ * recording, the audio analyzer, an export — sees silent video. There is
21300
+ * no server-side copy of this fact; the camera is the store and every read
21301
+ * is a read-through, which is why a switch over it cannot drift
21302
+ * ([D62](../../../../docs/decisions/adr-0062.md)).
21303
+ *
21304
+ * Both belong here for one reason: they are the two things an operator turns
21305
+ * off when the answer to "what is this camera allowed to record" changes, and
21306
+ * both are applied ON the device, before anything leaves it.
21307
+ *
21308
+ * **The audio flag has exactly one writer.** `stream-params` used to carry a
21309
+ * per-profile `audio` in its patch schema — reachable from no UI and honoured
21310
+ * by one provider — and it was removed when this landed. A second writer onto
21311
+ * one device register is the shape of every knob this repo has shipped that
21312
+ * disagreed with the one the reader read.
21214
21313
  */
21215
21314
  /** A privacy-mask region's geometry — rectangle or free polygon. */
21216
21315
  var PrivacyMaskShapeSchema = discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
@@ -21226,16 +21325,40 @@ object({
21226
21325
  enabled: boolean(),
21227
21326
  /** Active zones (normalized 0..1). Length ≤ maxRegions. */
21228
21327
  regions: array(PrivacyMaskRegionSchema),
21328
+ /**
21329
+ * Is the camera capturing sound right now? Read from the camera, never from
21330
+ * a server-side mirror.
21331
+ *
21332
+ * `null` means "no answer" — either this camera exposes no controllable
21333
+ * microphone (`getOptions().supportsAudioMute === false`) or the read
21334
+ * failed. A consumer must render `null` as UNKNOWN and never as `false`:
21335
+ * "the microphone is off" and "we could not ask" look identical to an
21336
+ * operator only until one of them is wrong.
21337
+ *
21338
+ * On a camera whose profiles carry the flag independently (Reolink writes
21339
+ * it per stream), `true` means AT LEAST ONE profile still carries audio —
21340
+ * privacy is only satisfied when every one of them is silent.
21341
+ */
21342
+ audioEnabled: boolean().nullable(),
21229
21343
  lastFetchedAt: number()
21230
21344
  });
21231
- /** Per-camera availability. */
21345
+ /** Per-camera availability. Probed, never assumed from the model name. */
21232
21346
  var PrivacyMaskOptionsSchema = object({
21233
21347
  /** Maximum number of supported zones. */
21234
21348
  maxRegions: number(),
21235
21349
  /** Shape kinds this camera accepts — Reolink: ['rect']; Hikvision: ['rect','polygon']. */
21236
21350
  supportedShapes: array(MaskShapeKindSchema),
21237
21351
  /** Polygon vertex bounds when 'polygon' is supported (Hikvision: {min:4,max:4}). */
21238
- polygonVertices: MaskPolygonVerticesSchema.optional()
21352
+ polygonVertices: MaskPolygonVerticesSchema.optional(),
21353
+ /**
21354
+ * Does this camera expose a microphone switch we can actually write?
21355
+ *
21356
+ * Camera-probed: `true` only when the firmware answered with an audio flag
21357
+ * we know how to patch. A camera that never answered is `false` — a control
21358
+ * the operator can press that changes nothing is worse than no control, and
21359
+ * the switch group renders "not available" instead.
21360
+ */
21361
+ supportsAudioMute: boolean()
21239
21362
  });
21240
21363
  /** Partial change — every field optional. */
21241
21364
  var PrivacyMaskPatchSchema = object({
@@ -21248,6 +21371,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), PrivacyMaskOptionsSche
21248
21371
  }), _void(), {
21249
21372
  kind: "mutation",
21250
21373
  auth: "admin"
21374
+ }), method(object({
21375
+ deviceId: number(),
21376
+ enabled: boolean()
21377
+ }), _void(), {
21378
+ kind: "mutation",
21379
+ auth: "admin"
21251
21380
  });
21252
21381
  var PtzPresetSchema = object({
21253
21382
  id: string(),
@@ -21457,6 +21586,21 @@ var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
21457
21586
  })]);
21458
21587
  /** Raw bytes of one finalized footage segment (read off disk on the recording node). */
21459
21588
  var ReadSegmentBytesResultSchema = object({ data: _instanceof(Uint8Array) });
21589
+ /**
21590
+ * One GOP of a finalized segment, cut by byte range through the segment's own
21591
+ * `mfra` (D31 on the D42 feeder path). `data` is the `ftyp`+`moov` head plus
21592
+ * the single `moof`+`mdat` covering the requested instant — standalone-
21593
+ * demuxable, never the whole file. When the segment's index cannot be parsed
21594
+ * the provider degrades INSIDE the mechanism to the whole segment (still one
21595
+ * `data`, `gopStartMs` = the segment start) — a worse read, not another path.
21596
+ */
21597
+ var ReadGopBytesResultSchema = object({
21598
+ data: _instanceof(Uint8Array),
21599
+ /** Absolute epoch ms of the returned fragment's first sample. */
21600
+ gopStartMs: number(),
21601
+ /** Media ms the returned fragment covers. */
21602
+ gopDurMs: number()
21603
+ });
21460
21604
  method(object({
21461
21605
  deviceId: number(),
21462
21606
  fromMs: number(),
@@ -21499,6 +21643,14 @@ method(object({
21499
21643
  }), ReadSegmentBytesResultSchema, {
21500
21644
  kind: "query",
21501
21645
  auth: "admin"
21646
+ }), method(object({
21647
+ deviceId: number(),
21648
+ profile: string(),
21649
+ startMs: number(),
21650
+ epochMs: number()
21651
+ }), ReadGopBytesResultSchema, {
21652
+ kind: "query",
21653
+ auth: "admin"
21502
21654
  }), method(object({
21503
21655
  deviceId: number(),
21504
21656
  config: RecordingConfigSchema
@@ -21979,6 +22131,16 @@ var StreamProfileConfigSchema = object({
21979
22131
  "baseline"
21980
22132
  ]).optional(),
21981
22133
  gop: number().optional(),
22134
+ /**
22135
+ * Whether THIS profile currently carries an audio track. READ-ONLY here.
22136
+ *
22137
+ * There is no matching field on {@link StreamProfilePatchSchema}: the
22138
+ * camera's microphone is owned by `privacy-mask` (`setAudioEnabled`), which
22139
+ * writes every profile at once so "audio off" means silent everywhere. A
22140
+ * per-profile writer beside it would let a camera be half-muted and would be
22141
+ * a second knob onto one device register — the failure D62 exists to
22142
+ * prevent. Absent when the firmware does not report the flag.
22143
+ */
21982
22144
  audio: boolean().optional()
21983
22145
  });
21984
22146
  object({
@@ -22019,7 +22181,13 @@ var StreamParamsOptionsSchema = object({
22019
22181
  ext: StreamProfileOptionsSchema.optional()
22020
22182
  });
22021
22183
  /** A partial change to one profile — every field optional; a provider
22022
- * ignores fields it doesn't support. */
22184
+ * ignores fields it doesn't support.
22185
+ *
22186
+ * There is deliberately NO `audio` here. It existed until 2026-08-07,
22187
+ * reachable from no form and honoured by exactly one provider, while the
22188
+ * camera's microphone is a whole-device fact. It now has one writer,
22189
+ * `privacyMask.setAudioEnabled`, which writes every profile — see
22190
+ * `privacy-mask.cap.ts`. */
22023
22191
  var StreamProfilePatchSchema = object({
22024
22192
  width: number().optional(),
22025
22193
  height: number().optional(),
@@ -22032,8 +22200,7 @@ var StreamProfilePatchSchema = object({
22032
22200
  "main",
22033
22201
  "baseline"
22034
22202
  ]).optional(),
22035
- gop: number().optional(),
22036
- audio: boolean().optional()
22203
+ gop: number().optional()
22037
22204
  });
22038
22205
  DeviceType.Camera, method(object({ deviceId: number() }), StreamParamsOptionsSchema), method(object({
22039
22206
  deviceId: number(),
@@ -26545,6 +26712,12 @@ Object.freeze({
26545
26712
  addonId: null,
26546
26713
  access: "view"
26547
26714
  },
26715
+ "privacyMask.setAudioEnabled": {
26716
+ capName: "privacy-mask",
26717
+ capScope: "device",
26718
+ addonId: null,
26719
+ access: "create"
26720
+ },
26548
26721
  "privacyMask.setMask": {
26549
26722
  capName: "privacy-mask",
26550
26723
  capScope: "device",
@@ -26713,6 +26886,12 @@ Object.freeze({
26713
26886
  addonId: null,
26714
26887
  access: "create"
26715
26888
  },
26889
+ "recording.readGopBytes": {
26890
+ capName: "recording",
26891
+ capScope: "system",
26892
+ addonId: null,
26893
+ access: "view"
26894
+ },
26716
26895
  "recording.readSegmentBytes": {
26717
26896
  capName: "recording",
26718
26897
  capScope: "system",
@@ -28133,6 +28312,88 @@ object({
28133
28312
  paddingRatio: .15,
28134
28313
  square: false
28135
28314
  }).paddingRatio;
28315
+ /**
28316
+ * WHICH delivered frames the decode worker retains a native copy of.
28317
+ *
28318
+ * - `all` — every frame the worker delivered to the runner. The shipped
28319
+ * behaviour, and the only correct one if something can ask for a crop of a
28320
+ * frame the runner never sent to inference.
28321
+ * - `inferred` — only the frames the runner ADMITTED to its detection queue.
28322
+ * A native-crop request always names a `frameId` that rode an inference
28323
+ * result, so that is the only set a request can name. How much it drops is
28324
+ * the two-plane governor's admit ratio and nothing else: measured at ~50% on
28325
+ * this cluster, not the ~80% the design sketch assumed, because the governor
28326
+ * was not throttling as hard as the sketch supposed. Read
28327
+ * `leaseAdmitted`/`leaseOffered` off the metrics line for the camera in front
28328
+ * of you rather than quoting a number from here. The newest delivered frame is
28329
+ * croppable regardless — it is still the worker's reserved slot, not a lease —
28330
+ * which covers the one-frame race between a mark and the supersede that
28331
+ * consumes it.
28332
+ */
28333
+ var NativeLeaseAdmissionSchema = _enum(["all", "inferred"]);
28334
+ object({
28335
+ /**
28336
+ * How long a retained native frame is served before it counts as a miss.
28337
+ *
28338
+ * Must cover the FULL late-crop horizon: detection inference + the
28339
+ * cross-process inference-result hop to hub post-analysis + tracking + the
28340
+ * tRPC crop round-trip back. Below ~500 ms the busiest cameras' subject crops
28341
+ * outrun it and fall back to the ≤640 detection frame; above ~3 s the resident
28342
+ * RAM per busy camera grows linearly with no measured hit-rate gain.
28343
+ */
28344
+ ttlMs: number().int().min(250).max(1e4),
28345
+ /**
28346
+ * Hard per-decode-worker RAM ceiling for retained native frames, in MB.
28347
+ *
28348
+ * Intended as a SAFETY ceiling with the TTL as the effective cap — but check
28349
+ * which one is actually binding before reasoning from that. At the shipped
28350
+ * 1024 MB and a 2 800 ms TTL, a 4K camera hits the CEILING first (~43 frames
28351
+ * at ~24 MB each) and the TTL never gets to expire anything; `leaseMb` /
28352
+ * `leaseFrames` on the metrics line say which. When the ceiling binds, a
28353
+ * change that admits fewer frames buys retention WINDOW at constant RAM
28354
+ * rather than giving RAM back — lower this knob if RAM is what you wanted.
28355
+ * `0` DISABLES the lease entirely and falls the worker back to the tiny
28356
+ * leak-prone GPU surface ring (~85% crop miss; that is what the lease exists
28357
+ * to replace).
28358
+ */
28359
+ budgetMb: number().int().min(0).max(4096),
28360
+ /**
28361
+ * Demand window: eager per-frame native retention runs only within this many
28362
+ * ms of the last native-crop request (or of the dial starting).
28363
+ *
28364
+ * `0` means ALWAYS ON — it disables the gate, it does not disable retention.
28365
+ * That is the legacy behaviour that saturated an N100 (24 native-4K downloads
28366
+ * per second on a camera with zero crop demand), so leave it non-zero unless
28367
+ * you are reproducing that.
28368
+ */
28369
+ activityMs: number().int().min(0).max(12e4),
28370
+ /**
28371
+ * Which delivered frames are retained at all — see
28372
+ * {@link NativeLeaseAdmissionSchema}. This is the only knob of the four that
28373
+ * changes WHAT is kept rather than for how long, so it is also the only one
28374
+ * that can turn a crop that used to hit into a miss. The worker counts every
28375
+ * crop request naming a frame it did NOT see marked
28376
+ * (`leaseUnmarkedCrops` on the session-decode metrics line): a non-zero value
28377
+ * there is the signal that some caller names frames outside the inference set
28378
+ * and that this must go back to `all`.
28379
+ */
28380
+ admission: NativeLeaseAdmissionSchema
28381
+ });
28382
+ /**
28383
+ * The values in force when the operator has set nothing — byte-for-byte the
28384
+ * constants the decode worker shipped with as env-var defaults, so making these
28385
+ * settings changed no behaviour on the day it landed.
28386
+ */
28387
+ var DEFAULT_NATIVE_LEASE_SETTINGS = {
28388
+ ttlMs: 1200,
28389
+ budgetMb: 1024,
28390
+ activityMs: 15e3,
28391
+ admission: "inferred"
28392
+ };
28393
+ DEFAULT_NATIVE_LEASE_SETTINGS.ttlMs;
28394
+ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
28395
+ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
28396
+ DEFAULT_NATIVE_LEASE_SETTINGS.admission;
28136
28397
  //#endregion
28137
28398
  //#region src/server/addon.ts
28138
28399
  var __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -28152,7 +28413,7 @@ var AdminUIAddon = class extends BaseAddon {
28152
28413
  capability: adminUiCapability,
28153
28414
  provider: {
28154
28415
  getStaticDir: async () => ({ staticDir: resolveAdminUiDistDir() }),
28155
- getVersion: async () => ({ version: "1.2.32" })
28416
+ getVersion: async () => ({ version: "1.2.33" })
28156
28417
  }
28157
28418
  }];
28158
28419
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-admin-ui",
3
- "version": "1.2.32",
3
+ "version": "1.2.33",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Admin UI — Vite frontend build and server-side addon",