@camstack/addon-osd-manager 0.1.132 → 0.1.133

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 (18) hide show
  1. package/dist/{MotionZonesSettings-BVKF49rs.mjs → MotionZonesSettings-2zsWJZfj.mjs} +2 -2
  2. package/dist/{PrivacyMaskSettings-DoIRAuP_.mjs → PrivacyMaskSettings-DrvVVdy1.mjs} +4 -4
  3. package/dist/{SceneMonitorEditor-3ApQNn3a.mjs → SceneMonitorEditor-BkvmuOvx.mjs} +3 -3
  4. package/dist/_stub.js +11 -11
  5. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-mIgOkpXI.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-DIsyEUg8.mjs} +3 -3
  6. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-dpphyADj.mjs +26 -0
  7. package/dist/{hostInit-vbrbPLQg.mjs → hostInit-BnK9fAeZ.mjs} +2 -2
  8. package/dist/index.js +1237 -981
  9. package/dist/index.mjs +1237 -981
  10. package/dist/{player-overlays-BPpRI9aB.mjs → player-overlays-DrhBQRby.mjs} +1 -1
  11. package/dist/remoteEntry.js +1 -1
  12. package/dist/{responsive-BEcoyxvC.mjs → responsive-D3JdB_Kk.mjs} +1 -1
  13. package/dist/{scene-monitor-copy-1LhlrkfK.mjs → scene-monitor-copy-y4eFIlLX.mjs} +1 -1
  14. package/dist/{square-Bw9o8y6p.mjs → square-CK1KHJ3v.mjs} +1 -1
  15. package/dist/{trash-2-ClX9SCS6.mjs → trash-2-jS-4ZOJJ.mjs} +1 -1
  16. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-BNiwvK5v.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-DgYdzX1B.mjs} +1 -1
  17. package/package.json +1 -1
  18. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D47POQ8_.mjs +0 -26
package/dist/index.mjs CHANGED
@@ -5359,7 +5359,7 @@ var ZodIssueCode = {
5359
5359
  var ZodFirstPartyTypeKind;
5360
5360
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5361
5361
  //#endregion
5362
- //#region ../types/dist/sleep-COWaSCAi.mjs
5362
+ //#region ../types/dist/sleep-PEo0-Fz9.mjs
5363
5363
  /**
5364
5364
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5365
5365
  * window to float samples (D455).
@@ -6495,6 +6495,24 @@ function normalizeAddonInitResult(result) {
6495
6495
  if (Array.isArray(result)) return { providers: result };
6496
6496
  return result;
6497
6497
  }
6498
+ /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
6499
+ var PeerBytesTicketSchema = object({
6500
+ /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
6501
+ url: string().min(1),
6502
+ /**
6503
+ * The HOST node this URL means something on — the hub or a named agent,
6504
+ * never a runner. {@link AddonPeerBytes.open} compares it to its own and
6505
+ * refuses `cross-node` by name when they differ, without dialling.
6506
+ */
6507
+ hostNodeId: string().min(1),
6508
+ expiresAtMs: number().int().nonnegative(),
6509
+ /**
6510
+ * What the producer DECLARED the body to be, when it knows — `null` when it
6511
+ * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
6512
+ * must be able to tell "the producer did not say" from "the body is empty".
6513
+ */
6514
+ declaredBytes: number().int().nonnegative().nullable()
6515
+ });
6498
6516
  /** Shared Zod schemas used across streaming capabilities. */
6499
6517
  var CamProfileSchema = _enum([
6500
6518
  "high",
@@ -7625,24 +7643,6 @@ object({
7625
7643
  unreachable: number()
7626
7644
  })
7627
7645
  });
7628
- /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
7629
- var PeerBytesTicketSchema = object({
7630
- /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
7631
- url: string().min(1),
7632
- /**
7633
- * The HOST node this URL means something on — the hub or a named agent,
7634
- * never a runner. {@link AddonPeerBytes.open} compares it to its own and
7635
- * refuses `cross-node` by name when they differ, without dialling.
7636
- */
7637
- hostNodeId: string().min(1),
7638
- expiresAtMs: number().int().nonnegative(),
7639
- /**
7640
- * What the producer DECLARED the body to be, when it knows — `null` when it
7641
- * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
7642
- * must be able to tell "the producer did not say" from "the body is empty".
7643
- */
7644
- declaredBytes: number().int().nonnegative().nullable()
7645
- });
7646
7646
  /**
7647
7647
  * Adoption job — the background form of `device-adoption.adopt`.
7648
7648
  *
@@ -7754,7 +7754,7 @@ var AdoptionJobSchema = object({
7754
7754
  * component's original options — detection to the detection-pipeline wrapper
7755
7755
  * binding, audio analysis to its own, recording to `RecordingConfig.enabled`
7756
7756
  * (which was always first-class; the switch was a veneer over
7757
- * `recording.setDeviceConfig`), notifications to a notification-center
7757
+ * `recordingArchive.setDeviceConfig`), notifications to a notification-center
7758
7758
  * per-device setting, the two camera planes to their own components.
7759
7759
  *
7760
7760
  * What survives is {@link composeSwitchedOff}: `CameraStatus.switchedOff`, the
@@ -7780,7 +7780,7 @@ var AdoptionJobSchema = object({
7780
7780
  * | `stream-broker` | `deviceManager.setDisabled` | `StreamBrokerManager.reconcileAllCatalogs` releases the brokers; `ensureBroker` refuses re-creation |
7781
7781
  * | `object-detection` | `deviceManager.setWrapperActive('detection-pipeline')` | `PipelineSettingsStore.resolvePipelineForDevice` returns `{ steps: [], audio: null }` |
7782
7782
  * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
7783
- * | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
7783
+ * | `recording` | `recordingArchive.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
7784
7784
  * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
7785
7785
  * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
7786
7786
  * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
@@ -12100,87 +12100,6 @@ var cameraPipelineConfigCapability = {
12100
12100
  exposesDeviceSettings: true,
12101
12101
  methods: {}
12102
12102
  };
12103
- /**
12104
- * The signals a device can emit to WAKE its own stream.
12105
- *
12106
- * A camera whose stream is built on demand sleeps until something asks for it,
12107
- * and "something" cannot be a consumer that is merely attached — a Frigate-style
12108
- * puller holds a session open for ever, and treating that as demand would keep
12109
- * a battery camera awake for ever, which is the whole thing the battery is for
12110
- * (D173). So the wake has to come from the CAMERA: an event it noticed by
12111
- * itself, with no stream running.
12112
- *
12113
- * ## The vocabulary is the PROVIDER'S, not ours
12114
- *
12115
- * Like `consumables`, this cap declares no vocabulary of its own. A provider
12116
- * names each signal with a `code` it chooses and a `label` an operator reads.
12117
- * Reolink offers motion and camera-native detection; another provider may offer
12118
- * a tamper, a doorbell press, a PIR, or something no camera in this fleet has
12119
- * yet. A fixed enum here would mean every new signal is a framework release.
12120
- *
12121
- * It is deliberately NOT derived from the caps a device already binds. Whether
12122
- * a camera CAN push firmware motion is expressed by `motionSources` containing
12123
- * `'onboard'`, and whether it does AI on-camera by the `native-object-detection`
12124
- * binding — but both answer "what drives the detection pipeline", which is a
12125
- * different question from "what may wake a sleeping stream". A camera can do
12126
- * the first and not be trusted with the second, and the operator picks per
12127
- * camera. Two questions, two authorities.
12128
- *
12129
- * ## Availability is not permission
12130
- *
12131
- * `listSignals` says what the device CAN emit. Whether a given signal actually
12132
- * wakes the stream is the operator's per-camera choice, held by the broker
12133
- * alongside the cooldown — see the stream-broker cap's wake settings. A
12134
- * provider declaring a signal is not a provider enabling it.
12135
- */
12136
- /** One signal a device can emit. */
12137
- var StreamSignalSchema = object({
12138
- /** Stable id chosen by the provider, e.g. `'motion'`, `'person'`, `'tamper'`. */
12139
- code: string().min(1),
12140
- /** What an operator reads in the picker. The provider's own wording. */
12141
- label: string().min(1),
12142
- /**
12143
- * Whether the provider recommends this signal ON when a camera is first set
12144
- * up. A provider knows which of its signals are cheap and reliable; an
12145
- * operator should not have to discover that by trial. Reolink recommends
12146
- * both of its own.
12147
- */
12148
- recommended: boolean()
12149
- });
12150
- var StreamSignalsStatusSchema = object({
12151
- signals: array(StreamSignalSchema),
12152
- lastFetchedAt: number()
12153
- });
12154
- var streamSignalsCapability = {
12155
- name: "stream-signals",
12156
- scope: "device",
12157
- deviceNative: true,
12158
- mode: "singleton",
12159
- deviceTypes: Object.values(DeviceType),
12160
- runtimeState: StreamSignalsStatusSchema,
12161
- /**
12162
- * Runtime-state durability: **session** — mirrored in RAM, never written.
12163
- *
12164
- * The slice holds what the DEVICE says it can emit. That is a probed fact,
12165
- * not an operator choice: the provider re-declares it on every registration,
12166
- * so losing it loses nothing and persisting it would freeze an answer the
12167
- * camera is entitled to change. Measured the same day on the sibling case —
12168
- * `native-object-detection.supportedClasses` was persisted, and a firmware
12169
- * class the camera really detected stayed missing for the life of the row
12170
- * because the fix could not reach it.
12171
- *
12172
- * See `RuntimeStateDurability`. Enforced by
12173
- * `scripts/check-runtime-state-durability.ts`.
12174
- */
12175
- durability: "session",
12176
- methods: {
12177
- /**
12178
- * What this device can emit. Empty is a valid and common answer — most
12179
- * cameras have nothing to offer here, and an empty list is what makes the
12180
- * broker's picker show nothing rather than a false choice.
12181
- */
12182
- listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
12183
- };
12184
12103
  /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
12185
12104
  var StreamFormatSchema = _enum([
12186
12105
  "webrtc",
@@ -14305,6 +14224,118 @@ var detectionPipelineCapability = {
14305
14224
  methods: {}
14306
14225
  };
14307
14226
  /**
14227
+ * device-admin-link — "this device has a management page of its own, and here
14228
+ * is its address".
14229
+ *
14230
+ * ## Why this is not a `deviceConfig` cap
14231
+ *
14232
+ * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
14233
+ * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
14234
+ * patch back through a setter; it costs a `builderId` reducer in
14235
+ * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
14236
+ * renders a form section. This cap answers ONE question with ONE read and
14237
+ * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
14238
+ * block, no `settings`, no `runtimeState` and no reducer — exactly like
14239
+ * `reboot`, the other pure-RPC device-native cap.
14240
+ *
14241
+ * ## Absent, and the difference between "no page" and "we cannot say"
14242
+ *
14243
+ * The two are answered at DIFFERENT layers, on purpose:
14244
+ *
14245
+ * - **"We cannot say"** → the provider never registers the cap for that
14246
+ * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
14247
+ * fan are reached only through a vendor cloud; there is no address to hand
14248
+ * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
14249
+ * conditioner DO have a LAN IP, and still have no HTTP management page
14250
+ * behind it. None of them register, so `deviceManager.getBindings` never
14251
+ * lists the cap and no surface asks.
14252
+ * - **"This device has no page, and I know that"** → the provider registers
14253
+ * and `getAdminLink` returns `null`. This is the answer for a device whose
14254
+ * sibling DOES have a page: a Reolink battery camera reached over UDP by
14255
+ * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
14256
+ * transport, a Home Assistant broker authenticated by supervisor token
14257
+ * (which carries no `baseUrl` at all).
14258
+ *
14259
+ * Both draw NOTHING. A button that opens a browser error is worse than no
14260
+ * button, and D62 is the same rule from the other side: an off switch is
14261
+ * reported off, never made to look broken. There is no third state where the
14262
+ * UI renders a disabled button "because the device might have a page".
14263
+ *
14264
+ * ## The URL never carries credentials
14265
+ *
14266
+ * Not in userinfo, not in a query string. Every provider builds through
14267
+ * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
14268
+ * scheme and path as separate arguments — there is no parameter a secret could
14269
+ * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
14270
+ * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
14271
+ * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
14272
+ * keeps providers from hand-rolling one anyway.
14273
+ *
14274
+ * This matters here more than anywhere else in the repo, because every provider
14275
+ * that knows a device's host knows its PASSWORD too: `{ host, port, username,
14276
+ * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
14277
+ * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
14278
+ * camera's own page will ask for its own login. That is correct, and pre-
14279
+ * filling it is the operator's business, not ours.
14280
+ *
14281
+ * ## It is a LAN fact
14282
+ *
14283
+ * The URL addresses the device where the NODE can see it. It is not proxied,
14284
+ * not made reachable from outside, and not sent anywhere. A surface renders it
14285
+ * as a link the operator's own browser follows, on the operator's own network,
14286
+ * or renders nothing.
14287
+ */
14288
+ /**
14289
+ * Whose page is it. The distinction is for the OPERATOR, who needs to know
14290
+ * before clicking whether he is about to land on a camera's own web server or
14291
+ * inside Home Assistant.
14292
+ */
14293
+ var AdminLinkTargetEnum = _enum(["device", "integration"]);
14294
+ var DeviceAdminLinkSchema = object({
14295
+ /**
14296
+ * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
14297
+ * free of userinfo and of any credential-shaped query key.
14298
+ */
14299
+ url: string(),
14300
+ /**
14301
+ * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
14302
+ * The PROVIDER names it, because only the provider knows what the page is;
14303
+ * a UI that invented the label from the addon id would call the Home
14304
+ * Assistant device page "Provider Homeassistant".
14305
+ */
14306
+ label: string(),
14307
+ target: AdminLinkTargetEnum,
14308
+ /**
14309
+ * Host the URL points at, without scheme, port or path — for the tooltip, so
14310
+ * an operator can see WHERE the button goes before he follows it. Redundant
14311
+ * with `url` by construction; carried separately so no surface has to parse
14312
+ * a URL to show it.
14313
+ */
14314
+ host: string()
14315
+ });
14316
+ var deviceAdminLinkCapability = {
14317
+ name: "device-admin-link",
14318
+ scope: "device",
14319
+ deviceNative: true,
14320
+ mode: "singleton",
14321
+ methods: {
14322
+ /**
14323
+ * The device's management page, or `null` when this device has none.
14324
+ *
14325
+ * `auth: 'admin'` deliberately. This is administration, not actuation —
14326
+ * the same bucket as `reboot` and `camera-credentials`, and explicitly NOT
14327
+ * the actuation set `scripts/check-actuation-not-admin.ts` protects (D403).
14328
+ * The URL is also a statement about the LAN, which a household member with
14329
+ * a `view` grant on a light has no reason to be handed.
14330
+ *
14331
+ * The surfaces gate on the QUERY, never on a role they guessed: a caller
14332
+ * without the right loses the query and draws nothing, which is the same
14333
+ * thing a device with no page draws. There is no path on which a button
14334
+ * appears and then fails — the D403 failure mode, from the other end.
14335
+ */
14336
+ getAdminLink: method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
14337
+ };
14338
+ /**
14308
14339
  * Identity envelope for a device's upstream-system metadata.
14309
14340
  *
14310
14341
  * Two jobs:
@@ -14696,118 +14727,6 @@ var deviceAdoptionCapability = {
14696
14727
  }
14697
14728
  };
14698
14729
  /**
14699
- * device-admin-link — "this device has a management page of its own, and here
14700
- * is its address".
14701
- *
14702
- * ## Why this is not a `deviceConfig` cap
14703
- *
14704
- * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
14705
- * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
14706
- * patch back through a setter; it costs a `builderId` reducer in
14707
- * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
14708
- * renders a form section. This cap answers ONE question with ONE read and
14709
- * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
14710
- * block, no `settings`, no `runtimeState` and no reducer — exactly like
14711
- * `reboot`, the other pure-RPC device-native cap.
14712
- *
14713
- * ## Absent, and the difference between "no page" and "we cannot say"
14714
- *
14715
- * The two are answered at DIFFERENT layers, on purpose:
14716
- *
14717
- * - **"We cannot say"** → the provider never registers the cap for that
14718
- * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
14719
- * fan are reached only through a vendor cloud; there is no address to hand
14720
- * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
14721
- * conditioner DO have a LAN IP, and still have no HTTP management page
14722
- * behind it. None of them register, so `deviceManager.getBindings` never
14723
- * lists the cap and no surface asks.
14724
- * - **"This device has no page, and I know that"** → the provider registers
14725
- * and `getAdminLink` returns `null`. This is the answer for a device whose
14726
- * sibling DOES have a page: a Reolink battery camera reached over UDP by
14727
- * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
14728
- * transport, a Home Assistant broker authenticated by supervisor token
14729
- * (which carries no `baseUrl` at all).
14730
- *
14731
- * Both draw NOTHING. A button that opens a browser error is worse than no
14732
- * button, and D62 is the same rule from the other side: an off switch is
14733
- * reported off, never made to look broken. There is no third state where the
14734
- * UI renders a disabled button "because the device might have a page".
14735
- *
14736
- * ## The URL never carries credentials
14737
- *
14738
- * Not in userinfo, not in a query string. Every provider builds through
14739
- * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
14740
- * scheme and path as separate arguments — there is no parameter a secret could
14741
- * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
14742
- * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
14743
- * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
14744
- * keeps providers from hand-rolling one anyway.
14745
- *
14746
- * This matters here more than anywhere else in the repo, because every provider
14747
- * that knows a device's host knows its PASSWORD too: `{ host, port, username,
14748
- * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
14749
- * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
14750
- * camera's own page will ask for its own login. That is correct, and pre-
14751
- * filling it is the operator's business, not ours.
14752
- *
14753
- * ## It is a LAN fact
14754
- *
14755
- * The URL addresses the device where the NODE can see it. It is not proxied,
14756
- * not made reachable from outside, and not sent anywhere. A surface renders it
14757
- * as a link the operator's own browser follows, on the operator's own network,
14758
- * or renders nothing.
14759
- */
14760
- /**
14761
- * Whose page is it. The distinction is for the OPERATOR, who needs to know
14762
- * before clicking whether he is about to land on a camera's own web server or
14763
- * inside Home Assistant.
14764
- */
14765
- var AdminLinkTargetEnum = _enum(["device", "integration"]);
14766
- var DeviceAdminLinkSchema = object({
14767
- /**
14768
- * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
14769
- * free of userinfo and of any credential-shaped query key.
14770
- */
14771
- url: string(),
14772
- /**
14773
- * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
14774
- * The PROVIDER names it, because only the provider knows what the page is;
14775
- * a UI that invented the label from the addon id would call the Home
14776
- * Assistant device page "Provider Homeassistant".
14777
- */
14778
- label: string(),
14779
- target: AdminLinkTargetEnum,
14780
- /**
14781
- * Host the URL points at, without scheme, port or path — for the tooltip, so
14782
- * an operator can see WHERE the button goes before he follows it. Redundant
14783
- * with `url` by construction; carried separately so no surface has to parse
14784
- * a URL to show it.
14785
- */
14786
- host: string()
14787
- });
14788
- var deviceAdminLinkCapability = {
14789
- name: "device-admin-link",
14790
- scope: "device",
14791
- deviceNative: true,
14792
- mode: "singleton",
14793
- methods: {
14794
- /**
14795
- * The device's management page, or `null` when this device has none.
14796
- *
14797
- * `auth: 'admin'` deliberately. This is administration, not actuation —
14798
- * the same bucket as `reboot` and `camera-credentials`, and explicitly NOT
14799
- * the actuation set `scripts/check-actuation-not-admin.ts` protects (D403).
14800
- * The URL is also a statement about the LAN, which a household member with
14801
- * a `view` grant on a light has no reason to be handed.
14802
- *
14803
- * The surfaces gate on the QUERY, never on a role they guessed: a caller
14804
- * without the right loses the query and draws nothing, which is the same
14805
- * thing a device with no page draws. There is no path on which a button
14806
- * appears and then fails — the D403 failure mode, from the other end.
14807
- */
14808
- getAdminLink: method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
14809
- };
14810
- /**
14811
14730
  * `device-export` — collection cap for addons that export camstack
14812
14731
  * devices to external ecosystems (HomeAssistant via MQTT discovery,
14813
14732
  * HomeKit/HAP, Alexa Smart Home, …).
@@ -28448,6 +28367,87 @@ var storageProviderCapability = {
28448
28367
  })
28449
28368
  }
28450
28369
  };
28370
+ /**
28371
+ * The signals a device can emit to WAKE its own stream.
28372
+ *
28373
+ * A camera whose stream is built on demand sleeps until something asks for it,
28374
+ * and "something" cannot be a consumer that is merely attached — a Frigate-style
28375
+ * puller holds a session open for ever, and treating that as demand would keep
28376
+ * a battery camera awake for ever, which is the whole thing the battery is for
28377
+ * (D173). So the wake has to come from the CAMERA: an event it noticed by
28378
+ * itself, with no stream running.
28379
+ *
28380
+ * ## The vocabulary is the PROVIDER'S, not ours
28381
+ *
28382
+ * Like `consumables`, this cap declares no vocabulary of its own. A provider
28383
+ * names each signal with a `code` it chooses and a `label` an operator reads.
28384
+ * Reolink offers motion and camera-native detection; another provider may offer
28385
+ * a tamper, a doorbell press, a PIR, or something no camera in this fleet has
28386
+ * yet. A fixed enum here would mean every new signal is a framework release.
28387
+ *
28388
+ * It is deliberately NOT derived from the caps a device already binds. Whether
28389
+ * a camera CAN push firmware motion is expressed by `motionSources` containing
28390
+ * `'onboard'`, and whether it does AI on-camera by the `native-object-detection`
28391
+ * binding — but both answer "what drives the detection pipeline", which is a
28392
+ * different question from "what may wake a sleeping stream". A camera can do
28393
+ * the first and not be trusted with the second, and the operator picks per
28394
+ * camera. Two questions, two authorities.
28395
+ *
28396
+ * ## Availability is not permission
28397
+ *
28398
+ * `listSignals` says what the device CAN emit. Whether a given signal actually
28399
+ * wakes the stream is the operator's per-camera choice, held by the broker
28400
+ * alongside the cooldown — see the stream-broker cap's wake settings. A
28401
+ * provider declaring a signal is not a provider enabling it.
28402
+ */
28403
+ /** One signal a device can emit. */
28404
+ var StreamSignalSchema = object({
28405
+ /** Stable id chosen by the provider, e.g. `'motion'`, `'person'`, `'tamper'`. */
28406
+ code: string().min(1),
28407
+ /** What an operator reads in the picker. The provider's own wording. */
28408
+ label: string().min(1),
28409
+ /**
28410
+ * Whether the provider recommends this signal ON when a camera is first set
28411
+ * up. A provider knows which of its signals are cheap and reliable; an
28412
+ * operator should not have to discover that by trial. Reolink recommends
28413
+ * both of its own.
28414
+ */
28415
+ recommended: boolean()
28416
+ });
28417
+ var StreamSignalsStatusSchema = object({
28418
+ signals: array(StreamSignalSchema),
28419
+ lastFetchedAt: number()
28420
+ });
28421
+ var streamSignalsCapability = {
28422
+ name: "stream-signals",
28423
+ scope: "device",
28424
+ deviceNative: true,
28425
+ mode: "singleton",
28426
+ deviceTypes: Object.values(DeviceType),
28427
+ runtimeState: StreamSignalsStatusSchema,
28428
+ /**
28429
+ * Runtime-state durability: **session** — mirrored in RAM, never written.
28430
+ *
28431
+ * The slice holds what the DEVICE says it can emit. That is a probed fact,
28432
+ * not an operator choice: the provider re-declares it on every registration,
28433
+ * so losing it loses nothing and persisting it would freeze an answer the
28434
+ * camera is entitled to change. Measured the same day on the sibling case —
28435
+ * `native-object-detection.supportedClasses` was persisted, and a firmware
28436
+ * class the camera really detected stayed missing for the life of the row
28437
+ * because the fix could not reach it.
28438
+ *
28439
+ * See `RuntimeStateDurability`. Enforced by
28440
+ * `scripts/check-runtime-state-durability.ts`.
28441
+ */
28442
+ durability: "session",
28443
+ methods: {
28444
+ /**
28445
+ * What this device can emit. Empty is a valid and common answer — most
28446
+ * cameras have nothing to offer here, and an empty list is what makes the
28447
+ * broker's picker show nothing rather than a false choice.
28448
+ */
28449
+ listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
28450
+ };
28451
28451
  /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
28452
28452
  var ProfileSettingsSchemaBridge = unknown().nullable();
28453
28453
  var ProfileSettingsBagSchema = record(string(), unknown());
@@ -31681,6 +31681,179 @@ getCredentials: method(object({ deviceId: number() }), CameraCredentialsSchema.n
31681
31681
  }
31682
31682
  };
31683
31683
  /**
31684
+ * camera-grid-layout — the geometry of a COMPOSITE camera, on the camera's own
31685
+ * page.
31686
+ *
31687
+ * ## Why this is a capability and not an addon settings schema
31688
+ *
31689
+ * It was one, and it did not render. The addon declared the editor as a
31690
+ * `type: 'widget'` field inside its own `deviceSettingsSchema()`; the hub
31691
+ * returned that section correctly and `ConfigFormField` renders `type:'widget'`
31692
+ * perfectly well — and nothing ever asked camera-grid for it. The Cluster →
31693
+ * Pipeline → Device Overrides page interrogates a HAND-WRITTEN list of four
31694
+ * addons (`PIPELINE_CLUSTER_DEVICE_ADDONS`), whose own comment says an addon
31695
+ * not on it "falls off silently".
31696
+ *
31697
+ * Adding a fifth name to that list would have been the wrong fix twice over:
31698
+ * that page is per-camera DETECTION tuning, and a grid's geometry belongs
31699
+ * beside PTZ and motion zones on the camera itself. The device page is
31700
+ * BINDING-driven (D12), so the way in is a capability bound to the device —
31701
+ * and this cap carries its section the way `recording` does, by RETURNING it
31702
+ * from `getDeviceSettingsContribution`.
31703
+ *
31704
+ * Seven other widgets are still declared the other way, through a
31705
+ * `deviceConfig.ui` block the framework derives a section from. That route
31706
+ * gives the addon no say in where its own panel lands and no way to decline
31707
+ * for a device the panel does not suit, which is why this one does not use it.
31708
+ *
31709
+ * ## Why one addon may implement it
31710
+ *
31711
+ * It is a device-scoped NATIVE cap, registered by the grid camera device
31712
+ * itself. Nothing else declares a composite camera, so nothing else has a
31713
+ * layout — and the device-scoped route means the widget asks THE camera, not
31714
+ * "the camera-grid addon", which is what let the old custom-action pair be
31715
+ * reached only by a caller that already knew the addon id.
31716
+ *
31717
+ * ## The tab
31718
+ *
31719
+ * `streaming`, not a top-tab of its own. A grid's geometry IS what its stream
31720
+ * is, so the Streaming tab is where it belongs; a `grid` top-tab would need an
31721
+ * entry in `WELL_KNOWN_TAB_MAP` or the device page renders the raw id as the
31722
+ * label (measured on the robot camera, 2026-09-06 — a tab called "navigation"
31723
+ * next to "PTZ").
31724
+ */
31725
+ /** A rectangle in normalized [0,1] coordinates of whatever contains it. */
31726
+ var GridNormalizedRectSchema = object({
31727
+ x: number().min(0).max(1),
31728
+ y: number().min(0).max(1),
31729
+ width: number().gt(0).max(1),
31730
+ height: number().gt(0).max(1)
31731
+ });
31732
+ /**
31733
+ * One source camera, the part of its picture taken, and where that part lands.
31734
+ *
31735
+ * Both rectangles are NORMALIZED (D519): a source camera can change resolution
31736
+ * — a profile switch, a firmware update, a substream that comes back different
31737
+ * — and a stored PIXEL rectangle would quietly start cutting the wrong region,
31738
+ * which is the class of bug nobody files.
31739
+ */
31740
+ var GridLayoutCellSchema = object({
31741
+ deviceId: number().int().positive(),
31742
+ /** The part of the SOURCE taken, normalized against the source. */
31743
+ source: GridNormalizedRectSchema,
31744
+ /** Where it lands, normalized against the CANVAS. */
31745
+ cell: GridNormalizedRectSchema
31746
+ });
31747
+ /**
31748
+ * Which profiles this grid can actually compose, and why not.
31749
+ *
31750
+ * A grid's `high` composes its sources' `high` and its `low` their `low`, so a
31751
+ * profile is on offer only when EVERY source can serve it. The refusal NAMES
31752
+ * the sources, because "this grid has no low" is not a finding — "615 has no
31753
+ * low" is, and it is the one an operator can act on.
31754
+ */
31755
+ var GridProfileOfferSchema = object({
31756
+ profile: _enum([
31757
+ "high",
31758
+ "mid",
31759
+ "low"
31760
+ ]),
31761
+ offered: boolean(),
31762
+ /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
31763
+ missingSources: array(number().int().positive()),
31764
+ /**
31765
+ * The canvas this profile composes onto, `WxH`, or empty when it is not
31766
+ * offered. DERIVED from the cells and the sources' own size at this profile —
31767
+ * it is reported because nothing else in the system would ever say what the
31768
+ * grid came out as, and because it is the number an operator would otherwise
31769
+ * expect to type.
31770
+ */
31771
+ canvas: string(),
31772
+ /**
31773
+ * Whether this profile is PUBLISHED, of the ones the grid could serve.
31774
+ *
31775
+ * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
31776
+ * a 4K canvas built from 4K decodes — something to opt into, not something a
31777
+ * viewer's adaptive should be handed by climbing to the top rung it can see.
31778
+ * Default is `mid` + `low`.
31779
+ */
31780
+ published: boolean()
31781
+ });
31782
+ var GridLayoutViewSchema = object({
31783
+ /** The persisted grid row this camera was declared from. */
31784
+ instanceId: string(),
31785
+ deviceId: number().int().nonnegative(),
31786
+ name: string(),
31787
+ /**
31788
+ * NO canvas size. A grid's resolution is not authored: each profile derives
31789
+ * its own from the cells and its sources' dimensions. The two numbers that
31790
+ * used to be here were a text field that silently decided both how much the
31791
+ * composite cost and how sharp it was — see `profiles[].canvas` for what it
31792
+ * came out as.
31793
+ */
31794
+ fps: number().int(),
31795
+ cells: array(GridLayoutCellSchema),
31796
+ /** What the catalog will publish, and what it refuses to. Read-only. */
31797
+ profiles: array(GridProfileOfferSchema)
31798
+ });
31799
+ var GridLayoutPatchSchema = object({
31800
+ deviceId: number().int().nonnegative(),
31801
+ name: string().min(1).max(160).optional(),
31802
+ fps: number().int().min(1).max(60).optional(),
31803
+ /** Which profiles to publish. See `GridProfileOffer.published`. */
31804
+ publishedProfiles: array(_enum([
31805
+ "high",
31806
+ "mid",
31807
+ "low"
31808
+ ])).max(3).optional(),
31809
+ /**
31810
+ * The whole cell list at once. A per-cell patch would need an ordering the
31811
+ * editor does not have, and a half-applied layout is a picture nobody asked
31812
+ * for.
31813
+ */
31814
+ cells: array(GridLayoutCellSchema).max(16)
31815
+ });
31816
+ var cameraGridLayoutCapability = {
31817
+ name: "camera-grid-layout",
31818
+ scope: "device",
31819
+ deviceNative: true,
31820
+ mode: "singleton",
31821
+ deviceTypes: [DeviceType.Camera],
31822
+ /**
31823
+ * The section is built by the ADDON and returned from
31824
+ * `getDeviceSettingsContribution`, not derived by the framework from a
31825
+ * `deviceConfig.ui` block.
31826
+ *
31827
+ * Both mechanisms render the same widget. This one hands the addon two
31828
+ * things the framework-derived route cannot give it:
31829
+ *
31830
+ * - it chooses its own section, `tab`, `location` and `order`, the way any
31831
+ * other setting does, instead of receiving them from a cap declaration;
31832
+ * - it can DECLINE per device. A camera that is not a grid gets no section
31833
+ * at all, rather than a widget that renders its own "not a grid" state.
31834
+ *
31835
+ * `recording` is the precedent (`recorder/recording-device-settings.ts`): it
31836
+ * returns `null` for anything that is not a camera, so the Recording tab
31837
+ * never appears there.
31838
+ */
31839
+ exposesDeviceSettings: true,
31840
+ methods: {
31841
+ /**
31842
+ * The grid behind this device.
31843
+ *
31844
+ * `null` means ANSWERED and this camera is not a grid — not "not yet
31845
+ * known". The widget renders its "this is not a grid camera" state only
31846
+ * from this answer, never from an unresolved query (D315).
31847
+ */
31848
+ getLayout: method(object({ deviceId: number().int().nonnegative() }), GridLayoutViewSchema.nullable(), { auth: "admin" }),
31849
+ /** Write the geometry back. Returns the grid as it now stands, profiles included. */
31850
+ saveLayout: method(GridLayoutPatchSchema, GridLayoutViewSchema, {
31851
+ kind: "mutation",
31852
+ auth: "admin"
31853
+ })
31854
+ }
31855
+ };
31856
+ /**
31684
31857
  * Carbon-monoxide alarm sensor. Drives Home Assistant `binary_sensor`
31685
31858
  * entries with `device_class: carbon_monoxide`. Push-driven.
31686
31859
  */
@@ -32491,346 +32664,6 @@ var dayNightCapability = {
32491
32664
  volatileStateFields: ["lastFetchedAt"]
32492
32665
  };
32493
32666
  /**
32494
- * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
32495
- * writes to the CAMERA's own card, on the camera's own schedule.
32496
- *
32497
- * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
32498
- * footage ledger, our storage locations, our retention. This one has a
32499
- * different authority — the camera's firmware — and per D62 it stores
32500
- * nothing of its own. Every value here is read from the camera and every
32501
- * write goes back to the camera; there is no CamStack-side mirror that
32502
- * could disagree with the device.
32503
- *
32504
- * ## One shape, two firmwares
32505
- *
32506
- * Measured 2026-09-22 against the live fleet:
32507
- *
32508
- * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
32509
- * | --- | --- | --- |
32510
- * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
32511
- * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
32512
- * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
32513
- * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
32514
- * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
32515
- * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
32516
- * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
32517
- * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
32518
- *
32519
- * The two schedule models look different and are the same thing in
32520
- * different coordinates: both answer "for this trigger, during which
32521
- * weekly windows does the camera record". {@link RecordWindow} is that
32522
- * question in one shape — Hikvision's ranges map straight onto it,
32523
- * Reolink's mask expands into hour-aligned windows.
32524
- *
32525
- * ## Union, not intersection
32526
- *
32527
- * **The same fields exist on every camera.** What differs per device is
32528
- * which VALUES that device accepts, and that is what {@link
32529
- * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
32530
- * per field plus the schedule's own limits. A control a camera cannot
32531
- * honour is rendered DISABLED WITH ITS REASON, never missing and never
32532
- * dead: disabled must not look like broken.
32533
- *
32534
- * ## Refusal by name
32535
- *
32536
- * A write a camera cannot honour is refused with a sentence the operator
32537
- * can read — never accepted and dropped. Both providers refuse through
32538
- * {@link describeOnboardRefusal}, so the vocabulary is one function and
32539
- * one test, not two hand-written vendor opinions.
32540
- *
32541
- * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
32542
- * `getOptions` advertises per-camera availability, `getStatus` (auto-
32543
- * injected from `status`) reports the live values, and a single
32544
- * `setSettings` mutation applies a partial change. No hand-written
32545
- * settings-contribution methods.
32546
- */
32547
- /**
32548
- * What makes the camera start recording during a window.
32549
- *
32550
- * The union of both vendors' vocabularies. `continuous` is Hikvision's
32551
- * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
32552
- * object-class triggers are Reolink-only today and the smart-event ones
32553
- * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
32554
- * firmwares measured — a camera that cannot record on a trigger simply
32555
- * does not list it in `options.schedule.triggers`, and a window naming
32556
- * it is REFUSED, not dropped.
32557
- */
32558
- var RecordTriggerSchema = _enum([
32559
- "continuous",
32560
- "motion",
32561
- "person",
32562
- "vehicle",
32563
- "animal",
32564
- "lineCrossing",
32565
- "intrusion",
32566
- "loitering",
32567
- "alarmInput"
32568
- ]);
32569
- /**
32570
- * One weekly recording window: "on `day`, from `startMinute` to
32571
- * `endMinute`, record on `trigger`".
32572
- *
32573
- * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
32574
- * both firmwares enumerate). Minutes are local camera time since
32575
- * midnight; `endMinute` may be 1440, meaning end of day — that is
32576
- * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
32577
- * collapsing it to 0 would turn a whole-day window into an empty one.
32578
- */
32579
- var RecordWindowSchema = object({
32580
- trigger: RecordTriggerSchema,
32581
- day: number().int().min(0).max(6),
32582
- startMinute: number().int().min(0).max(1439),
32583
- endMinute: number().int().min(1).max(1440)
32584
- });
32585
- /** Status of one physical volume, as the camera itself describes it. */
32586
- var OnboardStorageVolumeSchema = object({
32587
- /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
32588
- id: string(),
32589
- /** The camera's own name for it, when it gives one (`hddName`). */
32590
- label: string().optional(),
32591
- status: _enum([
32592
- "ok",
32593
- "unformatted",
32594
- "error",
32595
- "offline",
32596
- "unknown"
32597
- ]),
32598
- /**
32599
- * Total size in MB, or **null when the camera did not say**.
32600
- *
32601
- * Never 0 for an unreadable value: a measurement that failed is not a
32602
- * measurement (D393), and a card whose size is unknown must not be
32603
- * rendered as a card of size zero.
32604
- */
32605
- capacityMb: number().nullable(),
32606
- /**
32607
- * Free space in MB, or null when unknown.
32608
- *
32609
- * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
32610
- * 1439 both report exactly 11776 MB free — the fixed reserve a looping
32611
- * card converges on once it has wrapped. At loop steady state the
32612
- * number is identical whether the camera recorded yesterday or stopped
32613
- * a month ago.
32614
- */
32615
- freeMb: number().nullable(),
32616
- /** True when the camera reports the volume writable (`property` RW). */
32617
- writable: boolean().optional()
32618
- });
32619
- /**
32620
- * What the camera is doing with its own storage, right now.
32621
- *
32622
- * Every scalar is nullable and **null means the camera did not answer**,
32623
- * never a default. A form that seeds `0` from an unanswered read invites
32624
- * the operator to save that 0 back onto the camera.
32625
- */
32626
- var RecordingOnboardStatusSchema = object({
32627
- storage: discriminatedUnion("kind", [
32628
- object({
32629
- kind: literal("present"),
32630
- volumes: array(OnboardStorageVolumeSchema)
32631
- }),
32632
- object({
32633
- kind: literal("absent"),
32634
- reason: string()
32635
- }),
32636
- object({
32637
- kind: literal("unknown"),
32638
- reason: string()
32639
- })
32640
- ]),
32641
- tracks: array(object({
32642
- id: string(),
32643
- enabled: boolean(),
32644
- isVideo: boolean(),
32645
- /** From the camera's own track description. Null when it does not say. */
32646
- codec: string().nullable(),
32647
- resolution: string().nullable(),
32648
- /** Per-track overwrite flag, where the firmware keeps it per track. */
32649
- overwriteWhenFull: boolean().nullable()
32650
- })),
32651
- /**
32652
- * The track the write path targets — the enabled VIDEO one. Null when
32653
- * no track could be identified, which is itself a refusal reason.
32654
- */
32655
- primaryTrackId: string().nullable(),
32656
- /** Master "record to the card at all" switch. */
32657
- enabled: boolean().nullable(),
32658
- overwriteWhenFull: boolean().nullable(),
32659
- preRecordSec: number().nullable(),
32660
- postRecordSec: number().nullable(),
32661
- /** Length of one recorded file, in minutes. */
32662
- segmentMinutes: number().nullable(),
32663
- /** The primary track's weekly windows, flattened. */
32664
- windows: array(RecordWindowSchema),
32665
- /**
32666
- * How many windows the camera described that CamStack could NOT read —
32667
- * an unrecognised trigger, an unparseable clock, a weekday it does not
32668
- * name.
32669
- *
32670
- * A dropped window is work the reader threw away, and a schedule that
32671
- * silently shows fewer rows than the camera holds is how an operator
32672
- * saves back a schedule shorter than the one they were looking at
32673
- * (D391). Non-zero means the window list is INCOMPLETE and a write
32674
- * that replaces it would delete what was not shown — which is why a
32675
- * provider reporting a non-zero count also reports the schedule as not
32676
- * writable.
32677
- */
32678
- unreadableWindows: number(),
32679
- /**
32680
- * The camera is scheduled to record and has NO usable storage.
32681
- *
32682
- * A first-class fact because it is the fleet's most common silent
32683
- * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
32684
- * to a card that is not there. Neither the schedule nor the storage
32685
- * read says anything wrong on its own; only the pair does.
32686
- */
32687
- recordingToNowhere: boolean(),
32688
- lastFetchedAt: number()
32689
- });
32690
- /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
32691
- var RangeSchema = object({
32692
- min: number(),
32693
- max: number(),
32694
- step: number()
32695
- });
32696
- /**
32697
- * The values a camera actually takes for a numeric field, when they are a SET
32698
- * rather than a range.
32699
- *
32700
- * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
32701
- * (I91DN) on 2026-09-22 by writing each value and reading it back:
32702
- *
32703
- * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
32704
- * camera's "no limit" — `-1` and `4294967295` both land on it);
32705
- * - post-record: `5, 10, 30, 60, 120, 300, 600`.
32706
- *
32707
- * Neither is expressible as a step: the first has a sentinel two billion away
32708
- * from its neighbours, the second doubles and then jumps. A range that tried
32709
- * would forbid values the camera takes AND permit values it silently replaces
32710
- * with 5 — wrong in both directions at once.
32711
- *
32712
- * `sentinel` names the member that is not a duration, so a surface can render
32713
- * "no limit" instead of `2147483647` seconds.
32714
- */
32715
- var AllowedValuesSchema = object({
32716
- values: array(number()).min(1),
32717
- sentinel: object({
32718
- value: number(),
32719
- meaning: _enum(["no-limit", "disabled"])
32720
- }).optional()
32721
- });
32722
- /**
32723
- * Per-field availability on ONE camera.
32724
- *
32725
- * The field exists on every camera — this says whether this one can be
32726
- * read and whether it can be written, and `reason` says why not when
32727
- * either is false. The UI renders the control DISABLED with the reason
32728
- * rather than hiding it, so a limitation is legible instead of looking
32729
- * like a missing feature.
32730
- */
32731
- var OnboardFieldSupportSchema = object({
32732
- readable: boolean(),
32733
- writable: boolean(),
32734
- /** Required whenever `readable` or `writable` is false. */
32735
- reason: string().optional()
32736
- });
32737
- /** What this camera's schedule model can express. */
32738
- var OnboardScheduleSupportSchema = object({
32739
- support: OnboardFieldSupportSchema,
32740
- /**
32741
- * The smallest time step the camera can express, in minutes.
32742
- *
32743
- * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
32744
- * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
32745
- * window whose edges are not a multiple of this is REFUSED rather than
32746
- * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
32747
- * and nothing says so.
32748
- */
32749
- granularityMinutes: number(),
32750
- /** Triggers this camera can record on. A window naming another is refused. */
32751
- triggers: array(RecordTriggerSchema),
32752
- /**
32753
- * False when the camera stores ONE trigger per time range, so two
32754
- * windows overlapping on the same day cannot carry different triggers.
32755
- * True on Reolink, whose mask is per-trigger and independent.
32756
- */
32757
- supportsOverlappingTriggers: boolean()
32758
- });
32759
- var RecordingOnboardOptionsSchema = object({
32760
- enabled: OnboardFieldSupportSchema,
32761
- overwriteWhenFull: OnboardFieldSupportSchema,
32762
- preRecordSec: OnboardFieldSupportSchema,
32763
- preRecordSecRange: RangeSchema.optional(),
32764
- /** Preferred over the range when the camera takes a SET, not a span. */
32765
- preRecordSecAllowed: AllowedValuesSchema.optional(),
32766
- postRecordSec: OnboardFieldSupportSchema,
32767
- postRecordSecRange: RangeSchema.optional(),
32768
- /** Preferred over the range when the camera takes a SET, not a span. */
32769
- postRecordSecAllowed: AllowedValuesSchema.optional(),
32770
- segmentMinutes: OnboardFieldSupportSchema,
32771
- segmentMinutesRange: RangeSchema.optional(),
32772
- /** Preferred over the range when the camera takes a SET, not a span. */
32773
- segmentMinutesAllowed: AllowedValuesSchema.optional(),
32774
- schedule: OnboardScheduleSupportSchema
32775
- });
32776
- /**
32777
- * A partial change. Every field optional.
32778
- *
32779
- * Unlike the other `deviceConfig` caps, a provider here does **NOT**
32780
- * silently ignore a field it cannot support — it refuses, by name,
32781
- * through {@link describeOnboardRefusal}. Silence on a recording setting
32782
- * is the failure D62 exists to prevent: the operator believes the camera
32783
- * is recording the way the form says, and it is not.
32784
- */
32785
- var RecordingOnboardPatchSchema = object({
32786
- enabled: boolean().optional(),
32787
- overwriteWhenFull: boolean().optional(),
32788
- preRecordSec: number().optional(),
32789
- postRecordSec: number().optional(),
32790
- segmentMinutes: number().optional(),
32791
- /** The complete new window set for the primary track — not a delta. */
32792
- windows: array(RecordWindowSchema).optional()
32793
- });
32794
- var recordingOnboardCapability = {
32795
- name: "recording-onboard",
32796
- scope: "device",
32797
- deviceNative: true,
32798
- mode: "singleton",
32799
- deviceTypes: [DeviceType.Camera],
32800
- deviceConfig: { ui: {
32801
- kind: "derived-form",
32802
- builderId: "recording-onboard",
32803
- tab: "recording"
32804
- } },
32805
- methods: {
32806
- getOptions: method(object({ deviceId: number() }), RecordingOnboardOptionsSchema),
32807
- setSettings: method(object({
32808
- deviceId: number(),
32809
- settings: RecordingOnboardPatchSchema
32810
- }), _void(), {
32811
- kind: "mutation",
32812
- auth: "admin"
32813
- })
32814
- },
32815
- status: {
32816
- schema: RecordingOnboardStatusSchema,
32817
- kind: "poll"
32818
- },
32819
- runtimeState: RecordingOnboardStatusSchema,
32820
- /**
32821
- * Runtime-state durability: **restored** — operator-set camera-side
32822
- * recording config; mutation-driven, and the storage half is the last
32823
- * thing the camera said about its own card.
32824
- *
32825
- * See `RuntimeStateDurability`. Enforced by
32826
- * `scripts/check-runtime-state-durability.ts`.
32827
- */
32828
- durability: "restored",
32829
- /** Clock fields: written, but excluded from the compare that decides
32830
- * whether persisting is worth a SQLite commit. */
32831
- volatileStateFields: ["lastFetchedAt"]
32832
- };
32833
- /**
32834
32667
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
32835
32668
  * for every device, regardless of provider — the kernel needs a uniform
32836
32669
  * cap-keyed slice for the basic device flags every consumer expects to
@@ -33049,40 +32882,6 @@ var eventEmitterCapability = {
33049
32882
  */
33050
32883
  durability: "session"
33051
32884
  };
33052
- var EventItemSchema = object({
33053
- id: string(),
33054
- type: string(),
33055
- timestamp: number(),
33056
- label: string().optional(),
33057
- thumbnailUrl: string().optional(),
33058
- clipUrl: string().optional(),
33059
- metadata: record(string(), unknown()).optional()
33060
- });
33061
- var eventsCapability = {
33062
- name: "events",
33063
- scope: "device",
33064
- mode: "singleton",
33065
- deviceTypes: [DeviceType.Camera],
33066
- methods: {
33067
- getEvents: method(object({
33068
- deviceId: number(),
33069
- from: number().optional(),
33070
- to: number().optional(),
33071
- limit: number().optional()
33072
- }), array(EventItemSchema)),
33073
- getEventThumbnail: method(object({
33074
- deviceId: number(),
33075
- eventId: string()
33076
- }), object({
33077
- base64: string(),
33078
- contentType: string()
33079
- }).nullable()),
33080
- getEventClipUrl: method(object({
33081
- deviceId: number(),
33082
- eventId: string()
33083
- }), string().nullable())
33084
- }
33085
- };
33086
32885
  var IdentitySchema = object({
33087
32886
  id: string(),
33088
32887
  name: string(),
@@ -35570,179 +35369,6 @@ var motionTriggerCapability = {
35570
35369
  durability: "session"
35571
35370
  };
35572
35371
  /**
35573
- * camera-grid-layout — the geometry of a COMPOSITE camera, on the camera's own
35574
- * page.
35575
- *
35576
- * ## Why this is a capability and not an addon settings schema
35577
- *
35578
- * It was one, and it did not render. The addon declared the editor as a
35579
- * `type: 'widget'` field inside its own `deviceSettingsSchema()`; the hub
35580
- * returned that section correctly and `ConfigFormField` renders `type:'widget'`
35581
- * perfectly well — and nothing ever asked camera-grid for it. The Cluster →
35582
- * Pipeline → Device Overrides page interrogates a HAND-WRITTEN list of four
35583
- * addons (`PIPELINE_CLUSTER_DEVICE_ADDONS`), whose own comment says an addon
35584
- * not on it "falls off silently".
35585
- *
35586
- * Adding a fifth name to that list would have been the wrong fix twice over:
35587
- * that page is per-camera DETECTION tuning, and a grid's geometry belongs
35588
- * beside PTZ and motion zones on the camera itself. The device page is
35589
- * BINDING-driven (D12), so the way in is a capability bound to the device —
35590
- * and this cap carries its section the way `recording` does, by RETURNING it
35591
- * from `getDeviceSettingsContribution`.
35592
- *
35593
- * Seven other widgets are still declared the other way, through a
35594
- * `deviceConfig.ui` block the framework derives a section from. That route
35595
- * gives the addon no say in where its own panel lands and no way to decline
35596
- * for a device the panel does not suit, which is why this one does not use it.
35597
- *
35598
- * ## Why one addon may implement it
35599
- *
35600
- * It is a device-scoped NATIVE cap, registered by the grid camera device
35601
- * itself. Nothing else declares a composite camera, so nothing else has a
35602
- * layout — and the device-scoped route means the widget asks THE camera, not
35603
- * "the camera-grid addon", which is what let the old custom-action pair be
35604
- * reached only by a caller that already knew the addon id.
35605
- *
35606
- * ## The tab
35607
- *
35608
- * `streaming`, not a top-tab of its own. A grid's geometry IS what its stream
35609
- * is, so the Streaming tab is where it belongs; a `grid` top-tab would need an
35610
- * entry in `WELL_KNOWN_TAB_MAP` or the device page renders the raw id as the
35611
- * label (measured on the robot camera, 2026-09-06 — a tab called "navigation"
35612
- * next to "PTZ").
35613
- */
35614
- /** A rectangle in normalized [0,1] coordinates of whatever contains it. */
35615
- var GridNormalizedRectSchema = object({
35616
- x: number().min(0).max(1),
35617
- y: number().min(0).max(1),
35618
- width: number().gt(0).max(1),
35619
- height: number().gt(0).max(1)
35620
- });
35621
- /**
35622
- * One source camera, the part of its picture taken, and where that part lands.
35623
- *
35624
- * Both rectangles are NORMALIZED (D519): a source camera can change resolution
35625
- * — a profile switch, a firmware update, a substream that comes back different
35626
- * — and a stored PIXEL rectangle would quietly start cutting the wrong region,
35627
- * which is the class of bug nobody files.
35628
- */
35629
- var GridLayoutCellSchema = object({
35630
- deviceId: number().int().positive(),
35631
- /** The part of the SOURCE taken, normalized against the source. */
35632
- source: GridNormalizedRectSchema,
35633
- /** Where it lands, normalized against the CANVAS. */
35634
- cell: GridNormalizedRectSchema
35635
- });
35636
- /**
35637
- * Which profiles this grid can actually compose, and why not.
35638
- *
35639
- * A grid's `high` composes its sources' `high` and its `low` their `low`, so a
35640
- * profile is on offer only when EVERY source can serve it. The refusal NAMES
35641
- * the sources, because "this grid has no low" is not a finding — "615 has no
35642
- * low" is, and it is the one an operator can act on.
35643
- */
35644
- var GridProfileOfferSchema = object({
35645
- profile: _enum([
35646
- "high",
35647
- "mid",
35648
- "low"
35649
- ]),
35650
- offered: boolean(),
35651
- /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
35652
- missingSources: array(number().int().positive()),
35653
- /**
35654
- * The canvas this profile composes onto, `WxH`, or empty when it is not
35655
- * offered. DERIVED from the cells and the sources' own size at this profile —
35656
- * it is reported because nothing else in the system would ever say what the
35657
- * grid came out as, and because it is the number an operator would otherwise
35658
- * expect to type.
35659
- */
35660
- canvas: string(),
35661
- /**
35662
- * Whether this profile is PUBLISHED, of the ones the grid could serve.
35663
- *
35664
- * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
35665
- * a 4K canvas built from 4K decodes — something to opt into, not something a
35666
- * viewer's adaptive should be handed by climbing to the top rung it can see.
35667
- * Default is `mid` + `low`.
35668
- */
35669
- published: boolean()
35670
- });
35671
- var GridLayoutViewSchema = object({
35672
- /** The persisted grid row this camera was declared from. */
35673
- instanceId: string(),
35674
- deviceId: number().int().nonnegative(),
35675
- name: string(),
35676
- /**
35677
- * NO canvas size. A grid's resolution is not authored: each profile derives
35678
- * its own from the cells and its sources' dimensions. The two numbers that
35679
- * used to be here were a text field that silently decided both how much the
35680
- * composite cost and how sharp it was — see `profiles[].canvas` for what it
35681
- * came out as.
35682
- */
35683
- fps: number().int(),
35684
- cells: array(GridLayoutCellSchema),
35685
- /** What the catalog will publish, and what it refuses to. Read-only. */
35686
- profiles: array(GridProfileOfferSchema)
35687
- });
35688
- var GridLayoutPatchSchema = object({
35689
- deviceId: number().int().nonnegative(),
35690
- name: string().min(1).max(160).optional(),
35691
- fps: number().int().min(1).max(60).optional(),
35692
- /** Which profiles to publish. See `GridProfileOffer.published`. */
35693
- publishedProfiles: array(_enum([
35694
- "high",
35695
- "mid",
35696
- "low"
35697
- ])).max(3).optional(),
35698
- /**
35699
- * The whole cell list at once. A per-cell patch would need an ordering the
35700
- * editor does not have, and a half-applied layout is a picture nobody asked
35701
- * for.
35702
- */
35703
- cells: array(GridLayoutCellSchema).max(16)
35704
- });
35705
- var cameraGridLayoutCapability = {
35706
- name: "camera-grid-layout",
35707
- scope: "device",
35708
- deviceNative: true,
35709
- mode: "singleton",
35710
- deviceTypes: [DeviceType.Camera],
35711
- /**
35712
- * The section is built by the ADDON and returned from
35713
- * `getDeviceSettingsContribution`, not derived by the framework from a
35714
- * `deviceConfig.ui` block.
35715
- *
35716
- * Both mechanisms render the same widget. This one hands the addon two
35717
- * things the framework-derived route cannot give it:
35718
- *
35719
- * - it chooses its own section, `tab`, `location` and `order`, the way any
35720
- * other setting does, instead of receiving them from a cap declaration;
35721
- * - it can DECLINE per device. A camera that is not a grid gets no section
35722
- * at all, rather than a widget that renders its own "not a grid" state.
35723
- *
35724
- * `recording` is the precedent (`recorder/recording-device-settings.ts`): it
35725
- * returns `null` for anything that is not a camera, so the Recording tab
35726
- * never appears there.
35727
- */
35728
- exposesDeviceSettings: true,
35729
- methods: {
35730
- /**
35731
- * The grid behind this device.
35732
- *
35733
- * `null` means ANSWERED and this camera is not a grid — not "not yet
35734
- * known". The widget renders its "this is not a grid camera" state only
35735
- * from this answer, never from an unresolved query (D315).
35736
- */
35737
- getLayout: method(object({ deviceId: number().int().nonnegative() }), GridLayoutViewSchema.nullable(), { auth: "admin" }),
35738
- /** Write the geometry back. Returns the grid as it now stands, profiles included. */
35739
- saveLayout: method(GridLayoutPatchSchema, GridLayoutViewSchema, {
35740
- kind: "mutation",
35741
- auth: "admin"
35742
- })
35743
- }
35744
- };
35745
- /**
35746
35372
  * Motion-zones share the same MaskShape vocabulary as privacy-mask — the
35747
35373
  * on-camera motion-detection mask is a single `grid` region (a row-major
35748
35374
  * boolean cell lattice the camera's onboard VMD evaluates). Composing it as
@@ -38412,37 +38038,37 @@ var rebootCapability = {
38412
38038
  auth: "admin"
38413
38039
  }) }
38414
38040
  };
38415
- /**
38416
- * `recording` cap — footage availability + HLS playback manifests + per-device
38417
- * recording config. NOTE on events (source of truth, R5/C3): this cap carries
38418
- * NO event surface — `getPlaybackManifest` returns playlist URLs only. Timeline
38419
- * events (motion/object/audio) come from `pipelineAnalytics` (durable SQLite
38420
- * rows) and are the ONLY event surface — the recorder has none. The in-RAM
38421
- * playback markers it used to build were deleted on 2026-08-29 because nothing
38422
- * ever read them. Event<->footage joins are by time, padded with the shared
38423
- * `EVENT_PAD_MS` (`interfaces/recording-config.ts`).
38424
- */
38425
- var RecordingStatusSchema = object({
38426
- deviceId: number(),
38427
- enabled: boolean(),
38428
- /** THE derived storage mode, from the one definition
38429
- * (`deriveRecordingMode`) — never a second enum. A duplicated list is how
38430
- * `on-device-decision` could have reached the recorder and not the status. */
38431
- activeMode: RecordingStorageModeSchema,
38432
- nodeId: string(),
38433
- storageBytes: number()
38434
- });
38435
38041
  var RecordingRangeSchema = object({
38436
38042
  profile: string(),
38437
38043
  startMs: number(),
38438
38044
  endMs: number()
38439
38045
  });
38046
+ /**
38047
+ * How a source ANSWERED, on every singular read of this cap.
38048
+ *
38049
+ * `'read'` — it looked. `ranges: []` / `days: []` is then a real claim: this
38050
+ * source has no coverage in the window. `'unreadable'` — nobody could look
38051
+ * (the camera was unreachable, the calendar rung threw, the location is
38052
+ * unmounted, the node is still on the old build), and the emptiness beside it
38053
+ * means NOTHING.
38054
+ *
38055
+ * The batch rows have carried this since the grid existed; the SINGULAR
38056
+ * answers gained it with the collection (D625 §10.4), because they are the
38057
+ * ones the single-camera picker uses and because a half-converted fleet makes
38058
+ * "nobody looked" common for the length of a deploy. Without it the timeline
38059
+ * has no vocabulary for it, and `(data ?? [])` in a viewer turns a rollout into
38060
+ * a fleet of cameras that appear to have lost their recordings (D315, D393).
38061
+ */
38062
+ var RecordingReadSchema = _enum(["read", "unreadable"]);
38440
38063
  var RecordingAvailabilitySchema = object({
38441
38064
  deviceId: number(),
38065
+ /** See {@link RecordingReadSchema}. An `'unreadable'` answer carries an empty
38066
+ * `ranges` that means nothing — never draw it as "no footage". */
38067
+ read: RecordingReadSchema,
38442
38068
  ranges: array(RecordingRangeSchema),
38443
38069
  /**
38444
- * Every profile this camera has footage in — not only the one `ranges`
38445
- * describes (D433).
38070
+ * Every profile this camera has footage in AT THIS SOURCE — not only the one
38071
+ * `ranges` describes (D433).
38446
38072
  *
38447
38073
  * `ranges` answers for ONE profile by design: the timeline is a single bar,
38448
38074
  * and enumerating all of them triples the directory reads for a bar that
@@ -38459,15 +38085,344 @@ var RecordingAvailabilitySchema = object({
38459
38085
  });
38460
38086
  var RecordingDaysSchema = object({
38461
38087
  deviceId: number(),
38088
+ /** See {@link RecordingReadSchema}. `days: []` on an `'unreadable'` answer is
38089
+ * "nobody could look", and the date-picker must not spell it the same as
38090
+ * "no footage this month". */
38091
+ read: RecordingReadSchema,
38462
38092
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
38463
38093
  days: array(number())
38464
38094
  });
38095
+ var RecordingManifestSchema = object({
38096
+ deviceId: number(),
38097
+ /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
38098
+ localMasterPath: string().nullable(),
38099
+ /** HTTP(S) URL to the master playlist on the recording node's playback server
38100
+ * (the PRIMARY candidate); null when no recording / server. Carries the
38101
+ * scoped playback token in its path. */
38102
+ playbackUrl: string().nullable(),
38103
+ /**
38104
+ * Candidate master-playlist URLs the client tries in order (LAN first, then
38105
+ * remote — Tailscale/Cloudflare if the operator configured extra hosts), each
38106
+ * carrying the same scoped token. `playbackUrl` is the first entry. Empty when
38107
+ * there is no recording / server.
38108
+ */
38109
+ playbackEndpoints: array(string())
38110
+ });
38111
+ var RecordingSourceAvailabilitySchema = object({
38112
+ state: _enum([
38113
+ "ok",
38114
+ "sleeping",
38115
+ "unreachable",
38116
+ "no-storage",
38117
+ "index-empty"
38118
+ ]),
38119
+ /** Free text, shown verbatim. Names the camera's own refusal when there is one. */
38120
+ reason: string().optional(),
38121
+ /** When this source's coverage was last CONFIRMED. A cached answer is never
38122
+ * drawn as current: the surface shows the age whenever it is older than the
38123
+ * refresh interval. The clip catalog's `catalogAsOf`, under the name the
38124
+ * timeline uses for it. */
38125
+ coverageAsOf: number().optional()
38126
+ });
38127
+ /**
38128
+ * One SOURCE of recorded coverage for a camera — a row of the picker.
38129
+ *
38130
+ * A provider lists the sources IT serves for that device, and answers for each
38131
+ * of them whether it can answer at all. A provider with nothing to offer on a
38132
+ * camera returns `[]` — it is not that camera's business. The five availability
38133
+ * states are `ClipSourceAvailability`'s verbatim: they mean exactly the same
38134
+ * things about a coverage index as about a clip catalog, and `sleeping` in
38135
+ * particular is what stops a battery camera being woken to paint a bar.
38136
+ */
38137
+ var RecordingSourceSchema = object({
38138
+ /** The source id. {@link RECORDING_SOURCE_CAMSTACK} for ours (RESERVED), a
38139
+ * vendor namespace (`native:reolink:onboard`, …) for a camera's own store. */
38140
+ source: string(),
38141
+ /** Operator-facing name of the source ("CamStack recordings", "SD card"). */
38142
+ label: string(),
38143
+ /**
38144
+ * The addon that SERVES this row, and the value a later call passes as
38145
+ * `provider`.
38146
+ *
38147
+ * Optional for version skew only. The collection dispatcher stamps it from
38148
+ * the registry, so a row that travelled through the fan-out carries the
38149
+ * authoritative id whatever the provider filled in (D557 §4).
38150
+ */
38151
+ addonId: string().optional(),
38152
+ availability: RecordingSourceAvailabilitySchema
38153
+ });
38154
+ /**
38155
+ * What a surface may DRAW for this (camera, source) — D612's rule applied to a
38156
+ * timeline: **the source declares what it can do, and the surface draws what
38157
+ * was declared. It never assumes, and never offers a gesture it will then
38158
+ * refuse.** D612 exists because `8` and `16` were offered as clip rates, the
38159
+ * broker clamped them to `4`, and no line anywhere said so.
38160
+ *
38161
+ * Asked once per (camera, source) before anything is drawn — never replaced by
38162
+ * a constant the surface keeps, which is the second authority D612 ends.
38163
+ */
38164
+ var RecordingSourceOptionsSchema = object({
38165
+ /** How this source's media reaches the player.
38166
+ * `archive` = our own indexed segment tree; `stream` = the provider's
38167
+ * forward-only fMP4 (D597); `realtime` = a replay bound to wall clock. */
38168
+ transport: _enum([
38169
+ "archive",
38170
+ "stream",
38171
+ "realtime"
38172
+ ]),
38173
+ /** What the BAR means. `continuous` = gaps are holes in a recording;
38174
+ * `sparse` = gaps are the absence of one, and must be drawn as such.
38175
+ *
38176
+ * Not an onboard-only concession: measured 2026-09-24, OUR bar covers 98.8 %
38177
+ * of 592's day and 1.2 % of 1436's. It is a fact about a (source, camera)
38178
+ * pair, and ours answers it per camera from `deriveRecordingMode`. */
38179
+ coverage: _enum(["continuous", "sparse"]),
38180
+ /** Where the playhead may be put.
38181
+ * `free` — anywhere, to the frame.
38182
+ * `forward` — only ahead of the current position.
38183
+ * `segment` — a position SNAPS to the head of the covering segment; a finer
38184
+ * ask is accepted by the camera and SILENTLY IGNORED. Measured
38185
+ * on 1436 (Hikvision V5.7.1, 2026-09-23): a window-narrowed
38186
+ * `ContentMgmt/search` returns a row and a `playbackURI`, the
38187
+ * replay opens 200 and delivers media — and the burned-in OSD of
38188
+ * the first frame reads the SEGMENT HEAD every time. Calling
38189
+ * that `forward` would tell the surface it may move the playhead
38190
+ * ahead within a loaded segment, which it may not. */
38191
+ seek: _enum([
38192
+ "free",
38193
+ "forward",
38194
+ "segment"
38195
+ ]),
38196
+ /** Frame-step BACKWARD is meaningful. */
38197
+ stepBack: boolean(),
38198
+ /** Whether the drag-scrub gesture is served, as opposed to refused by name. */
38199
+ scrub: boolean(),
38200
+ /** Deliverable rates, ascending, always containing `1`. The surface draws its
38201
+ * picker from this and from NOTHING else (D612, D620, D621). `0` is not a
38202
+ * member: pause is the absence of a rate. */
38203
+ rates: array(number().positive()).min(1).readonly(),
38204
+ /** TRUE when a read of this source HOLDS the camera's only playback session.
38205
+ * A surface with this set makes at most ONE read at a time and draws no
38206
+ * scrub-thumbnail strip, no hover preview, no prefetch and no background
38207
+ * refresh. The precedent is exact and expensive: filling one screen of
38208
+ * Hikvision thumbnails at 1.01× realtime consumed fifteen minutes of that
38209
+ * camera's only playback session (1.2.126, reported within minutes), and a
38210
+ * timeline is a screenful of reads by construction. */
38211
+ exclusive: boolean()
38212
+ });
38213
+ /**
38214
+ * How to PLAY the instant that was asked for, from the chosen source.
38215
+ *
38216
+ * No new media transport is built for onboard sources: the `clip` arm is a
38217
+ * DELEGATION to the `videoclips` transport that vendor already has (D597 /
38218
+ * D616 / D617). The onboard half of this collection is a PROJECTION of
38219
+ * `videoclips` for coverage and a delegation to it for bytes.
38220
+ */
38221
+ var RecordingPlaybackSchema = discriminatedUnion("kind", [
38222
+ object({
38223
+ kind: literal("hls"),
38224
+ manifest: RecordingManifestSchema
38225
+ }),
38226
+ object({
38227
+ kind: literal("clip"),
38228
+ /** The `videoclips` source namespace this clip id belongs to. */
38229
+ source: string(),
38230
+ clipId: string(),
38231
+ /** Where this clip actually STARTS. On a `seek: 'segment'` source the
38232
+ * playhead lands here, not at the requested instant — the surface must be
38233
+ * TOLD, not left to discover it from a burned-in OSD. */
38234
+ startsAtMs: number()
38235
+ }),
38236
+ object({
38237
+ kind: literal("none"),
38238
+ reason: string()
38239
+ })
38240
+ ]);
38241
+ var recordingCapability = {
38242
+ name: "recording",
38243
+ scope: "device",
38244
+ /** Several sources per camera, listed beside each other. The mount stays
38245
+ * `device-scoped` — see `resolveCapMount`'s ordering and D554: per-device
38246
+ * wins over the global collection fan-out. */
38247
+ mode: "collection",
38248
+ kind: "wrapper",
38249
+ defaultActive: true,
38250
+ /** Recorded coverage is a property of a camera — the cap is meaningless on a
38251
+ * sensor, a button or an event emitter, and the `defaultActive` auto-bind
38252
+ * reads this to decide which devices it may claim. */
38253
+ deviceTypes: [DeviceType.Camera],
38254
+ methods: {
38255
+ /**
38256
+ * The sources this camera has, WITH the reason any of them cannot answer.
38257
+ *
38258
+ * Asked separately from `getAvailability` because an empty bar is
38259
+ * ambiguous and this is the only place the ambiguity is resolved: every
38260
+ * bound provider contributes its own rows, and a provider that could not be
38261
+ * reached at all still produces one row saying so. A surface that draws "no
38262
+ * recordings" without reading this is drawing a guess.
38263
+ *
38264
+ * The ONLY method here without a `provider` — it is the call that tells the
38265
+ * caller what to put there.
38266
+ */
38267
+ listSources: method(object({ deviceId: number() }), array(RecordingSourceSchema).readonly(), {
38268
+ kind: "query",
38269
+ auth: "protected"
38270
+ }),
38271
+ /**
38272
+ * Recorded coverage of `[fromMs, toMs)` at ONE source — the timeline bar.
38273
+ *
38274
+ * `protected`, not `admin`: a per-camera read is exactly what a camera
38275
+ * viewer is FOR, and the device-scoped mount routes through
38276
+ * `getProviderForDevice`, so a camera outside the caller's scope is refused
38277
+ * before a provider is reached.
38278
+ */
38279
+ getAvailability: method(object({
38280
+ deviceId: number(),
38281
+ /**
38282
+ * WHICH provider to ask — the `addonId` a {@link RecordingSourceSchema}
38283
+ * row carries, never a source id and never a list. **REQUIRED**, in the
38284
+ * schema, where the generated types make it unomittable rather than
38285
+ * merely discouraged (D554 amended).
38286
+ *
38287
+ * It was learned the expensive way on `videoclips.listClips`: measured
38288
+ * on the live hub 2026-09-20, device 592 bound to `recorder` AND
38289
+ * `provider-reolink`, a bare call with `limit: 3` answered SIX rows,
38290
+ * three from each source, merged — `device-collection-dispatch.ts`
38291
+ * leaves an unpinned fan-out un-narrowed, so absence buys the union the
38292
+ * method exists not to be. An un-narrowed `getAvailability` would do
38293
+ * that to a TIMELINE: our ranges and the card's clips unioned into one
38294
+ * bar, which is "two sources are never drawn together" broken in the
38295
+ * one place it matters most.
38296
+ *
38297
+ * A provider the device is not bound to is refused BY NAME (D552's
38298
+ * `rejectUnresolvedAddonPin`), never answered by another one.
38299
+ */
38300
+ provider: string().min(1),
38301
+ fromMs: number(),
38302
+ toMs: number(),
38303
+ /**
38304
+ * Answer for THIS profile instead of the source's preferred one (D433).
38305
+ * Absent keeps the timeline's behaviour — one bar, one profile, one set
38306
+ * of reads. `profilesWithFootage` on the answer says what may be asked
38307
+ * for.
38308
+ */
38309
+ profile: string().optional()
38310
+ }), RecordingAvailabilitySchema, {
38311
+ kind: "query",
38312
+ auth: "protected"
38313
+ }),
38314
+ /** Which calendar days in [fromMs,toMs) this source has ≥1 recording in,
38315
+ * bucketed by the client's local day (`tzOffsetMinutes` = minutes to add
38316
+ * to UTC). Drives the theater date-picker's day dots. `provider` is
38317
+ * REQUIRED for the reason `getAvailability` states. */
38318
+ getDaysWithRecordings: method(object({
38319
+ deviceId: number(),
38320
+ provider: string().min(1),
38321
+ fromMs: number(),
38322
+ toMs: number(),
38323
+ tzOffsetMinutes: number()
38324
+ }), RecordingDaysSchema, {
38325
+ kind: "query",
38326
+ auth: "protected"
38327
+ }),
38328
+ /**
38329
+ * How to PLAY `[fromMs, toMs)` at this source.
38330
+ *
38331
+ * It was `getPlaybackManifest`, and the rename is not cosmetic: a
38332
+ * "manifest" is an HLS master playlist, which is a property of OUR recorder
38333
+ * and of nothing else. Keeping the name would make every onboard
38334
+ * implementation a lie in its signature. The old shape survives verbatim
38335
+ * inside the union's `hls` arm, so the recorder's implementation is
38336
+ * unchanged behind it.
38337
+ */
38338
+ getPlayback: method(object({
38339
+ deviceId: number(),
38340
+ provider: string().min(1),
38341
+ fromMs: number(),
38342
+ toMs: number(),
38343
+ profile: CamProfileSchema.optional()
38344
+ }), RecordingPlaybackSchema, {
38345
+ kind: "query",
38346
+ auth: "protected"
38347
+ }),
38348
+ /**
38349
+ * What this (camera, source) can actually DO — asked before anything is
38350
+ * drawn. See {@link RecordingSourceOptionsSchema}; a constant the surface
38351
+ * keeps instead is the second authority D612 exists to end.
38352
+ */
38353
+ getPlaybackOptions: method(object({
38354
+ deviceId: number(),
38355
+ provider: string().min(1)
38356
+ }), RecordingSourceOptionsSchema, {
38357
+ kind: "query",
38358
+ auth: "protected"
38359
+ })
38360
+ }
38361
+ };
38362
+ /**
38363
+ * `recording-archive` — OUR archive, and the intent that fills it.
38364
+ *
38365
+ * The system-singleton half of the 2026-09-24 cut (D625). `recording` used to
38366
+ * be one 33-method system singleton holding two unrelated subjects: three
38367
+ * per-camera READS about coverage and playback, and everything else — storage
38368
+ * locations, retention, relocation, rebalance, the ops log, the placement
38369
+ * table and the byte-plane primitives our scrub and export are built on.
38370
+ *
38371
+ * The reads became a device-scoped COLLECTION, so a camera's own card can be a
38372
+ * source beside ours (`recording.cap.ts`). Everything that is about OUR store,
38373
+ * or unimplementable by a camera, stayed here.
38374
+ *
38375
+ * ## On the name
38376
+ *
38377
+ * `recording-storage` was the obvious choice and is wrong: this cap also holds
38378
+ * `getDeviceConfig`/`setDeviceConfig`, which are recording INTENT — bands,
38379
+ * retention, the D62 switch authority — and a name that says "storage" invites
38380
+ * the next reader to move them out again. An archive is a thing we keep, and
38381
+ * what we keep it under is a policy; the name covers both halves honestly and
38382
+ * sits in the existing family (`recording-onboard`, `recording-export`,
38383
+ * `recording-signal`).
38384
+ *
38385
+ * ## What must NOT happen to it
38386
+ *
38387
+ * It stays a SINGLETON. It is registered by `recorder`, which is
38388
+ * `placement: 'any-node'` and runs on every recording node; the hub dispatches
38389
+ * to one of them. Putting the ledger, the placement table or the relocation
38390
+ * jobs behind a fan-out is the one genuinely dangerous move in this cut.
38391
+ *
38392
+ * `getDeviceConfig` / `setDeviceConfig` in particular are the D62 recording
38393
+ * authority (`CameraSwitch.authority`). If a write reached a different provider
38394
+ * than the read — which a collection fan-out permits — two authorities would
38395
+ * decide when one camera records, and the symptom (recording silently off, or
38396
+ * a `bands` array clobbered by a partial write) is durable and silent. Keeping
38397
+ * them here means the worst case during a rollout is a 412: the switch refuses
38398
+ * to flip and SAYS so. **Do not move them into the collection, at any point,
38399
+ * for any reason.**
38400
+ *
38401
+ * ## The two batch reads
38402
+ *
38403
+ * `getAvailabilityBatch` / `getDaysWithRecordingsBatch` take `deviceIds:
38404
+ * number[]` with no single `deviceId`, and a device-scoped mount routes
38405
+ * through `getProviderForDevice(deviceId)` — there is nothing for it to route
38406
+ * on. They stay here, and on this cap the batch is explicitly OURS: a grid has
38407
+ * no per-camera picker, and a caller that wants another source's coverage asks
38408
+ * `recording.getAvailability` per device with that source's `provider`.
38409
+ */
38410
+ var RecordingStatusSchema = object({
38411
+ deviceId: number(),
38412
+ enabled: boolean(),
38413
+ /** THE derived storage mode, from the one definition
38414
+ * (`deriveRecordingMode`) — never a second enum. A duplicated list is how
38415
+ * `on-device-decision` could have reached the recorder and not the status. */
38416
+ activeMode: RecordingStorageModeSchema,
38417
+ nodeId: string(),
38418
+ storageBytes: number()
38419
+ });
38465
38420
  /**
38466
38421
  * One camera's row in a `getAvailabilityBatch` answer.
38467
38422
  *
38468
- * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
38469
- * batch collapses the transport, not the work — plus the one thing the singular
38470
- * method never had to say:
38423
+ * `ranges` is EXACTLY what `recording.getAvailability` returns for that camera
38424
+ * at OUR source — the batch collapses the transport, not the work — plus the
38425
+ * `read` mark the singular answer now carries too (D625):
38471
38426
  *
38472
38427
  * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
38473
38428
  * no footage in the window", which is a real claim.
@@ -38499,22 +38454,6 @@ var RecordingDaysForDeviceSchema = object({
38499
38454
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
38500
38455
  days: array(number()).readonly()
38501
38456
  });
38502
- var RecordingManifestSchema = object({
38503
- deviceId: number(),
38504
- /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
38505
- localMasterPath: string().nullable(),
38506
- /** HTTP(S) URL to the master playlist on the recording node's playback server
38507
- * (the PRIMARY candidate); null when no recording / server. Carries the
38508
- * scoped playback token in its path. */
38509
- playbackUrl: string().nullable(),
38510
- /**
38511
- * Candidate master-playlist URLs the client tries in order (LAN first, then
38512
- * remote — Tailscale/Cloudflare if the operator configured extra hosts), each
38513
- * carrying the same scoped token. `playbackUrl` is the first entry. Empty when
38514
- * there is no recording / server.
38515
- */
38516
- playbackEndpoints: array(string())
38517
- });
38518
38457
  /**
38519
38458
  * Recording storage usage for one camera — what the ARCHIVE holds for it,
38520
38459
  * across every profile and every resolvable location on this node.
@@ -38772,33 +38711,22 @@ var ReadWindowBytesResultSchema = discriminatedUnion("kind", [object({
38772
38711
  * not a retry hint (retrying the same window would refuse again). */
38773
38712
  segmentEndMs: number()
38774
38713
  })]);
38775
- var recordingCapability = {
38776
- name: "recording",
38714
+ var recordingArchiveCapability = {
38715
+ name: "recording-archive",
38777
38716
  scope: "system",
38778
38717
  mode: "singleton",
38718
+ /** Moves here with the config pair — the derived Recording settings
38719
+ * section is a view over `getDeviceConfig`/`setDeviceConfig` (D14), and
38720
+ * those did not move (D625). */
38779
38721
  exposesDeviceSettings: true,
38780
38722
  status: {
38781
38723
  schema: RecordingStatusSchema,
38782
38724
  kind: "command-driven"
38783
38725
  },
38784
38726
  methods: {
38785
- getAvailability: method(object({
38786
- deviceId: number(),
38787
- fromMs: number(),
38788
- toMs: number(),
38789
- /**
38790
- * Answer for THIS profile instead of the preferred one (D433). Absent
38791
- * keeps the timeline's behaviour — one bar, one profile, one set of
38792
- * reads. `profilesWithFootage` on the answer says what may be asked
38793
- * for.
38794
- */
38795
- profile: string().optional()
38796
- }), RecordingAvailabilitySchema, {
38797
- kind: "query",
38798
- auth: "protected"
38799
- }),
38800
38727
  /**
38801
- * `getAvailability` for a SET of cameras, in one round trip.
38728
+ * `recording.getAvailability` for a SET of cameras, in one round trip, at
38729
+ * OUR source.
38802
38730
  *
38803
38731
  * A multi-camera timeline re-asks availability for every camera in the grid
38804
38732
  * on every day change; fanned out that is one request per camera for N
@@ -38806,6 +38734,13 @@ var recordingCapability = {
38806
38734
  * `availabilityProfileFor` + `rangesIn`, run concurrently inside the
38807
38735
  * recorder) — only the transport collapses.
38808
38736
  *
38737
+ * It lives on the ARCHIVE, not on the `recording` collection, because it
38738
+ * takes `deviceIds` with no single `deviceId` and a device-scoped mount has
38739
+ * nothing to route on (D625 §10.2). The consequence is stated rather than
38740
+ * hidden: the batch answers for OURS. A caller that needs another source's
38741
+ * coverage asks `recording.getAvailability` per device, naming that
38742
+ * source's provider.
38743
+ *
38809
38744
  * `protected` for the same reason the singular method is: every id in
38810
38745
  * `deviceIds` is a device reference, so the F1 #3 gate refuses any camera
38811
38746
  * outside the caller's scope — one id out of scope refuses the CALL, it
@@ -38823,20 +38758,10 @@ var recordingCapability = {
38823
38758
  kind: "query",
38824
38759
  auth: "protected"
38825
38760
  }),
38826
- /** Which calendar days in [fromMs,toMs) have ≥1 recorded segment, bucketed by
38827
- * the client's local day (`tzOffsetMinutes` = minutes to add to UTC). Drives
38828
- * the theater date-picker's day dots. */
38829
- getDaysWithRecordings: method(object({
38830
- deviceId: number(),
38831
- fromMs: number(),
38832
- toMs: number(),
38833
- tzOffsetMinutes: number()
38834
- }), RecordingDaysSchema, {
38835
- kind: "query",
38836
- auth: "protected"
38837
- }),
38838
38761
  /**
38839
- * `getDaysWithRecordings` for a SET of cameras, in one round trip.
38762
+ * `recording.getDaysWithRecordings` for a SET of cameras, in one round
38763
+ * trip, at OUR source. Same placement argument as
38764
+ * {@link getAvailabilityBatch}.
38840
38765
  *
38841
38766
  * The cheapest question in the product, asked once per camera per month
38842
38767
  * change. One directory read per day per camera at the owner, unchanged;
@@ -38855,14 +38780,6 @@ var recordingCapability = {
38855
38780
  kind: "query",
38856
38781
  auth: "protected"
38857
38782
  }),
38858
- getPlaybackManifest: method(object({
38859
- deviceId: number(),
38860
- fromMs: number(),
38861
- toMs: number()
38862
- }), RecordingManifestSchema, {
38863
- kind: "query",
38864
- auth: "protected"
38865
- }),
38866
38783
  getStorageUsage: method(object({}), RecordingStorageUsageSchema, {
38867
38784
  kind: "query",
38868
38785
  auth: "admin"
@@ -38878,6 +38795,10 @@ var recordingCapability = {
38878
38795
  * value (D315, D393, D590). It carries schedules and retention, no secret,
38879
38796
  * and the per-device gate in `scope-access.ts` still applies; `setDeviceConfig`
38880
38797
  * stays `admin`.
38798
+ *
38799
+ * It is a SINGLETON method and stays one (D625 §16.2): it is the D62
38800
+ * recording authority, and an authority that several providers could answer
38801
+ * is the "two knobs over one decision" D62 forbids.
38881
38802
  */
38882
38803
  getDeviceConfig: method(object({ deviceId: number() }), RecordingConfigSchema, {
38883
38804
  kind: "query",
@@ -38885,7 +38806,13 @@ var recordingCapability = {
38885
38806
  }),
38886
38807
  /** Locate footage at a wall-clock instant: the covering segment's window,
38887
38808
  * or a gap with the forward nearest covered edge. Used by a feeder running
38888
- * in another addon process to seek recorded footage over tRPC. */
38809
+ * in another addon process to seek recorded footage over tRPC.
38810
+ *
38811
+ * BYTE PLANE: defined by `mfra` byte ranges over OUR own MP4 segment tree.
38812
+ * There is no vendor-neutral statement of it, its only callers are feeders
38813
+ * in other addon processes (the stream broker, the replay-clip source),
38814
+ * and an onboard source that ever needs bytes delegates to the
38815
+ * `videoclips` transport its vendor already has (D625 §9.3, §10.5). */
38889
38816
  locateSegment: method(object({
38890
38817
  deviceId: number(),
38891
38818
  profile: string(),
@@ -39500,6 +39427,346 @@ var recordingExportCapability = {
39500
39427
  }
39501
39428
  };
39502
39429
  /**
39430
+ * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
39431
+ * writes to the CAMERA's own card, on the camera's own schedule.
39432
+ *
39433
+ * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
39434
+ * footage ledger, our storage locations, our retention. This one has a
39435
+ * different authority — the camera's firmware — and per D62 it stores
39436
+ * nothing of its own. Every value here is read from the camera and every
39437
+ * write goes back to the camera; there is no CamStack-side mirror that
39438
+ * could disagree with the device.
39439
+ *
39440
+ * ## One shape, two firmwares
39441
+ *
39442
+ * Measured 2026-09-22 against the live fleet:
39443
+ *
39444
+ * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
39445
+ * | --- | --- | --- |
39446
+ * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
39447
+ * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
39448
+ * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
39449
+ * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
39450
+ * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
39451
+ * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
39452
+ * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
39453
+ * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
39454
+ *
39455
+ * The two schedule models look different and are the same thing in
39456
+ * different coordinates: both answer "for this trigger, during which
39457
+ * weekly windows does the camera record". {@link RecordWindow} is that
39458
+ * question in one shape — Hikvision's ranges map straight onto it,
39459
+ * Reolink's mask expands into hour-aligned windows.
39460
+ *
39461
+ * ## Union, not intersection
39462
+ *
39463
+ * **The same fields exist on every camera.** What differs per device is
39464
+ * which VALUES that device accepts, and that is what {@link
39465
+ * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
39466
+ * per field plus the schedule's own limits. A control a camera cannot
39467
+ * honour is rendered DISABLED WITH ITS REASON, never missing and never
39468
+ * dead: disabled must not look like broken.
39469
+ *
39470
+ * ## Refusal by name
39471
+ *
39472
+ * A write a camera cannot honour is refused with a sentence the operator
39473
+ * can read — never accepted and dropped. Both providers refuse through
39474
+ * {@link describeOnboardRefusal}, so the vocabulary is one function and
39475
+ * one test, not two hand-written vendor opinions.
39476
+ *
39477
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
39478
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
39479
+ * injected from `status`) reports the live values, and a single
39480
+ * `setSettings` mutation applies a partial change. No hand-written
39481
+ * settings-contribution methods.
39482
+ */
39483
+ /**
39484
+ * What makes the camera start recording during a window.
39485
+ *
39486
+ * The union of both vendors' vocabularies. `continuous` is Hikvision's
39487
+ * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
39488
+ * object-class triggers are Reolink-only today and the smart-event ones
39489
+ * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
39490
+ * firmwares measured — a camera that cannot record on a trigger simply
39491
+ * does not list it in `options.schedule.triggers`, and a window naming
39492
+ * it is REFUSED, not dropped.
39493
+ */
39494
+ var RecordTriggerSchema = _enum([
39495
+ "continuous",
39496
+ "motion",
39497
+ "person",
39498
+ "vehicle",
39499
+ "animal",
39500
+ "lineCrossing",
39501
+ "intrusion",
39502
+ "loitering",
39503
+ "alarmInput"
39504
+ ]);
39505
+ /**
39506
+ * One weekly recording window: "on `day`, from `startMinute` to
39507
+ * `endMinute`, record on `trigger`".
39508
+ *
39509
+ * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
39510
+ * both firmwares enumerate). Minutes are local camera time since
39511
+ * midnight; `endMinute` may be 1440, meaning end of day — that is
39512
+ * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
39513
+ * collapsing it to 0 would turn a whole-day window into an empty one.
39514
+ */
39515
+ var RecordWindowSchema = object({
39516
+ trigger: RecordTriggerSchema,
39517
+ day: number().int().min(0).max(6),
39518
+ startMinute: number().int().min(0).max(1439),
39519
+ endMinute: number().int().min(1).max(1440)
39520
+ });
39521
+ /** Status of one physical volume, as the camera itself describes it. */
39522
+ var OnboardStorageVolumeSchema = object({
39523
+ /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
39524
+ id: string(),
39525
+ /** The camera's own name for it, when it gives one (`hddName`). */
39526
+ label: string().optional(),
39527
+ status: _enum([
39528
+ "ok",
39529
+ "unformatted",
39530
+ "error",
39531
+ "offline",
39532
+ "unknown"
39533
+ ]),
39534
+ /**
39535
+ * Total size in MB, or **null when the camera did not say**.
39536
+ *
39537
+ * Never 0 for an unreadable value: a measurement that failed is not a
39538
+ * measurement (D393), and a card whose size is unknown must not be
39539
+ * rendered as a card of size zero.
39540
+ */
39541
+ capacityMb: number().nullable(),
39542
+ /**
39543
+ * Free space in MB, or null when unknown.
39544
+ *
39545
+ * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
39546
+ * 1439 both report exactly 11776 MB free — the fixed reserve a looping
39547
+ * card converges on once it has wrapped. At loop steady state the
39548
+ * number is identical whether the camera recorded yesterday or stopped
39549
+ * a month ago.
39550
+ */
39551
+ freeMb: number().nullable(),
39552
+ /** True when the camera reports the volume writable (`property` RW). */
39553
+ writable: boolean().optional()
39554
+ });
39555
+ /**
39556
+ * What the camera is doing with its own storage, right now.
39557
+ *
39558
+ * Every scalar is nullable and **null means the camera did not answer**,
39559
+ * never a default. A form that seeds `0` from an unanswered read invites
39560
+ * the operator to save that 0 back onto the camera.
39561
+ */
39562
+ var RecordingOnboardStatusSchema = object({
39563
+ storage: discriminatedUnion("kind", [
39564
+ object({
39565
+ kind: literal("present"),
39566
+ volumes: array(OnboardStorageVolumeSchema)
39567
+ }),
39568
+ object({
39569
+ kind: literal("absent"),
39570
+ reason: string()
39571
+ }),
39572
+ object({
39573
+ kind: literal("unknown"),
39574
+ reason: string()
39575
+ })
39576
+ ]),
39577
+ tracks: array(object({
39578
+ id: string(),
39579
+ enabled: boolean(),
39580
+ isVideo: boolean(),
39581
+ /** From the camera's own track description. Null when it does not say. */
39582
+ codec: string().nullable(),
39583
+ resolution: string().nullable(),
39584
+ /** Per-track overwrite flag, where the firmware keeps it per track. */
39585
+ overwriteWhenFull: boolean().nullable()
39586
+ })),
39587
+ /**
39588
+ * The track the write path targets — the enabled VIDEO one. Null when
39589
+ * no track could be identified, which is itself a refusal reason.
39590
+ */
39591
+ primaryTrackId: string().nullable(),
39592
+ /** Master "record to the card at all" switch. */
39593
+ enabled: boolean().nullable(),
39594
+ overwriteWhenFull: boolean().nullable(),
39595
+ preRecordSec: number().nullable(),
39596
+ postRecordSec: number().nullable(),
39597
+ /** Length of one recorded file, in minutes. */
39598
+ segmentMinutes: number().nullable(),
39599
+ /** The primary track's weekly windows, flattened. */
39600
+ windows: array(RecordWindowSchema),
39601
+ /**
39602
+ * How many windows the camera described that CamStack could NOT read —
39603
+ * an unrecognised trigger, an unparseable clock, a weekday it does not
39604
+ * name.
39605
+ *
39606
+ * A dropped window is work the reader threw away, and a schedule that
39607
+ * silently shows fewer rows than the camera holds is how an operator
39608
+ * saves back a schedule shorter than the one they were looking at
39609
+ * (D391). Non-zero means the window list is INCOMPLETE and a write
39610
+ * that replaces it would delete what was not shown — which is why a
39611
+ * provider reporting a non-zero count also reports the schedule as not
39612
+ * writable.
39613
+ */
39614
+ unreadableWindows: number(),
39615
+ /**
39616
+ * The camera is scheduled to record and has NO usable storage.
39617
+ *
39618
+ * A first-class fact because it is the fleet's most common silent
39619
+ * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
39620
+ * to a card that is not there. Neither the schedule nor the storage
39621
+ * read says anything wrong on its own; only the pair does.
39622
+ */
39623
+ recordingToNowhere: boolean(),
39624
+ lastFetchedAt: number()
39625
+ });
39626
+ /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
39627
+ var RangeSchema = object({
39628
+ min: number(),
39629
+ max: number(),
39630
+ step: number()
39631
+ });
39632
+ /**
39633
+ * The values a camera actually takes for a numeric field, when they are a SET
39634
+ * rather than a range.
39635
+ *
39636
+ * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
39637
+ * (I91DN) on 2026-09-22 by writing each value and reading it back:
39638
+ *
39639
+ * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
39640
+ * camera's "no limit" — `-1` and `4294967295` both land on it);
39641
+ * - post-record: `5, 10, 30, 60, 120, 300, 600`.
39642
+ *
39643
+ * Neither is expressible as a step: the first has a sentinel two billion away
39644
+ * from its neighbours, the second doubles and then jumps. A range that tried
39645
+ * would forbid values the camera takes AND permit values it silently replaces
39646
+ * with 5 — wrong in both directions at once.
39647
+ *
39648
+ * `sentinel` names the member that is not a duration, so a surface can render
39649
+ * "no limit" instead of `2147483647` seconds.
39650
+ */
39651
+ var AllowedValuesSchema = object({
39652
+ values: array(number()).min(1),
39653
+ sentinel: object({
39654
+ value: number(),
39655
+ meaning: _enum(["no-limit", "disabled"])
39656
+ }).optional()
39657
+ });
39658
+ /**
39659
+ * Per-field availability on ONE camera.
39660
+ *
39661
+ * The field exists on every camera — this says whether this one can be
39662
+ * read and whether it can be written, and `reason` says why not when
39663
+ * either is false. The UI renders the control DISABLED with the reason
39664
+ * rather than hiding it, so a limitation is legible instead of looking
39665
+ * like a missing feature.
39666
+ */
39667
+ var OnboardFieldSupportSchema = object({
39668
+ readable: boolean(),
39669
+ writable: boolean(),
39670
+ /** Required whenever `readable` or `writable` is false. */
39671
+ reason: string().optional()
39672
+ });
39673
+ /** What this camera's schedule model can express. */
39674
+ var OnboardScheduleSupportSchema = object({
39675
+ support: OnboardFieldSupportSchema,
39676
+ /**
39677
+ * The smallest time step the camera can express, in minutes.
39678
+ *
39679
+ * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
39680
+ * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
39681
+ * window whose edges are not a multiple of this is REFUSED rather than
39682
+ * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
39683
+ * and nothing says so.
39684
+ */
39685
+ granularityMinutes: number(),
39686
+ /** Triggers this camera can record on. A window naming another is refused. */
39687
+ triggers: array(RecordTriggerSchema),
39688
+ /**
39689
+ * False when the camera stores ONE trigger per time range, so two
39690
+ * windows overlapping on the same day cannot carry different triggers.
39691
+ * True on Reolink, whose mask is per-trigger and independent.
39692
+ */
39693
+ supportsOverlappingTriggers: boolean()
39694
+ });
39695
+ var RecordingOnboardOptionsSchema = object({
39696
+ enabled: OnboardFieldSupportSchema,
39697
+ overwriteWhenFull: OnboardFieldSupportSchema,
39698
+ preRecordSec: OnboardFieldSupportSchema,
39699
+ preRecordSecRange: RangeSchema.optional(),
39700
+ /** Preferred over the range when the camera takes a SET, not a span. */
39701
+ preRecordSecAllowed: AllowedValuesSchema.optional(),
39702
+ postRecordSec: OnboardFieldSupportSchema,
39703
+ postRecordSecRange: RangeSchema.optional(),
39704
+ /** Preferred over the range when the camera takes a SET, not a span. */
39705
+ postRecordSecAllowed: AllowedValuesSchema.optional(),
39706
+ segmentMinutes: OnboardFieldSupportSchema,
39707
+ segmentMinutesRange: RangeSchema.optional(),
39708
+ /** Preferred over the range when the camera takes a SET, not a span. */
39709
+ segmentMinutesAllowed: AllowedValuesSchema.optional(),
39710
+ schedule: OnboardScheduleSupportSchema
39711
+ });
39712
+ /**
39713
+ * A partial change. Every field optional.
39714
+ *
39715
+ * Unlike the other `deviceConfig` caps, a provider here does **NOT**
39716
+ * silently ignore a field it cannot support — it refuses, by name,
39717
+ * through {@link describeOnboardRefusal}. Silence on a recording setting
39718
+ * is the failure D62 exists to prevent: the operator believes the camera
39719
+ * is recording the way the form says, and it is not.
39720
+ */
39721
+ var RecordingOnboardPatchSchema = object({
39722
+ enabled: boolean().optional(),
39723
+ overwriteWhenFull: boolean().optional(),
39724
+ preRecordSec: number().optional(),
39725
+ postRecordSec: number().optional(),
39726
+ segmentMinutes: number().optional(),
39727
+ /** The complete new window set for the primary track — not a delta. */
39728
+ windows: array(RecordWindowSchema).optional()
39729
+ });
39730
+ var recordingOnboardCapability = {
39731
+ name: "recording-onboard",
39732
+ scope: "device",
39733
+ deviceNative: true,
39734
+ mode: "singleton",
39735
+ deviceTypes: [DeviceType.Camera],
39736
+ deviceConfig: { ui: {
39737
+ kind: "derived-form",
39738
+ builderId: "recording-onboard",
39739
+ tab: "recording"
39740
+ } },
39741
+ methods: {
39742
+ getOptions: method(object({ deviceId: number() }), RecordingOnboardOptionsSchema),
39743
+ setSettings: method(object({
39744
+ deviceId: number(),
39745
+ settings: RecordingOnboardPatchSchema
39746
+ }), _void(), {
39747
+ kind: "mutation",
39748
+ auth: "admin"
39749
+ })
39750
+ },
39751
+ status: {
39752
+ schema: RecordingOnboardStatusSchema,
39753
+ kind: "poll"
39754
+ },
39755
+ runtimeState: RecordingOnboardStatusSchema,
39756
+ /**
39757
+ * Runtime-state durability: **restored** — operator-set camera-side
39758
+ * recording config; mutation-driven, and the storage half is the last
39759
+ * thing the camera said about its own card.
39760
+ *
39761
+ * See `RuntimeStateDurability`. Enforced by
39762
+ * `scripts/check-runtime-state-durability.ts`.
39763
+ */
39764
+ durability: "restored",
39765
+ /** Clock fields: written, but excluded from the compare that decides
39766
+ * whether persisting is worth a SQLite commit. */
39767
+ volatileStateFields: ["lastFetchedAt"]
39768
+ };
39769
+ /**
39503
39770
  * A camera's own "record me NOW" LEVEL — a signal the device raises while
39504
39771
  * something it knows about is happening (a robot vacuum cleaning, a machine
39505
39772
  * running, a gate open) and lowers when it stops.
@@ -43583,7 +43850,6 @@ var ALL_CAPABILITY_DEFINITIONS = [
43583
43850
  embeddingEncoderCapability,
43584
43851
  enumSensorCapability,
43585
43852
  eventEmitterCapability,
43586
- eventsCapability,
43587
43853
  faceGalleryCapability,
43588
43854
  failureContributionCapability,
43589
43855
  fanControlCapability,
@@ -43644,6 +43910,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
43644
43910
  ptzAutotrackCapability,
43645
43911
  rebootCapability,
43646
43912
  recordingCapability,
43913
+ recordingArchiveCapability,
43647
43914
  recordingExportCapability,
43648
43915
  recordingOnboardCapability,
43649
43916
  recordingSignalCapability,
@@ -45878,24 +46145,6 @@ Object.freeze({
45878
46145
  addonId: null,
45879
46146
  access: "view"
45880
46147
  },
45881
- "events.getEventClipUrl": {
45882
- capName: "events",
45883
- capScope: "device",
45884
- addonId: null,
45885
- access: "view"
45886
- },
45887
- "events.getEvents": {
45888
- capName: "events",
45889
- capScope: "device",
45890
- addonId: null,
45891
- access: "view"
45892
- },
45893
- "events.getEventThumbnail": {
45894
- capName: "events",
45895
- capScope: "device",
45896
- addonId: null,
45897
- access: "view"
45898
- },
45899
46148
  "faceGallery.assignFace": {
45900
46149
  capName: "face-gallery",
45901
46150
  capScope: "system",
@@ -48770,224 +49019,236 @@ Object.freeze({
48770
49019
  addonId: null,
48771
49020
  access: "create"
48772
49021
  },
48773
- "recording.applyDeviceSettingsPatch": {
49022
+ "recording.getAvailability": {
48774
49023
  capName: "recording",
48775
- capScope: "system",
49024
+ capScope: "device",
48776
49025
  addonId: null,
48777
- access: "create"
49026
+ access: "view"
48778
49027
  },
48779
- "recording.cancelRelocateJob": {
49028
+ "recording.getDaysWithRecordings": {
48780
49029
  capName: "recording",
48781
- capScope: "system",
49030
+ capScope: "device",
48782
49031
  addonId: null,
48783
- access: "create"
49032
+ access: "view"
48784
49033
  },
48785
- "recording.cancelStorageMigrationMove": {
49034
+ "recording.getPlayback": {
48786
49035
  capName: "recording",
48787
- capScope: "system",
49036
+ capScope: "device",
48788
49037
  addonId: null,
48789
- access: "create"
49038
+ access: "view"
48790
49039
  },
48791
- "recording.deleteFootprint": {
49040
+ "recording.getPlaybackOptions": {
48792
49041
  capName: "recording",
48793
- capScope: "system",
49042
+ capScope: "device",
48794
49043
  addonId: null,
48795
- access: "delete"
49044
+ access: "view"
48796
49045
  },
48797
- "recording.getAvailability": {
49046
+ "recording.listSources": {
48798
49047
  capName: "recording",
48799
- capScope: "system",
49048
+ capScope: "device",
48800
49049
  addonId: null,
48801
49050
  access: "view"
48802
49051
  },
48803
- "recording.getAvailabilityBatch": {
48804
- capName: "recording",
49052
+ "recordingArchive.applyDeviceSettingsPatch": {
49053
+ capName: "recording-archive",
48805
49054
  capScope: "system",
48806
49055
  addonId: null,
48807
- access: "view"
49056
+ access: "create"
48808
49057
  },
48809
- "recording.getDaysWithRecordings": {
48810
- capName: "recording",
49058
+ "recordingArchive.cancelRelocateJob": {
49059
+ capName: "recording-archive",
48811
49060
  capScope: "system",
48812
49061
  addonId: null,
48813
- access: "view"
49062
+ access: "create"
48814
49063
  },
48815
- "recording.getDaysWithRecordingsBatch": {
48816
- capName: "recording",
49064
+ "recordingArchive.cancelStorageMigrationMove": {
49065
+ capName: "recording-archive",
49066
+ capScope: "system",
49067
+ addonId: null,
49068
+ access: "create"
49069
+ },
49070
+ "recordingArchive.deleteFootprint": {
49071
+ capName: "recording-archive",
49072
+ capScope: "system",
49073
+ addonId: null,
49074
+ access: "delete"
49075
+ },
49076
+ "recordingArchive.getAvailabilityBatch": {
49077
+ capName: "recording-archive",
48817
49078
  capScope: "system",
48818
49079
  addonId: null,
48819
49080
  access: "view"
48820
49081
  },
48821
- "recording.getDeviceConfig": {
48822
- capName: "recording",
49082
+ "recordingArchive.getDaysWithRecordingsBatch": {
49083
+ capName: "recording-archive",
48823
49084
  capScope: "system",
48824
49085
  addonId: null,
48825
49086
  access: "view"
48826
49087
  },
48827
- "recording.getDeviceLiveContribution": {
48828
- capName: "recording",
49088
+ "recordingArchive.getDeviceConfig": {
49089
+ capName: "recording-archive",
48829
49090
  capScope: "system",
48830
49091
  addonId: null,
48831
49092
  access: "view"
48832
49093
  },
48833
- "recording.getDeviceSettingsContribution": {
48834
- capName: "recording",
49094
+ "recordingArchive.getDeviceLiveContribution": {
49095
+ capName: "recording-archive",
48835
49096
  capScope: "system",
48836
49097
  addonId: null,
48837
49098
  access: "view"
48838
49099
  },
48839
- "recording.getPlacement": {
48840
- capName: "recording",
49100
+ "recordingArchive.getDeviceSettingsContribution": {
49101
+ capName: "recording-archive",
48841
49102
  capScope: "system",
48842
49103
  addonId: null,
48843
49104
  access: "view"
48844
49105
  },
48845
- "recording.getPlaybackManifest": {
48846
- capName: "recording",
49106
+ "recordingArchive.getPlacement": {
49107
+ capName: "recording-archive",
48847
49108
  capScope: "system",
48848
49109
  addonId: null,
48849
49110
  access: "view"
48850
49111
  },
48851
- "recording.getRelocateResidue": {
48852
- capName: "recording",
49112
+ "recordingArchive.getRelocateResidue": {
49113
+ capName: "recording-archive",
48853
49114
  capScope: "system",
48854
49115
  addonId: null,
48855
49116
  access: "view"
48856
49117
  },
48857
- "recording.getStatus": {
48858
- capName: "recording",
49118
+ "recordingArchive.getStatus": {
49119
+ capName: "recording-archive",
48859
49120
  capScope: "system",
48860
49121
  addonId: null,
48861
49122
  access: "view"
48862
49123
  },
48863
- "recording.getStorageMigrationMoveStatus": {
48864
- capName: "recording",
49124
+ "recordingArchive.getStorageMigrationMoveStatus": {
49125
+ capName: "recording-archive",
48865
49126
  capScope: "system",
48866
49127
  addonId: null,
48867
49128
  access: "view"
48868
49129
  },
48869
- "recording.getStorageUsage": {
48870
- capName: "recording",
49130
+ "recordingArchive.getStorageUsage": {
49131
+ capName: "recording-archive",
48871
49132
  capScope: "system",
48872
49133
  addonId: null,
48873
49134
  access: "view"
48874
49135
  },
48875
- "recording.listOpsLog": {
48876
- capName: "recording",
49136
+ "recordingArchive.listOpsLog": {
49137
+ capName: "recording-archive",
48877
49138
  capScope: "system",
48878
49139
  addonId: null,
48879
49140
  access: "view"
48880
49141
  },
48881
- "recording.listRelocateJobs": {
48882
- capName: "recording",
49142
+ "recordingArchive.listRelocateJobs": {
49143
+ capName: "recording-archive",
48883
49144
  capScope: "system",
48884
49145
  addonId: null,
48885
49146
  access: "view"
48886
49147
  },
48887
- "recording.locateSegment": {
48888
- capName: "recording",
49148
+ "recordingArchive.locateSegment": {
49149
+ capName: "recording-archive",
48889
49150
  capScope: "system",
48890
49151
  addonId: null,
48891
49152
  access: "view"
48892
49153
  },
48893
- "recording.pauseForStorageMigration": {
48894
- capName: "recording",
49154
+ "recordingArchive.pauseForStorageMigration": {
49155
+ capName: "recording-archive",
48895
49156
  capScope: "system",
48896
49157
  addonId: null,
48897
49158
  access: "create"
48898
49159
  },
48899
- "recording.planStorageRebalance": {
48900
- capName: "recording",
49160
+ "recordingArchive.planStorageRebalance": {
49161
+ capName: "recording-archive",
48901
49162
  capScope: "system",
48902
49163
  addonId: null,
48903
49164
  access: "view"
48904
49165
  },
48905
- "recording.pruneFootage": {
48906
- capName: "recording",
49166
+ "recordingArchive.pruneFootage": {
49167
+ capName: "recording-archive",
48907
49168
  capScope: "system",
48908
49169
  addonId: null,
48909
49170
  access: "create"
48910
49171
  },
48911
- "recording.readGopBytes": {
48912
- capName: "recording",
49172
+ "recordingArchive.readGopBytes": {
49173
+ capName: "recording-archive",
48913
49174
  capScope: "system",
48914
49175
  addonId: null,
48915
49176
  access: "view"
48916
49177
  },
48917
- "recording.readSegmentBytes": {
48918
- capName: "recording",
49178
+ "recordingArchive.readSegmentBytes": {
49179
+ capName: "recording-archive",
48919
49180
  capScope: "system",
48920
49181
  addonId: null,
48921
49182
  access: "view"
48922
49183
  },
48923
- "recording.readWindowBytes": {
48924
- capName: "recording",
49184
+ "recordingArchive.readWindowBytes": {
49185
+ capName: "recording-archive",
48925
49186
  capScope: "system",
48926
49187
  addonId: null,
48927
49188
  access: "view"
48928
49189
  },
48929
- "recording.reconcileLedgerAgainstDisk": {
48930
- capName: "recording",
49190
+ "recordingArchive.reconcileLedgerAgainstDisk": {
49191
+ capName: "recording-archive",
48931
49192
  capScope: "system",
48932
49193
  addonId: null,
48933
49194
  access: "create"
48934
49195
  },
48935
- "recording.refreshStorageLocationsForMigration": {
48936
- capName: "recording",
49196
+ "recordingArchive.refreshStorageLocationsForMigration": {
49197
+ capName: "recording-archive",
48937
49198
  capScope: "system",
48938
49199
  addonId: null,
48939
49200
  access: "create"
48940
49201
  },
48941
- "recording.relocateFootage": {
48942
- capName: "recording",
49202
+ "recordingArchive.relocateFootage": {
49203
+ capName: "recording-archive",
48943
49204
  capScope: "system",
48944
49205
  addonId: null,
48945
49206
  access: "create"
48946
49207
  },
48947
- "recording.renderClip": {
48948
- capName: "recording",
49208
+ "recordingArchive.renderClip": {
49209
+ capName: "recording-archive",
48949
49210
  capScope: "system",
48950
49211
  addonId: null,
48951
49212
  access: "create"
48952
49213
  },
48953
- "recording.renderGif": {
48954
- capName: "recording",
49214
+ "recordingArchive.renderGif": {
49215
+ capName: "recording-archive",
48955
49216
  capScope: "system",
48956
49217
  addonId: null,
48957
49218
  access: "create"
48958
49219
  },
48959
- "recording.rescanStorage": {
48960
- capName: "recording",
49220
+ "recordingArchive.rescanStorage": {
49221
+ capName: "recording-archive",
48961
49222
  capScope: "system",
48962
49223
  addonId: null,
48963
49224
  access: "create"
48964
49225
  },
48965
- "recording.resumeForStorageMigration": {
48966
- capName: "recording",
49226
+ "recordingArchive.resumeForStorageMigration": {
49227
+ capName: "recording-archive",
48967
49228
  capScope: "system",
48968
49229
  addonId: null,
48969
49230
  access: "create"
48970
49231
  },
48971
- "recording.setDeviceConfig": {
48972
- capName: "recording",
49232
+ "recordingArchive.setDeviceConfig": {
49233
+ capName: "recording-archive",
48973
49234
  capScope: "system",
48974
49235
  addonId: null,
48975
49236
  access: "create"
48976
49237
  },
48977
- "recording.setDevicePlacement": {
48978
- capName: "recording",
49238
+ "recordingArchive.setDevicePlacement": {
49239
+ capName: "recording-archive",
48979
49240
  capScope: "system",
48980
49241
  addonId: null,
48981
49242
  access: "create"
48982
49243
  },
48983
- "recording.startStorageMigrationMove": {
48984
- capName: "recording",
49244
+ "recordingArchive.startStorageMigrationMove": {
49245
+ capName: "recording-archive",
48985
49246
  capScope: "system",
48986
49247
  addonId: null,
48987
49248
  access: "create"
48988
49249
  },
48989
- "recording.startStorageRebalance": {
48990
- capName: "recording",
49250
+ "recordingArchive.startStorageRebalance": {
49251
+ capName: "recording-archive",
48991
49252
  capScope: "system",
48992
49253
  addonId: null,
48993
49254
  access: "create"
@@ -51292,21 +51553,6 @@ Object.freeze({
51292
51553
  form: "single",
51293
51554
  optional: false
51294
51555
  }],
51295
- "events.getEventClipUrl": [{
51296
- name: "deviceId",
51297
- form: "single",
51298
- optional: false
51299
- }],
51300
- "events.getEvents": [{
51301
- name: "deviceId",
51302
- form: "single",
51303
- optional: false
51304
- }],
51305
- "events.getEventThumbnail": [{
51306
- name: "deviceId",
51307
- form: "single",
51308
- optional: false
51309
- }],
51310
51556
  "faceGallery.getFaceByTrack": [{
51311
51557
  name: "deviceId",
51312
51558
  form: "single",
@@ -52225,107 +52471,117 @@ Object.freeze({
52225
52471
  form: "single",
52226
52472
  optional: false
52227
52473
  }],
52228
- "recording.deleteFootprint": [{
52474
+ "recording.getAvailability": [{
52229
52475
  name: "deviceId",
52230
52476
  form: "single",
52231
52477
  optional: false
52232
52478
  }],
52233
- "recording.getAvailability": [{
52479
+ "recording.getDaysWithRecordings": [{
52234
52480
  name: "deviceId",
52235
52481
  form: "single",
52236
52482
  optional: false
52237
52483
  }],
52238
- "recording.getAvailabilityBatch": [{
52239
- name: "deviceIds",
52240
- form: "array",
52484
+ "recording.getPlayback": [{
52485
+ name: "deviceId",
52486
+ form: "single",
52241
52487
  optional: false
52242
52488
  }],
52243
- "recording.getDaysWithRecordings": [{
52489
+ "recording.getPlaybackOptions": [{
52244
52490
  name: "deviceId",
52245
52491
  form: "single",
52246
52492
  optional: false
52247
52493
  }],
52248
- "recording.getDaysWithRecordingsBatch": [{
52249
- name: "deviceIds",
52250
- form: "array",
52494
+ "recording.listSources": [{
52495
+ name: "deviceId",
52496
+ form: "single",
52251
52497
  optional: false
52252
52498
  }],
52253
- "recording.getDeviceConfig": [{
52499
+ "recordingArchive.deleteFootprint": [{
52254
52500
  name: "deviceId",
52255
52501
  form: "single",
52256
52502
  optional: false
52257
52503
  }],
52258
- "recording.getPlaybackManifest": [{
52504
+ "recordingArchive.getAvailabilityBatch": [{
52505
+ name: "deviceIds",
52506
+ form: "array",
52507
+ optional: false
52508
+ }],
52509
+ "recordingArchive.getDaysWithRecordingsBatch": [{
52510
+ name: "deviceIds",
52511
+ form: "array",
52512
+ optional: false
52513
+ }],
52514
+ "recordingArchive.getDeviceConfig": [{
52259
52515
  name: "deviceId",
52260
52516
  form: "single",
52261
52517
  optional: false
52262
52518
  }],
52263
- "recording.listOpsLog": [{
52519
+ "recordingArchive.listOpsLog": [{
52264
52520
  name: "deviceId",
52265
52521
  form: "single",
52266
52522
  optional: true
52267
52523
  }],
52268
- "recording.locateSegment": [{
52524
+ "recordingArchive.locateSegment": [{
52269
52525
  name: "deviceId",
52270
52526
  form: "single",
52271
52527
  optional: false
52272
52528
  }],
52273
- "recording.pruneFootage": [{
52529
+ "recordingArchive.pruneFootage": [{
52274
52530
  name: "deviceId",
52275
52531
  form: "single",
52276
52532
  optional: false
52277
52533
  }],
52278
- "recording.readGopBytes": [{
52534
+ "recordingArchive.readGopBytes": [{
52279
52535
  name: "deviceId",
52280
52536
  form: "single",
52281
52537
  optional: false
52282
52538
  }],
52283
- "recording.readSegmentBytes": [{
52539
+ "recordingArchive.readSegmentBytes": [{
52284
52540
  name: "deviceId",
52285
52541
  form: "single",
52286
52542
  optional: false
52287
52543
  }],
52288
- "recording.readWindowBytes": [{
52544
+ "recordingArchive.readWindowBytes": [{
52289
52545
  name: "deviceId",
52290
52546
  form: "single",
52291
52547
  optional: false
52292
52548
  }],
52293
- "recording.reconcileLedgerAgainstDisk": [{
52549
+ "recordingArchive.reconcileLedgerAgainstDisk": [{
52294
52550
  name: "deviceId",
52295
52551
  form: "single",
52296
52552
  optional: true
52297
52553
  }],
52298
- "recording.relocateFootage": [{
52554
+ "recordingArchive.relocateFootage": [{
52299
52555
  name: "deviceId",
52300
52556
  form: "single",
52301
52557
  optional: true
52302
52558
  }],
52303
- "recording.renderClip": [{
52559
+ "recordingArchive.renderClip": [{
52304
52560
  name: "deviceId",
52305
52561
  form: "single",
52306
52562
  optional: false
52307
52563
  }],
52308
- "recording.renderGif": [{
52564
+ "recordingArchive.renderGif": [{
52309
52565
  name: "deviceId",
52310
52566
  form: "single",
52311
52567
  optional: false
52312
52568
  }],
52313
- "recording.rescanStorage": [{
52569
+ "recordingArchive.rescanStorage": [{
52314
52570
  name: "deviceId",
52315
52571
  form: "single",
52316
52572
  optional: false
52317
52573
  }],
52318
- "recording.setDeviceConfig": [{
52574
+ "recordingArchive.setDeviceConfig": [{
52319
52575
  name: "deviceId",
52320
52576
  form: "single",
52321
52577
  optional: false
52322
52578
  }],
52323
- "recording.setDevicePlacement": [{
52579
+ "recordingArchive.setDevicePlacement": [{
52324
52580
  name: "deviceId",
52325
52581
  form: "single",
52326
52582
  optional: false
52327
52583
  }],
52328
- "recording.startStorageMigrationMove": [{
52584
+ "recordingArchive.startStorageMigrationMove": [{
52329
52585
  name: "deviceId",
52330
52586
  form: "single",
52331
52587
  optional: true