@camstack/addon-osd-manager 0.1.131 → 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-BEiIAXxX.mjs → MotionZonesSettings-2zsWJZfj.mjs} +2 -2
  2. package/dist/{PrivacyMaskSettings-CvIg_072.mjs → PrivacyMaskSettings-DrvVVdy1.mjs} +4 -4
  3. package/dist/{SceneMonitorEditor-JZwBd1Jv.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-NFf3Y4nO.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-CwDM2suA.mjs → hostInit-BnK9fAeZ.mjs} +2 -2
  8. package/dist/index.js +1425 -971
  9. package/dist/index.mjs +1425 -971
  10. package/dist/{player-overlays-9RdwSS8n.mjs → player-overlays-DrhBQRby.mjs} +1 -1
  11. package/dist/remoteEntry.js +1 -1
  12. package/dist/{responsive-DOUWrPJ-.mjs → responsive-D3JdB_Kk.mjs} +1 -1
  13. package/dist/{scene-monitor-copy-Cr8ZNK1p.mjs → scene-monitor-copy-y4eFIlLX.mjs} +1 -1
  14. package/dist/{square-DLZPuHya.mjs → square-CK1KHJ3v.mjs} +1 -1
  15. package/dist/{trash-2-DE6KKFYA.mjs → trash-2-jS-4ZOJJ.mjs} +1 -1
  16. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-8Z1DwgHL.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-D23J0_pM.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-i3eUVc-d.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",
@@ -7736,7 +7754,7 @@ var AdoptionJobSchema = object({
7736
7754
  * component's original options — detection to the detection-pipeline wrapper
7737
7755
  * binding, audio analysis to its own, recording to `RecordingConfig.enabled`
7738
7756
  * (which was always first-class; the switch was a veneer over
7739
- * `recording.setDeviceConfig`), notifications to a notification-center
7757
+ * `recordingArchive.setDeviceConfig`), notifications to a notification-center
7740
7758
  * per-device setting, the two camera planes to their own components.
7741
7759
  *
7742
7760
  * What survives is {@link composeSwitchedOff}: `CameraStatus.switchedOff`, the
@@ -7762,7 +7780,7 @@ var AdoptionJobSchema = object({
7762
7780
  * | `stream-broker` | `deviceManager.setDisabled` | `StreamBrokerManager.reconcileAllCatalogs` releases the brokers; `ensureBroker` refuses re-creation |
7763
7781
  * | `object-detection` | `deviceManager.setWrapperActive('detection-pipeline')` | `PipelineSettingsStore.resolvePipelineForDevice` returns `{ steps: [], audio: null }` |
7764
7782
  * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
7765
- * | `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 |
7766
7784
  * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
7767
7785
  * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
7768
7786
  * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
@@ -12082,87 +12100,6 @@ var cameraPipelineConfigCapability = {
12082
12100
  exposesDeviceSettings: true,
12083
12101
  methods: {}
12084
12102
  };
12085
- /**
12086
- * The signals a device can emit to WAKE its own stream.
12087
- *
12088
- * A camera whose stream is built on demand sleeps until something asks for it,
12089
- * and "something" cannot be a consumer that is merely attached — a Frigate-style
12090
- * puller holds a session open for ever, and treating that as demand would keep
12091
- * a battery camera awake for ever, which is the whole thing the battery is for
12092
- * (D173). So the wake has to come from the CAMERA: an event it noticed by
12093
- * itself, with no stream running.
12094
- *
12095
- * ## The vocabulary is the PROVIDER'S, not ours
12096
- *
12097
- * Like `consumables`, this cap declares no vocabulary of its own. A provider
12098
- * names each signal with a `code` it chooses and a `label` an operator reads.
12099
- * Reolink offers motion and camera-native detection; another provider may offer
12100
- * a tamper, a doorbell press, a PIR, or something no camera in this fleet has
12101
- * yet. A fixed enum here would mean every new signal is a framework release.
12102
- *
12103
- * It is deliberately NOT derived from the caps a device already binds. Whether
12104
- * a camera CAN push firmware motion is expressed by `motionSources` containing
12105
- * `'onboard'`, and whether it does AI on-camera by the `native-object-detection`
12106
- * binding — but both answer "what drives the detection pipeline", which is a
12107
- * different question from "what may wake a sleeping stream". A camera can do
12108
- * the first and not be trusted with the second, and the operator picks per
12109
- * camera. Two questions, two authorities.
12110
- *
12111
- * ## Availability is not permission
12112
- *
12113
- * `listSignals` says what the device CAN emit. Whether a given signal actually
12114
- * wakes the stream is the operator's per-camera choice, held by the broker
12115
- * alongside the cooldown — see the stream-broker cap's wake settings. A
12116
- * provider declaring a signal is not a provider enabling it.
12117
- */
12118
- /** One signal a device can emit. */
12119
- var StreamSignalSchema = object({
12120
- /** Stable id chosen by the provider, e.g. `'motion'`, `'person'`, `'tamper'`. */
12121
- code: string().min(1),
12122
- /** What an operator reads in the picker. The provider's own wording. */
12123
- label: string().min(1),
12124
- /**
12125
- * Whether the provider recommends this signal ON when a camera is first set
12126
- * up. A provider knows which of its signals are cheap and reliable; an
12127
- * operator should not have to discover that by trial. Reolink recommends
12128
- * both of its own.
12129
- */
12130
- recommended: boolean()
12131
- });
12132
- var StreamSignalsStatusSchema = object({
12133
- signals: array(StreamSignalSchema),
12134
- lastFetchedAt: number()
12135
- });
12136
- var streamSignalsCapability = {
12137
- name: "stream-signals",
12138
- scope: "device",
12139
- deviceNative: true,
12140
- mode: "singleton",
12141
- deviceTypes: Object.values(DeviceType),
12142
- runtimeState: StreamSignalsStatusSchema,
12143
- /**
12144
- * Runtime-state durability: **session** — mirrored in RAM, never written.
12145
- *
12146
- * The slice holds what the DEVICE says it can emit. That is a probed fact,
12147
- * not an operator choice: the provider re-declares it on every registration,
12148
- * so losing it loses nothing and persisting it would freeze an answer the
12149
- * camera is entitled to change. Measured the same day on the sibling case —
12150
- * `native-object-detection.supportedClasses` was persisted, and a firmware
12151
- * class the camera really detected stayed missing for the life of the row
12152
- * because the fix could not reach it.
12153
- *
12154
- * See `RuntimeStateDurability`. Enforced by
12155
- * `scripts/check-runtime-state-durability.ts`.
12156
- */
12157
- durability: "session",
12158
- methods: {
12159
- /**
12160
- * What this device can emit. Empty is a valid and common answer — most
12161
- * cameras have nothing to offer here, and an empty list is what makes the
12162
- * broker's picker show nothing rather than a false choice.
12163
- */
12164
- listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
12165
- };
12166
12103
  /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
12167
12104
  var StreamFormatSchema = _enum([
12168
12105
  "webrtc",
@@ -14287,6 +14224,118 @@ var detectionPipelineCapability = {
14287
14224
  methods: {}
14288
14225
  };
14289
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
+ /**
14290
14339
  * Identity envelope for a device's upstream-system metadata.
14291
14340
  *
14292
14341
  * Two jobs:
@@ -14678,118 +14727,6 @@ var deviceAdoptionCapability = {
14678
14727
  }
14679
14728
  };
14680
14729
  /**
14681
- * device-admin-link — "this device has a management page of its own, and here
14682
- * is its address".
14683
- *
14684
- * ## Why this is not a `deviceConfig` cap
14685
- *
14686
- * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
14687
- * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
14688
- * patch back through a setter; it costs a `builderId` reducer in
14689
- * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
14690
- * renders a form section. This cap answers ONE question with ONE read and
14691
- * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
14692
- * block, no `settings`, no `runtimeState` and no reducer — exactly like
14693
- * `reboot`, the other pure-RPC device-native cap.
14694
- *
14695
- * ## Absent, and the difference between "no page" and "we cannot say"
14696
- *
14697
- * The two are answered at DIFFERENT layers, on purpose:
14698
- *
14699
- * - **"We cannot say"** → the provider never registers the cap for that
14700
- * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
14701
- * fan are reached only through a vendor cloud; there is no address to hand
14702
- * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
14703
- * conditioner DO have a LAN IP, and still have no HTTP management page
14704
- * behind it. None of them register, so `deviceManager.getBindings` never
14705
- * lists the cap and no surface asks.
14706
- * - **"This device has no page, and I know that"** → the provider registers
14707
- * and `getAdminLink` returns `null`. This is the answer for a device whose
14708
- * sibling DOES have a page: a Reolink battery camera reached over UDP by
14709
- * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
14710
- * transport, a Home Assistant broker authenticated by supervisor token
14711
- * (which carries no `baseUrl` at all).
14712
- *
14713
- * Both draw NOTHING. A button that opens a browser error is worse than no
14714
- * button, and D62 is the same rule from the other side: an off switch is
14715
- * reported off, never made to look broken. There is no third state where the
14716
- * UI renders a disabled button "because the device might have a page".
14717
- *
14718
- * ## The URL never carries credentials
14719
- *
14720
- * Not in userinfo, not in a query string. Every provider builds through
14721
- * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
14722
- * scheme and path as separate arguments — there is no parameter a secret could
14723
- * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
14724
- * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
14725
- * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
14726
- * keeps providers from hand-rolling one anyway.
14727
- *
14728
- * This matters here more than anywhere else in the repo, because every provider
14729
- * that knows a device's host knows its PASSWORD too: `{ host, port, username,
14730
- * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
14731
- * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
14732
- * camera's own page will ask for its own login. That is correct, and pre-
14733
- * filling it is the operator's business, not ours.
14734
- *
14735
- * ## It is a LAN fact
14736
- *
14737
- * The URL addresses the device where the NODE can see it. It is not proxied,
14738
- * not made reachable from outside, and not sent anywhere. A surface renders it
14739
- * as a link the operator's own browser follows, on the operator's own network,
14740
- * or renders nothing.
14741
- */
14742
- /**
14743
- * Whose page is it. The distinction is for the OPERATOR, who needs to know
14744
- * before clicking whether he is about to land on a camera's own web server or
14745
- * inside Home Assistant.
14746
- */
14747
- var AdminLinkTargetEnum = _enum(["device", "integration"]);
14748
- var DeviceAdminLinkSchema = object({
14749
- /**
14750
- * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
14751
- * free of userinfo and of any credential-shaped query key.
14752
- */
14753
- url: string(),
14754
- /**
14755
- * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
14756
- * The PROVIDER names it, because only the provider knows what the page is;
14757
- * a UI that invented the label from the addon id would call the Home
14758
- * Assistant device page "Provider Homeassistant".
14759
- */
14760
- label: string(),
14761
- target: AdminLinkTargetEnum,
14762
- /**
14763
- * Host the URL points at, without scheme, port or path — for the tooltip, so
14764
- * an operator can see WHERE the button goes before he follows it. Redundant
14765
- * with `url` by construction; carried separately so no surface has to parse
14766
- * a URL to show it.
14767
- */
14768
- host: string()
14769
- });
14770
- var deviceAdminLinkCapability = {
14771
- name: "device-admin-link",
14772
- scope: "device",
14773
- deviceNative: true,
14774
- mode: "singleton",
14775
- methods: {
14776
- /**
14777
- * The device's management page, or `null` when this device has none.
14778
- *
14779
- * `auth: 'admin'` deliberately. This is administration, not actuation —
14780
- * the same bucket as `reboot` and `camera-credentials`, and explicitly NOT
14781
- * the actuation set `scripts/check-actuation-not-admin.ts` protects (D403).
14782
- * The URL is also a statement about the LAN, which a household member with
14783
- * a `view` grant on a light has no reason to be handed.
14784
- *
14785
- * The surfaces gate on the QUERY, never on a role they guessed: a caller
14786
- * without the right loses the query and draws nothing, which is the same
14787
- * thing a device with no page draws. There is no path on which a button
14788
- * appears and then fails — the D403 failure mode, from the other end.
14789
- */
14790
- getAdminLink: method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
14791
- };
14792
- /**
14793
14730
  * `device-export` — collection cap for addons that export camstack
14794
14731
  * devices to external ecosystems (HomeAssistant via MQTT discovery,
14795
14732
  * HomeKit/HAP, Alexa Smart Home, …).
@@ -28430,6 +28367,87 @@ var storageProviderCapability = {
28430
28367
  })
28431
28368
  }
28432
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
+ };
28433
28451
  /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
28434
28452
  var ProfileSettingsSchemaBridge = unknown().nullable();
28435
28453
  var ProfileSettingsBagSchema = record(string(), unknown());
@@ -29132,6 +29150,7 @@ _enum([
29132
29150
  "sleeping",
29133
29151
  "camera-refused",
29134
29152
  "no-keyframe",
29153
+ "decode-failed",
29135
29154
  "no-catalog-row",
29136
29155
  "unsupported",
29137
29156
  "unknown-device",
@@ -29406,6 +29425,32 @@ var ClipBytesSchema = object({
29406
29425
  durationMs: number().positive().optional()
29407
29426
  });
29408
29427
  /**
29428
+ * Where a clip's finished bytes can be TAKEN (D613) — the answer to
29429
+ * {@link videoclipsCapability.methods.offerClipBytes}.
29430
+ *
29431
+ * Everything {@link ClipBytesSchema} carries except the bytes themselves, plus
29432
+ * the one-shot ticket that leads to them. The metadata is answered BEFORE the
29433
+ * transfer on purpose: a consumer learns which twin it got, what to call the
29434
+ * file and how long the clip runs without having to read a byte, so a decision
29435
+ * it would make on that metadata (a wrong twin, an implausible duration) costs
29436
+ * no transfer at all.
29437
+ */
29438
+ var ClipBytesOfferSchema = object({
29439
+ /**
29440
+ * One shot, seconds-long, loopback, on the PROVIDER's own host. Open it with
29441
+ * `ctx.peerBytes.open(...)`, which refuses a ticket from another node by
29442
+ * name rather than dialling a port that means something else here.
29443
+ */
29444
+ ticket: PeerBytesTicketSchema,
29445
+ contentType: string(),
29446
+ /** Suggested filename, extension included. */
29447
+ name: string(),
29448
+ /** Which twin was actually served — see {@link ClipBytesSchema.served}. */
29449
+ served: CamProfileSchema,
29450
+ /** See {@link ClipBytesSchema.durationMs}. Absent when nothing measured it. */
29451
+ durationMs: number().positive().optional()
29452
+ });
29453
+ /**
29409
29454
  * Where a clip's STREAM can be dialled (D597) — the answer to
29410
29455
  * {@link videoclipsCapability.methods.dialClipStream}.
29411
29456
  *
@@ -29481,6 +29526,44 @@ var ClipStreamDialSchema = object({
29481
29526
  /** Why `servedAudio` is `none` although sound was asked for. */
29482
29527
  audioReason: ClipStreamAudioReasonSchema.optional()
29483
29528
  });
29529
+ /**
29530
+ * What a surface may DRAW for this provider's clips — the answer to
29531
+ * {@link videoclipsCapability.methods.getPlaybackOptions} (D612).
29532
+ *
29533
+ * The envelope is a PROVIDER fact, not a clip fact, and that is measured, not
29534
+ * assumed: the broker's `chooseClipPath` reads exactly two inputs — whether
29535
+ * `dialClipStream` and `readClipBytes` are wired — and both are constants of
29536
+ * the broker's own closure over the provider's methods. The `profile` it is
29537
+ * handed is explicitly not read. So every clip of a provider is served the
29538
+ * same way, and a per-clip channel carried a value that could not vary. The
29539
+ * per-clip `clipTransport` server message was removed for exactly that reason.
29540
+ *
29541
+ * Queried per camera, before a clip is picked, so a control is rendered or
29542
+ * DISABLED rather than offered and refused at play time (D62: a disabled
29543
+ * control reads as unavailable, one that undoes the gesture reads as broken).
29544
+ */
29545
+ var ClipPlaybackOptionsSchema = object({
29546
+ /**
29547
+ * How this provider's clips reach the player. `stream` is the provider's
29548
+ * forward-only fMP4 (D597); `file` is one bounded by-handle fetch of the
29549
+ * whole clip, `stbl` indexed (D575).
29550
+ */
29551
+ transport: _enum(["stream", "file"]),
29552
+ /** `forward` = only ahead of the playhead. `free` = anywhere. */
29553
+ seek: _enum(["forward", "free"]),
29554
+ /** Frame-step BACKWARD is meaningful. Forward always is. */
29555
+ stepBack: boolean(),
29556
+ /** Whether the scrub gesture is served, as opposed to refused by name. */
29557
+ scrub: boolean(),
29558
+ /**
29559
+ * The rates that can be delivered, ascending, always containing `1`. The
29560
+ * viewer draws its picker from this and from nothing else — a constant it
29561
+ * keeps instead is the second authority that produced the defect: `8` and
29562
+ * `16` were offered, the broker clamped them to `4`, and no line anywhere
29563
+ * said so. `0` is not a member: pause is the absence of a rate.
29564
+ */
29565
+ rates: array(number().positive()).min(1).readonly()
29566
+ });
29484
29567
  var ClipSourceAvailabilitySchema = object({
29485
29568
  state: _enum([
29486
29569
  "ok",
@@ -29659,14 +29742,18 @@ var videoclipsCapability = {
29659
29742
  *
29660
29743
  * `getClipPlayback` is the right answer for a player: it hands back a URL
29661
29744
  * on a plane the hub serves `access:'authenticated'`, which a browser and a
29662
- * viewer session satisfy. It is the wrong answer for another ADDON. There
29663
- * is no addon→addon byte transport in this framework — `AddonDataPlane`
29664
- * only lets an addon SERVE, on `127.0.0.1` behind a per-listener secret
29665
- * only the hub may present — so a recorder that wants a camera's clip
29666
- * cannot fetch that URL. This method is the one seam that exists for it,
29667
- * and it is deliberately the same shape (and the same bound) as
29668
- * `recordingExport.readExportBytes`, which exists for the mirror-image
29669
- * reason.
29745
+ * viewer session satisfy. It is the wrong answer for another ADDON: the
29746
+ * hub's proxy in front of that plane takes only a user credential, which
29747
+ * an addon does not hold, so a recorder that wants a camera's clip cannot
29748
+ * fetch that URL. This method is the shape that answer forced — the same
29749
+ * one (and the same bound) as `recordingExport.readExportBytes`.
29750
+ *
29751
+ * **It is no longer the only seam.** Until D613 there was no addon→addon
29752
+ * byte transport at all; there is now
29753
+ * ({@link offerClipBytes}, over `ctx.peerBytes`), it holds nothing on
29754
+ * either side, and it is what a clip EXPORT uses. This method remains for
29755
+ * a consumer that genuinely wants the bytes in hand, and as the named
29756
+ * fallback for a provider not yet redeployed.
29670
29757
  *
29671
29758
  * Routing needs no `provider` pin: the id is source-prefixed and
29672
29759
  * self-contained, so `device-collection-dispatch.ts` rule 3 hands the call
@@ -29733,6 +29820,65 @@ var videoclipsCapability = {
29733
29820
  auth: "protected"
29734
29821
  }),
29735
29822
  /**
29823
+ * Where this clip's finished bytes can be TAKEN — the by-handle read a
29824
+ * clip EXPORT pulls, over the addon→addon byte transport (D613).
29825
+ *
29826
+ * This is {@link readClipBytes} with the envelope removed. Same gates,
29827
+ * same vocabulary, same completion rules, same `served` contract — the
29828
+ * only difference is that the bytes travel over a one-shot loopback
29829
+ * socket instead of inside a base64 field, so neither side holds the
29830
+ * payload whole and the 50 MiB refusal on a long `high` twin stops
29831
+ * existing. The bound that remains is
29832
+ * {@link VIDEOCLIPS_MAX_OFFER_BYTES}, and it bounds the PRODUCER's own
29833
+ * copy rather than the transport.
29834
+ *
29835
+ * **The ticket is loopback and same-host.** A provider on an agent mints a
29836
+ * URL that means nothing on the hub, and `ctx.peerBytes.open` refuses it
29837
+ * `cross-node` by name rather than dialling whatever else holds that port
29838
+ * here. A consumer that can be on the other side of a node boundary from
29839
+ * its provider must be able to read that refusal and say so; it must not
29840
+ * treat it as "no bytes".
29841
+ *
29842
+ * **A ticket is a one-shot bearer credential with a seconds-long life.**
29843
+ * Take it immediately, never persist it, never log its `url`. An untaken
29844
+ * ticket costs the provider one map entry until its TTL, and outstanding
29845
+ * tickets are bounded — which is also what makes a per-frame misuse of
29846
+ * this method refuse by name rather than work slowly (D9/D18: this is a
29847
+ * by-handle fetch of finished media, not a frame pipe).
29848
+ *
29849
+ * Optional on the provider for the same reason `readClipBytes` is: a
29850
+ * source with no camera socket behind it has no bytes. A provider that
29851
+ * predates this method answers `NOT_IMPLEMENTED`, and a consumer may fall
29852
+ * back to `readClipBytes` — but it says so in the log, with the deploy
29853
+ * hint, because that fallback re-imposes the 50 MiB refusal and an
29854
+ * operator who sees `too-large-to-transfer` after this shipped is looking
29855
+ * at a stale addon, not at a clip that cannot be exported.
29856
+ */
29857
+ offerClipBytes: optionalMethod(object({
29858
+ deviceId: number(),
29859
+ clipId: string().min(1),
29860
+ /** WHICH provider holds the bytes — see `readClipBytes.provider`. */
29861
+ provider: string().min(1),
29862
+ /** Which twin — `low | mid` → the sub file, `high` → the main twin. */
29863
+ profile: CamProfileSchema.optional(),
29864
+ /**
29865
+ * The CALLER's byte bound, so an over-size clip is refused before the
29866
+ * camera is touched rather than after. Capped by
29867
+ * {@link VIDEOCLIPS_MAX_OFFER_BYTES} whatever is passed; absent means
29868
+ * that ceiling.
29869
+ */
29870
+ maxBytes: number().int().positive().optional(),
29871
+ /**
29872
+ * The operator's authorisation to wake a sleeping camera for this
29873
+ * read. Absent — the default — means a sleeping standalone battery
29874
+ * camera is REFUSED by name, before any session is opened.
29875
+ */
29876
+ wake: ClipWakeSchema.optional()
29877
+ }), ClipBytesOfferSchema, {
29878
+ kind: "query",
29879
+ auth: "protected"
29880
+ }),
29881
+ /**
29736
29882
  * Where this clip's STREAM can be dialled (D597) — the forward-only
29737
29883
  * fMP4 the provider writes from the first muxed byte, for the broker to
29738
29884
  * play through the same WebRTC session as recorded footage, with the
@@ -29769,6 +29915,36 @@ var videoclipsCapability = {
29769
29915
  }), ClipStreamDialSchema, {
29770
29916
  kind: "query",
29771
29917
  auth: "protected"
29918
+ }),
29919
+ /**
29920
+ * What a surface may DRAW for this provider's clips: the rates it can be
29921
+ * played at, whether scrub is served, how far a position may be moved,
29922
+ * and whether a backward frame-step means anything (D612).
29923
+ *
29924
+ * **The only authority.** The per-clip `clipTransport` server message
29925
+ * that used to carry the same answer was removed: the broker's transport
29926
+ * choice reads nothing that varies per clip, so the clip level had no
29927
+ * information the provider does not already have, and two channels that
29928
+ * can disagree are worse than one (D62).
29929
+ *
29930
+ * Asked per camera and per provider, so it must stay CHEAP — it is a
29931
+ * statement about wiring, answered from a constant, never a call to the
29932
+ * camera. A provider answers with one of {@link CLIP_PLAYBACK_OPTIONS}
29933
+ * and never composes an envelope of its own.
29934
+ *
29935
+ * Optional, and absence is load-bearing: a provider that has not answered
29936
+ * has not restricted anything, and a viewer reads it as the freedom it
29937
+ * always had. See D612 on the rollout order that absence implies.
29938
+ */
29939
+ getPlaybackOptions: optionalMethod(object({
29940
+ deviceId: number(),
29941
+ /** WHICH provider to ask — the `addonId` a {@link ClipSourceSchema}
29942
+ * row carries. Required for the same reason `listClips` requires it:
29943
+ * a collection cap has no "the bound one" to resolve to (D554). */
29944
+ provider: string().min(1)
29945
+ }), ClipPlaybackOptionsSchema, {
29946
+ kind: "query",
29947
+ auth: "protected"
29772
29948
  })
29773
29949
  }
29774
29950
  };
@@ -31505,6 +31681,179 @@ getCredentials: method(object({ deviceId: number() }), CameraCredentialsSchema.n
31505
31681
  }
31506
31682
  };
31507
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
+ /**
31508
31857
  * Carbon-monoxide alarm sensor. Drives Home Assistant `binary_sensor`
31509
31858
  * entries with `device_class: carbon_monoxide`. Push-driven.
31510
31859
  */
@@ -32315,346 +32664,6 @@ var dayNightCapability = {
32315
32664
  volatileStateFields: ["lastFetchedAt"]
32316
32665
  };
32317
32666
  /**
32318
- * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
32319
- * writes to the CAMERA's own card, on the camera's own schedule.
32320
- *
32321
- * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
32322
- * footage ledger, our storage locations, our retention. This one has a
32323
- * different authority — the camera's firmware — and per D62 it stores
32324
- * nothing of its own. Every value here is read from the camera and every
32325
- * write goes back to the camera; there is no CamStack-side mirror that
32326
- * could disagree with the device.
32327
- *
32328
- * ## One shape, two firmwares
32329
- *
32330
- * Measured 2026-09-22 against the live fleet:
32331
- *
32332
- * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
32333
- * | --- | --- | --- |
32334
- * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
32335
- * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
32336
- * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
32337
- * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
32338
- * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
32339
- * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
32340
- * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
32341
- * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
32342
- *
32343
- * The two schedule models look different and are the same thing in
32344
- * different coordinates: both answer "for this trigger, during which
32345
- * weekly windows does the camera record". {@link RecordWindow} is that
32346
- * question in one shape — Hikvision's ranges map straight onto it,
32347
- * Reolink's mask expands into hour-aligned windows.
32348
- *
32349
- * ## Union, not intersection
32350
- *
32351
- * **The same fields exist on every camera.** What differs per device is
32352
- * which VALUES that device accepts, and that is what {@link
32353
- * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
32354
- * per field plus the schedule's own limits. A control a camera cannot
32355
- * honour is rendered DISABLED WITH ITS REASON, never missing and never
32356
- * dead: disabled must not look like broken.
32357
- *
32358
- * ## Refusal by name
32359
- *
32360
- * A write a camera cannot honour is refused with a sentence the operator
32361
- * can read — never accepted and dropped. Both providers refuse through
32362
- * {@link describeOnboardRefusal}, so the vocabulary is one function and
32363
- * one test, not two hand-written vendor opinions.
32364
- *
32365
- * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
32366
- * `getOptions` advertises per-camera availability, `getStatus` (auto-
32367
- * injected from `status`) reports the live values, and a single
32368
- * `setSettings` mutation applies a partial change. No hand-written
32369
- * settings-contribution methods.
32370
- */
32371
- /**
32372
- * What makes the camera start recording during a window.
32373
- *
32374
- * The union of both vendors' vocabularies. `continuous` is Hikvision's
32375
- * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
32376
- * object-class triggers are Reolink-only today and the smart-event ones
32377
- * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
32378
- * firmwares measured — a camera that cannot record on a trigger simply
32379
- * does not list it in `options.schedule.triggers`, and a window naming
32380
- * it is REFUSED, not dropped.
32381
- */
32382
- var RecordTriggerSchema = _enum([
32383
- "continuous",
32384
- "motion",
32385
- "person",
32386
- "vehicle",
32387
- "animal",
32388
- "lineCrossing",
32389
- "intrusion",
32390
- "loitering",
32391
- "alarmInput"
32392
- ]);
32393
- /**
32394
- * One weekly recording window: "on `day`, from `startMinute` to
32395
- * `endMinute`, record on `trigger`".
32396
- *
32397
- * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
32398
- * both firmwares enumerate). Minutes are local camera time since
32399
- * midnight; `endMinute` may be 1440, meaning end of day — that is
32400
- * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
32401
- * collapsing it to 0 would turn a whole-day window into an empty one.
32402
- */
32403
- var RecordWindowSchema = object({
32404
- trigger: RecordTriggerSchema,
32405
- day: number().int().min(0).max(6),
32406
- startMinute: number().int().min(0).max(1439),
32407
- endMinute: number().int().min(1).max(1440)
32408
- });
32409
- /** Status of one physical volume, as the camera itself describes it. */
32410
- var OnboardStorageVolumeSchema = object({
32411
- /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
32412
- id: string(),
32413
- /** The camera's own name for it, when it gives one (`hddName`). */
32414
- label: string().optional(),
32415
- status: _enum([
32416
- "ok",
32417
- "unformatted",
32418
- "error",
32419
- "offline",
32420
- "unknown"
32421
- ]),
32422
- /**
32423
- * Total size in MB, or **null when the camera did not say**.
32424
- *
32425
- * Never 0 for an unreadable value: a measurement that failed is not a
32426
- * measurement (D393), and a card whose size is unknown must not be
32427
- * rendered as a card of size zero.
32428
- */
32429
- capacityMb: number().nullable(),
32430
- /**
32431
- * Free space in MB, or null when unknown.
32432
- *
32433
- * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
32434
- * 1439 both report exactly 11776 MB free — the fixed reserve a looping
32435
- * card converges on once it has wrapped. At loop steady state the
32436
- * number is identical whether the camera recorded yesterday or stopped
32437
- * a month ago.
32438
- */
32439
- freeMb: number().nullable(),
32440
- /** True when the camera reports the volume writable (`property` RW). */
32441
- writable: boolean().optional()
32442
- });
32443
- /**
32444
- * What the camera is doing with its own storage, right now.
32445
- *
32446
- * Every scalar is nullable and **null means the camera did not answer**,
32447
- * never a default. A form that seeds `0` from an unanswered read invites
32448
- * the operator to save that 0 back onto the camera.
32449
- */
32450
- var RecordingOnboardStatusSchema = object({
32451
- storage: discriminatedUnion("kind", [
32452
- object({
32453
- kind: literal("present"),
32454
- volumes: array(OnboardStorageVolumeSchema)
32455
- }),
32456
- object({
32457
- kind: literal("absent"),
32458
- reason: string()
32459
- }),
32460
- object({
32461
- kind: literal("unknown"),
32462
- reason: string()
32463
- })
32464
- ]),
32465
- tracks: array(object({
32466
- id: string(),
32467
- enabled: boolean(),
32468
- isVideo: boolean(),
32469
- /** From the camera's own track description. Null when it does not say. */
32470
- codec: string().nullable(),
32471
- resolution: string().nullable(),
32472
- /** Per-track overwrite flag, where the firmware keeps it per track. */
32473
- overwriteWhenFull: boolean().nullable()
32474
- })),
32475
- /**
32476
- * The track the write path targets — the enabled VIDEO one. Null when
32477
- * no track could be identified, which is itself a refusal reason.
32478
- */
32479
- primaryTrackId: string().nullable(),
32480
- /** Master "record to the card at all" switch. */
32481
- enabled: boolean().nullable(),
32482
- overwriteWhenFull: boolean().nullable(),
32483
- preRecordSec: number().nullable(),
32484
- postRecordSec: number().nullable(),
32485
- /** Length of one recorded file, in minutes. */
32486
- segmentMinutes: number().nullable(),
32487
- /** The primary track's weekly windows, flattened. */
32488
- windows: array(RecordWindowSchema),
32489
- /**
32490
- * How many windows the camera described that CamStack could NOT read —
32491
- * an unrecognised trigger, an unparseable clock, a weekday it does not
32492
- * name.
32493
- *
32494
- * A dropped window is work the reader threw away, and a schedule that
32495
- * silently shows fewer rows than the camera holds is how an operator
32496
- * saves back a schedule shorter than the one they were looking at
32497
- * (D391). Non-zero means the window list is INCOMPLETE and a write
32498
- * that replaces it would delete what was not shown — which is why a
32499
- * provider reporting a non-zero count also reports the schedule as not
32500
- * writable.
32501
- */
32502
- unreadableWindows: number(),
32503
- /**
32504
- * The camera is scheduled to record and has NO usable storage.
32505
- *
32506
- * A first-class fact because it is the fleet's most common silent
32507
- * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
32508
- * to a card that is not there. Neither the schedule nor the storage
32509
- * read says anything wrong on its own; only the pair does.
32510
- */
32511
- recordingToNowhere: boolean(),
32512
- lastFetchedAt: number()
32513
- });
32514
- /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
32515
- var RangeSchema = object({
32516
- min: number(),
32517
- max: number(),
32518
- step: number()
32519
- });
32520
- /**
32521
- * The values a camera actually takes for a numeric field, when they are a SET
32522
- * rather than a range.
32523
- *
32524
- * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
32525
- * (I91DN) on 2026-09-22 by writing each value and reading it back:
32526
- *
32527
- * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
32528
- * camera's "no limit" — `-1` and `4294967295` both land on it);
32529
- * - post-record: `5, 10, 30, 60, 120, 300, 600`.
32530
- *
32531
- * Neither is expressible as a step: the first has a sentinel two billion away
32532
- * from its neighbours, the second doubles and then jumps. A range that tried
32533
- * would forbid values the camera takes AND permit values it silently replaces
32534
- * with 5 — wrong in both directions at once.
32535
- *
32536
- * `sentinel` names the member that is not a duration, so a surface can render
32537
- * "no limit" instead of `2147483647` seconds.
32538
- */
32539
- var AllowedValuesSchema = object({
32540
- values: array(number()).min(1),
32541
- sentinel: object({
32542
- value: number(),
32543
- meaning: _enum(["no-limit", "disabled"])
32544
- }).optional()
32545
- });
32546
- /**
32547
- * Per-field availability on ONE camera.
32548
- *
32549
- * The field exists on every camera — this says whether this one can be
32550
- * read and whether it can be written, and `reason` says why not when
32551
- * either is false. The UI renders the control DISABLED with the reason
32552
- * rather than hiding it, so a limitation is legible instead of looking
32553
- * like a missing feature.
32554
- */
32555
- var OnboardFieldSupportSchema = object({
32556
- readable: boolean(),
32557
- writable: boolean(),
32558
- /** Required whenever `readable` or `writable` is false. */
32559
- reason: string().optional()
32560
- });
32561
- /** What this camera's schedule model can express. */
32562
- var OnboardScheduleSupportSchema = object({
32563
- support: OnboardFieldSupportSchema,
32564
- /**
32565
- * The smallest time step the camera can express, in minutes.
32566
- *
32567
- * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
32568
- * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
32569
- * window whose edges are not a multiple of this is REFUSED rather than
32570
- * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
32571
- * and nothing says so.
32572
- */
32573
- granularityMinutes: number(),
32574
- /** Triggers this camera can record on. A window naming another is refused. */
32575
- triggers: array(RecordTriggerSchema),
32576
- /**
32577
- * False when the camera stores ONE trigger per time range, so two
32578
- * windows overlapping on the same day cannot carry different triggers.
32579
- * True on Reolink, whose mask is per-trigger and independent.
32580
- */
32581
- supportsOverlappingTriggers: boolean()
32582
- });
32583
- var RecordingOnboardOptionsSchema = object({
32584
- enabled: OnboardFieldSupportSchema,
32585
- overwriteWhenFull: OnboardFieldSupportSchema,
32586
- preRecordSec: OnboardFieldSupportSchema,
32587
- preRecordSecRange: RangeSchema.optional(),
32588
- /** Preferred over the range when the camera takes a SET, not a span. */
32589
- preRecordSecAllowed: AllowedValuesSchema.optional(),
32590
- postRecordSec: OnboardFieldSupportSchema,
32591
- postRecordSecRange: RangeSchema.optional(),
32592
- /** Preferred over the range when the camera takes a SET, not a span. */
32593
- postRecordSecAllowed: AllowedValuesSchema.optional(),
32594
- segmentMinutes: OnboardFieldSupportSchema,
32595
- segmentMinutesRange: RangeSchema.optional(),
32596
- /** Preferred over the range when the camera takes a SET, not a span. */
32597
- segmentMinutesAllowed: AllowedValuesSchema.optional(),
32598
- schedule: OnboardScheduleSupportSchema
32599
- });
32600
- /**
32601
- * A partial change. Every field optional.
32602
- *
32603
- * Unlike the other `deviceConfig` caps, a provider here does **NOT**
32604
- * silently ignore a field it cannot support — it refuses, by name,
32605
- * through {@link describeOnboardRefusal}. Silence on a recording setting
32606
- * is the failure D62 exists to prevent: the operator believes the camera
32607
- * is recording the way the form says, and it is not.
32608
- */
32609
- var RecordingOnboardPatchSchema = object({
32610
- enabled: boolean().optional(),
32611
- overwriteWhenFull: boolean().optional(),
32612
- preRecordSec: number().optional(),
32613
- postRecordSec: number().optional(),
32614
- segmentMinutes: number().optional(),
32615
- /** The complete new window set for the primary track — not a delta. */
32616
- windows: array(RecordWindowSchema).optional()
32617
- });
32618
- var recordingOnboardCapability = {
32619
- name: "recording-onboard",
32620
- scope: "device",
32621
- deviceNative: true,
32622
- mode: "singleton",
32623
- deviceTypes: [DeviceType.Camera],
32624
- deviceConfig: { ui: {
32625
- kind: "derived-form",
32626
- builderId: "recording-onboard",
32627
- tab: "recording"
32628
- } },
32629
- methods: {
32630
- getOptions: method(object({ deviceId: number() }), RecordingOnboardOptionsSchema),
32631
- setSettings: method(object({
32632
- deviceId: number(),
32633
- settings: RecordingOnboardPatchSchema
32634
- }), _void(), {
32635
- kind: "mutation",
32636
- auth: "admin"
32637
- })
32638
- },
32639
- status: {
32640
- schema: RecordingOnboardStatusSchema,
32641
- kind: "poll"
32642
- },
32643
- runtimeState: RecordingOnboardStatusSchema,
32644
- /**
32645
- * Runtime-state durability: **restored** — operator-set camera-side
32646
- * recording config; mutation-driven, and the storage half is the last
32647
- * thing the camera said about its own card.
32648
- *
32649
- * See `RuntimeStateDurability`. Enforced by
32650
- * `scripts/check-runtime-state-durability.ts`.
32651
- */
32652
- durability: "restored",
32653
- /** Clock fields: written, but excluded from the compare that decides
32654
- * whether persisting is worth a SQLite commit. */
32655
- volatileStateFields: ["lastFetchedAt"]
32656
- };
32657
- /**
32658
32667
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
32659
32668
  * for every device, regardless of provider — the kernel needs a uniform
32660
32669
  * cap-keyed slice for the basic device flags every consumer expects to
@@ -32873,40 +32882,6 @@ var eventEmitterCapability = {
32873
32882
  */
32874
32883
  durability: "session"
32875
32884
  };
32876
- var EventItemSchema = object({
32877
- id: string(),
32878
- type: string(),
32879
- timestamp: number(),
32880
- label: string().optional(),
32881
- thumbnailUrl: string().optional(),
32882
- clipUrl: string().optional(),
32883
- metadata: record(string(), unknown()).optional()
32884
- });
32885
- var eventsCapability = {
32886
- name: "events",
32887
- scope: "device",
32888
- mode: "singleton",
32889
- deviceTypes: [DeviceType.Camera],
32890
- methods: {
32891
- getEvents: method(object({
32892
- deviceId: number(),
32893
- from: number().optional(),
32894
- to: number().optional(),
32895
- limit: number().optional()
32896
- }), array(EventItemSchema)),
32897
- getEventThumbnail: method(object({
32898
- deviceId: number(),
32899
- eventId: string()
32900
- }), object({
32901
- base64: string(),
32902
- contentType: string()
32903
- }).nullable()),
32904
- getEventClipUrl: method(object({
32905
- deviceId: number(),
32906
- eventId: string()
32907
- }), string().nullable())
32908
- }
32909
- };
32910
32885
  var IdentitySchema = object({
32911
32886
  id: string(),
32912
32887
  name: string(),
@@ -35394,179 +35369,6 @@ var motionTriggerCapability = {
35394
35369
  durability: "session"
35395
35370
  };
35396
35371
  /**
35397
- * camera-grid-layout — the geometry of a COMPOSITE camera, on the camera's own
35398
- * page.
35399
- *
35400
- * ## Why this is a capability and not an addon settings schema
35401
- *
35402
- * It was one, and it did not render. The addon declared the editor as a
35403
- * `type: 'widget'` field inside its own `deviceSettingsSchema()`; the hub
35404
- * returned that section correctly and `ConfigFormField` renders `type:'widget'`
35405
- * perfectly well — and nothing ever asked camera-grid for it. The Cluster →
35406
- * Pipeline → Device Overrides page interrogates a HAND-WRITTEN list of four
35407
- * addons (`PIPELINE_CLUSTER_DEVICE_ADDONS`), whose own comment says an addon
35408
- * not on it "falls off silently".
35409
- *
35410
- * Adding a fifth name to that list would have been the wrong fix twice over:
35411
- * that page is per-camera DETECTION tuning, and a grid's geometry belongs
35412
- * beside PTZ and motion zones on the camera itself. The device page is
35413
- * BINDING-driven (D12), so the way in is a capability bound to the device —
35414
- * and this cap carries its section the way `recording` does, by RETURNING it
35415
- * from `getDeviceSettingsContribution`.
35416
- *
35417
- * Seven other widgets are still declared the other way, through a
35418
- * `deviceConfig.ui` block the framework derives a section from. That route
35419
- * gives the addon no say in where its own panel lands and no way to decline
35420
- * for a device the panel does not suit, which is why this one does not use it.
35421
- *
35422
- * ## Why one addon may implement it
35423
- *
35424
- * It is a device-scoped NATIVE cap, registered by the grid camera device
35425
- * itself. Nothing else declares a composite camera, so nothing else has a
35426
- * layout — and the device-scoped route means the widget asks THE camera, not
35427
- * "the camera-grid addon", which is what let the old custom-action pair be
35428
- * reached only by a caller that already knew the addon id.
35429
- *
35430
- * ## The tab
35431
- *
35432
- * `streaming`, not a top-tab of its own. A grid's geometry IS what its stream
35433
- * is, so the Streaming tab is where it belongs; a `grid` top-tab would need an
35434
- * entry in `WELL_KNOWN_TAB_MAP` or the device page renders the raw id as the
35435
- * label (measured on the robot camera, 2026-09-06 — a tab called "navigation"
35436
- * next to "PTZ").
35437
- */
35438
- /** A rectangle in normalized [0,1] coordinates of whatever contains it. */
35439
- var GridNormalizedRectSchema = object({
35440
- x: number().min(0).max(1),
35441
- y: number().min(0).max(1),
35442
- width: number().gt(0).max(1),
35443
- height: number().gt(0).max(1)
35444
- });
35445
- /**
35446
- * One source camera, the part of its picture taken, and where that part lands.
35447
- *
35448
- * Both rectangles are NORMALIZED (D519): a source camera can change resolution
35449
- * — a profile switch, a firmware update, a substream that comes back different
35450
- * — and a stored PIXEL rectangle would quietly start cutting the wrong region,
35451
- * which is the class of bug nobody files.
35452
- */
35453
- var GridLayoutCellSchema = object({
35454
- deviceId: number().int().positive(),
35455
- /** The part of the SOURCE taken, normalized against the source. */
35456
- source: GridNormalizedRectSchema,
35457
- /** Where it lands, normalized against the CANVAS. */
35458
- cell: GridNormalizedRectSchema
35459
- });
35460
- /**
35461
- * Which profiles this grid can actually compose, and why not.
35462
- *
35463
- * A grid's `high` composes its sources' `high` and its `low` their `low`, so a
35464
- * profile is on offer only when EVERY source can serve it. The refusal NAMES
35465
- * the sources, because "this grid has no low" is not a finding — "615 has no
35466
- * low" is, and it is the one an operator can act on.
35467
- */
35468
- var GridProfileOfferSchema = object({
35469
- profile: _enum([
35470
- "high",
35471
- "mid",
35472
- "low"
35473
- ]),
35474
- offered: boolean(),
35475
- /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
35476
- missingSources: array(number().int().positive()),
35477
- /**
35478
- * The canvas this profile composes onto, `WxH`, or empty when it is not
35479
- * offered. DERIVED from the cells and the sources' own size at this profile —
35480
- * it is reported because nothing else in the system would ever say what the
35481
- * grid came out as, and because it is the number an operator would otherwise
35482
- * expect to type.
35483
- */
35484
- canvas: string(),
35485
- /**
35486
- * Whether this profile is PUBLISHED, of the ones the grid could serve.
35487
- *
35488
- * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
35489
- * a 4K canvas built from 4K decodes — something to opt into, not something a
35490
- * viewer's adaptive should be handed by climbing to the top rung it can see.
35491
- * Default is `mid` + `low`.
35492
- */
35493
- published: boolean()
35494
- });
35495
- var GridLayoutViewSchema = object({
35496
- /** The persisted grid row this camera was declared from. */
35497
- instanceId: string(),
35498
- deviceId: number().int().nonnegative(),
35499
- name: string(),
35500
- /**
35501
- * NO canvas size. A grid's resolution is not authored: each profile derives
35502
- * its own from the cells and its sources' dimensions. The two numbers that
35503
- * used to be here were a text field that silently decided both how much the
35504
- * composite cost and how sharp it was — see `profiles[].canvas` for what it
35505
- * came out as.
35506
- */
35507
- fps: number().int(),
35508
- cells: array(GridLayoutCellSchema),
35509
- /** What the catalog will publish, and what it refuses to. Read-only. */
35510
- profiles: array(GridProfileOfferSchema)
35511
- });
35512
- var GridLayoutPatchSchema = object({
35513
- deviceId: number().int().nonnegative(),
35514
- name: string().min(1).max(160).optional(),
35515
- fps: number().int().min(1).max(60).optional(),
35516
- /** Which profiles to publish. See `GridProfileOffer.published`. */
35517
- publishedProfiles: array(_enum([
35518
- "high",
35519
- "mid",
35520
- "low"
35521
- ])).max(3).optional(),
35522
- /**
35523
- * The whole cell list at once. A per-cell patch would need an ordering the
35524
- * editor does not have, and a half-applied layout is a picture nobody asked
35525
- * for.
35526
- */
35527
- cells: array(GridLayoutCellSchema).max(16)
35528
- });
35529
- var cameraGridLayoutCapability = {
35530
- name: "camera-grid-layout",
35531
- scope: "device",
35532
- deviceNative: true,
35533
- mode: "singleton",
35534
- deviceTypes: [DeviceType.Camera],
35535
- /**
35536
- * The section is built by the ADDON and returned from
35537
- * `getDeviceSettingsContribution`, not derived by the framework from a
35538
- * `deviceConfig.ui` block.
35539
- *
35540
- * Both mechanisms render the same widget. This one hands the addon two
35541
- * things the framework-derived route cannot give it:
35542
- *
35543
- * - it chooses its own section, `tab`, `location` and `order`, the way any
35544
- * other setting does, instead of receiving them from a cap declaration;
35545
- * - it can DECLINE per device. A camera that is not a grid gets no section
35546
- * at all, rather than a widget that renders its own "not a grid" state.
35547
- *
35548
- * `recording` is the precedent (`recorder/recording-device-settings.ts`): it
35549
- * returns `null` for anything that is not a camera, so the Recording tab
35550
- * never appears there.
35551
- */
35552
- exposesDeviceSettings: true,
35553
- methods: {
35554
- /**
35555
- * The grid behind this device.
35556
- *
35557
- * `null` means ANSWERED and this camera is not a grid — not "not yet
35558
- * known". The widget renders its "this is not a grid camera" state only
35559
- * from this answer, never from an unresolved query (D315).
35560
- */
35561
- getLayout: method(object({ deviceId: number().int().nonnegative() }), GridLayoutViewSchema.nullable(), { auth: "admin" }),
35562
- /** Write the geometry back. Returns the grid as it now stands, profiles included. */
35563
- saveLayout: method(GridLayoutPatchSchema, GridLayoutViewSchema, {
35564
- kind: "mutation",
35565
- auth: "admin"
35566
- })
35567
- }
35568
- };
35569
- /**
35570
35372
  * Motion-zones share the same MaskShape vocabulary as privacy-mask — the
35571
35373
  * on-camera motion-detection mask is a single `grid` region (a row-major
35572
35374
  * boolean cell lattice the camera's onboard VMD evaluates). Composing it as
@@ -38236,37 +38038,37 @@ var rebootCapability = {
38236
38038
  auth: "admin"
38237
38039
  }) }
38238
38040
  };
38239
- /**
38240
- * `recording` cap — footage availability + HLS playback manifests + per-device
38241
- * recording config. NOTE on events (source of truth, R5/C3): this cap carries
38242
- * NO event surface — `getPlaybackManifest` returns playlist URLs only. Timeline
38243
- * events (motion/object/audio) come from `pipelineAnalytics` (durable SQLite
38244
- * rows) and are the ONLY event surface — the recorder has none. The in-RAM
38245
- * playback markers it used to build were deleted on 2026-08-29 because nothing
38246
- * ever read them. Event<->footage joins are by time, padded with the shared
38247
- * `EVENT_PAD_MS` (`interfaces/recording-config.ts`).
38248
- */
38249
- var RecordingStatusSchema = object({
38250
- deviceId: number(),
38251
- enabled: boolean(),
38252
- /** THE derived storage mode, from the one definition
38253
- * (`deriveRecordingMode`) — never a second enum. A duplicated list is how
38254
- * `on-device-decision` could have reached the recorder and not the status. */
38255
- activeMode: RecordingStorageModeSchema,
38256
- nodeId: string(),
38257
- storageBytes: number()
38258
- });
38259
38041
  var RecordingRangeSchema = object({
38260
38042
  profile: string(),
38261
38043
  startMs: number(),
38262
38044
  endMs: number()
38263
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"]);
38264
38063
  var RecordingAvailabilitySchema = object({
38265
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,
38266
38068
  ranges: array(RecordingRangeSchema),
38267
38069
  /**
38268
- * Every profile this camera has footage in — not only the one `ranges`
38269
- * describes (D433).
38070
+ * Every profile this camera has footage in AT THIS SOURCE — not only the one
38071
+ * `ranges` describes (D433).
38270
38072
  *
38271
38073
  * `ranges` answers for ONE profile by design: the timeline is a single bar,
38272
38074
  * and enumerating all of them triples the directory reads for a bar that
@@ -38283,15 +38085,344 @@ var RecordingAvailabilitySchema = object({
38283
38085
  });
38284
38086
  var RecordingDaysSchema = object({
38285
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,
38286
38092
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
38287
38093
  days: array(number())
38288
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
+ });
38289
38420
  /**
38290
38421
  * One camera's row in a `getAvailabilityBatch` answer.
38291
38422
  *
38292
- * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
38293
- * batch collapses the transport, not the work — plus the one thing the singular
38294
- * 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):
38295
38426
  *
38296
38427
  * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
38297
38428
  * no footage in the window", which is a real claim.
@@ -38323,22 +38454,6 @@ var RecordingDaysForDeviceSchema = object({
38323
38454
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
38324
38455
  days: array(number()).readonly()
38325
38456
  });
38326
- var RecordingManifestSchema = object({
38327
- deviceId: number(),
38328
- /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
38329
- localMasterPath: string().nullable(),
38330
- /** HTTP(S) URL to the master playlist on the recording node's playback server
38331
- * (the PRIMARY candidate); null when no recording / server. Carries the
38332
- * scoped playback token in its path. */
38333
- playbackUrl: string().nullable(),
38334
- /**
38335
- * Candidate master-playlist URLs the client tries in order (LAN first, then
38336
- * remote — Tailscale/Cloudflare if the operator configured extra hosts), each
38337
- * carrying the same scoped token. `playbackUrl` is the first entry. Empty when
38338
- * there is no recording / server.
38339
- */
38340
- playbackEndpoints: array(string())
38341
- });
38342
38457
  /**
38343
38458
  * Recording storage usage for one camera — what the ARCHIVE holds for it,
38344
38459
  * across every profile and every resolvable location on this node.
@@ -38596,33 +38711,22 @@ var ReadWindowBytesResultSchema = discriminatedUnion("kind", [object({
38596
38711
  * not a retry hint (retrying the same window would refuse again). */
38597
38712
  segmentEndMs: number()
38598
38713
  })]);
38599
- var recordingCapability = {
38600
- name: "recording",
38714
+ var recordingArchiveCapability = {
38715
+ name: "recording-archive",
38601
38716
  scope: "system",
38602
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). */
38603
38721
  exposesDeviceSettings: true,
38604
38722
  status: {
38605
38723
  schema: RecordingStatusSchema,
38606
38724
  kind: "command-driven"
38607
38725
  },
38608
38726
  methods: {
38609
- getAvailability: method(object({
38610
- deviceId: number(),
38611
- fromMs: number(),
38612
- toMs: number(),
38613
- /**
38614
- * Answer for THIS profile instead of the preferred one (D433). Absent
38615
- * keeps the timeline's behaviour — one bar, one profile, one set of
38616
- * reads. `profilesWithFootage` on the answer says what may be asked
38617
- * for.
38618
- */
38619
- profile: string().optional()
38620
- }), RecordingAvailabilitySchema, {
38621
- kind: "query",
38622
- auth: "protected"
38623
- }),
38624
38727
  /**
38625
- * `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.
38626
38730
  *
38627
38731
  * A multi-camera timeline re-asks availability for every camera in the grid
38628
38732
  * on every day change; fanned out that is one request per camera for N
@@ -38630,6 +38734,13 @@ var recordingCapability = {
38630
38734
  * `availabilityProfileFor` + `rangesIn`, run concurrently inside the
38631
38735
  * recorder) — only the transport collapses.
38632
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
+ *
38633
38744
  * `protected` for the same reason the singular method is: every id in
38634
38745
  * `deviceIds` is a device reference, so the F1 #3 gate refuses any camera
38635
38746
  * outside the caller's scope — one id out of scope refuses the CALL, it
@@ -38647,20 +38758,10 @@ var recordingCapability = {
38647
38758
  kind: "query",
38648
38759
  auth: "protected"
38649
38760
  }),
38650
- /** Which calendar days in [fromMs,toMs) have ≥1 recorded segment, bucketed by
38651
- * the client's local day (`tzOffsetMinutes` = minutes to add to UTC). Drives
38652
- * the theater date-picker's day dots. */
38653
- getDaysWithRecordings: method(object({
38654
- deviceId: number(),
38655
- fromMs: number(),
38656
- toMs: number(),
38657
- tzOffsetMinutes: number()
38658
- }), RecordingDaysSchema, {
38659
- kind: "query",
38660
- auth: "protected"
38661
- }),
38662
38761
  /**
38663
- * `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}.
38664
38765
  *
38665
38766
  * The cheapest question in the product, asked once per camera per month
38666
38767
  * change. One directory read per day per camera at the owner, unchanged;
@@ -38679,14 +38780,6 @@ var recordingCapability = {
38679
38780
  kind: "query",
38680
38781
  auth: "protected"
38681
38782
  }),
38682
- getPlaybackManifest: method(object({
38683
- deviceId: number(),
38684
- fromMs: number(),
38685
- toMs: number()
38686
- }), RecordingManifestSchema, {
38687
- kind: "query",
38688
- auth: "protected"
38689
- }),
38690
38783
  getStorageUsage: method(object({}), RecordingStorageUsageSchema, {
38691
38784
  kind: "query",
38692
38785
  auth: "admin"
@@ -38702,6 +38795,10 @@ var recordingCapability = {
38702
38795
  * value (D315, D393, D590). It carries schedules and retention, no secret,
38703
38796
  * and the per-device gate in `scope-access.ts` still applies; `setDeviceConfig`
38704
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.
38705
38802
  */
38706
38803
  getDeviceConfig: method(object({ deviceId: number() }), RecordingConfigSchema, {
38707
38804
  kind: "query",
@@ -38709,7 +38806,13 @@ var recordingCapability = {
38709
38806
  }),
38710
38807
  /** Locate footage at a wall-clock instant: the covering segment's window,
38711
38808
  * or a gap with the forward nearest covered edge. Used by a feeder running
38712
- * 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). */
38713
38816
  locateSegment: method(object({
38714
38817
  deviceId: number(),
38715
38818
  profile: string(),
@@ -39324,6 +39427,346 @@ var recordingExportCapability = {
39324
39427
  }
39325
39428
  };
39326
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
+ /**
39327
39770
  * A camera's own "record me NOW" LEVEL — a signal the device raises while
39328
39771
  * something it knows about is happening (a robot vacuum cleaning, a machine
39329
39772
  * running, a gate open) and lowers when it stops.
@@ -43407,7 +43850,6 @@ var ALL_CAPABILITY_DEFINITIONS = [
43407
43850
  embeddingEncoderCapability,
43408
43851
  enumSensorCapability,
43409
43852
  eventEmitterCapability,
43410
- eventsCapability,
43411
43853
  faceGalleryCapability,
43412
43854
  failureContributionCapability,
43413
43855
  fanControlCapability,
@@ -43468,6 +43910,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
43468
43910
  ptzAutotrackCapability,
43469
43911
  rebootCapability,
43470
43912
  recordingCapability,
43913
+ recordingArchiveCapability,
43471
43914
  recordingExportCapability,
43472
43915
  recordingOnboardCapability,
43473
43916
  recordingSignalCapability,
@@ -45702,24 +46145,6 @@ Object.freeze({
45702
46145
  addonId: null,
45703
46146
  access: "view"
45704
46147
  },
45705
- "events.getEventClipUrl": {
45706
- capName: "events",
45707
- capScope: "device",
45708
- addonId: null,
45709
- access: "view"
45710
- },
45711
- "events.getEvents": {
45712
- capName: "events",
45713
- capScope: "device",
45714
- addonId: null,
45715
- access: "view"
45716
- },
45717
- "events.getEventThumbnail": {
45718
- capName: "events",
45719
- capScope: "device",
45720
- addonId: null,
45721
- access: "view"
45722
- },
45723
46148
  "faceGallery.assignFace": {
45724
46149
  capName: "face-gallery",
45725
46150
  capScope: "system",
@@ -48594,224 +49019,236 @@ Object.freeze({
48594
49019
  addonId: null,
48595
49020
  access: "create"
48596
49021
  },
48597
- "recording.applyDeviceSettingsPatch": {
49022
+ "recording.getAvailability": {
48598
49023
  capName: "recording",
48599
- capScope: "system",
49024
+ capScope: "device",
48600
49025
  addonId: null,
48601
- access: "create"
49026
+ access: "view"
48602
49027
  },
48603
- "recording.cancelRelocateJob": {
49028
+ "recording.getDaysWithRecordings": {
48604
49029
  capName: "recording",
48605
- capScope: "system",
49030
+ capScope: "device",
48606
49031
  addonId: null,
48607
- access: "create"
49032
+ access: "view"
48608
49033
  },
48609
- "recording.cancelStorageMigrationMove": {
49034
+ "recording.getPlayback": {
48610
49035
  capName: "recording",
48611
- capScope: "system",
49036
+ capScope: "device",
48612
49037
  addonId: null,
48613
- access: "create"
49038
+ access: "view"
48614
49039
  },
48615
- "recording.deleteFootprint": {
49040
+ "recording.getPlaybackOptions": {
48616
49041
  capName: "recording",
48617
- capScope: "system",
49042
+ capScope: "device",
48618
49043
  addonId: null,
48619
- access: "delete"
49044
+ access: "view"
48620
49045
  },
48621
- "recording.getAvailability": {
49046
+ "recording.listSources": {
48622
49047
  capName: "recording",
48623
- capScope: "system",
49048
+ capScope: "device",
48624
49049
  addonId: null,
48625
49050
  access: "view"
48626
49051
  },
48627
- "recording.getAvailabilityBatch": {
48628
- capName: "recording",
49052
+ "recordingArchive.applyDeviceSettingsPatch": {
49053
+ capName: "recording-archive",
48629
49054
  capScope: "system",
48630
49055
  addonId: null,
48631
- access: "view"
49056
+ access: "create"
48632
49057
  },
48633
- "recording.getDaysWithRecordings": {
48634
- capName: "recording",
49058
+ "recordingArchive.cancelRelocateJob": {
49059
+ capName: "recording-archive",
48635
49060
  capScope: "system",
48636
49061
  addonId: null,
48637
- access: "view"
49062
+ access: "create"
48638
49063
  },
48639
- "recording.getDaysWithRecordingsBatch": {
48640
- 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",
48641
49078
  capScope: "system",
48642
49079
  addonId: null,
48643
49080
  access: "view"
48644
49081
  },
48645
- "recording.getDeviceConfig": {
48646
- capName: "recording",
49082
+ "recordingArchive.getDaysWithRecordingsBatch": {
49083
+ capName: "recording-archive",
48647
49084
  capScope: "system",
48648
49085
  addonId: null,
48649
49086
  access: "view"
48650
49087
  },
48651
- "recording.getDeviceLiveContribution": {
48652
- capName: "recording",
49088
+ "recordingArchive.getDeviceConfig": {
49089
+ capName: "recording-archive",
48653
49090
  capScope: "system",
48654
49091
  addonId: null,
48655
49092
  access: "view"
48656
49093
  },
48657
- "recording.getDeviceSettingsContribution": {
48658
- capName: "recording",
49094
+ "recordingArchive.getDeviceLiveContribution": {
49095
+ capName: "recording-archive",
48659
49096
  capScope: "system",
48660
49097
  addonId: null,
48661
49098
  access: "view"
48662
49099
  },
48663
- "recording.getPlacement": {
48664
- capName: "recording",
49100
+ "recordingArchive.getDeviceSettingsContribution": {
49101
+ capName: "recording-archive",
48665
49102
  capScope: "system",
48666
49103
  addonId: null,
48667
49104
  access: "view"
48668
49105
  },
48669
- "recording.getPlaybackManifest": {
48670
- capName: "recording",
49106
+ "recordingArchive.getPlacement": {
49107
+ capName: "recording-archive",
48671
49108
  capScope: "system",
48672
49109
  addonId: null,
48673
49110
  access: "view"
48674
49111
  },
48675
- "recording.getRelocateResidue": {
48676
- capName: "recording",
49112
+ "recordingArchive.getRelocateResidue": {
49113
+ capName: "recording-archive",
48677
49114
  capScope: "system",
48678
49115
  addonId: null,
48679
49116
  access: "view"
48680
49117
  },
48681
- "recording.getStatus": {
48682
- capName: "recording",
49118
+ "recordingArchive.getStatus": {
49119
+ capName: "recording-archive",
48683
49120
  capScope: "system",
48684
49121
  addonId: null,
48685
49122
  access: "view"
48686
49123
  },
48687
- "recording.getStorageMigrationMoveStatus": {
48688
- capName: "recording",
49124
+ "recordingArchive.getStorageMigrationMoveStatus": {
49125
+ capName: "recording-archive",
48689
49126
  capScope: "system",
48690
49127
  addonId: null,
48691
49128
  access: "view"
48692
49129
  },
48693
- "recording.getStorageUsage": {
48694
- capName: "recording",
49130
+ "recordingArchive.getStorageUsage": {
49131
+ capName: "recording-archive",
48695
49132
  capScope: "system",
48696
49133
  addonId: null,
48697
49134
  access: "view"
48698
49135
  },
48699
- "recording.listOpsLog": {
48700
- capName: "recording",
49136
+ "recordingArchive.listOpsLog": {
49137
+ capName: "recording-archive",
48701
49138
  capScope: "system",
48702
49139
  addonId: null,
48703
49140
  access: "view"
48704
49141
  },
48705
- "recording.listRelocateJobs": {
48706
- capName: "recording",
49142
+ "recordingArchive.listRelocateJobs": {
49143
+ capName: "recording-archive",
48707
49144
  capScope: "system",
48708
49145
  addonId: null,
48709
49146
  access: "view"
48710
49147
  },
48711
- "recording.locateSegment": {
48712
- capName: "recording",
49148
+ "recordingArchive.locateSegment": {
49149
+ capName: "recording-archive",
48713
49150
  capScope: "system",
48714
49151
  addonId: null,
48715
49152
  access: "view"
48716
49153
  },
48717
- "recording.pauseForStorageMigration": {
48718
- capName: "recording",
49154
+ "recordingArchive.pauseForStorageMigration": {
49155
+ capName: "recording-archive",
48719
49156
  capScope: "system",
48720
49157
  addonId: null,
48721
49158
  access: "create"
48722
49159
  },
48723
- "recording.planStorageRebalance": {
48724
- capName: "recording",
49160
+ "recordingArchive.planStorageRebalance": {
49161
+ capName: "recording-archive",
48725
49162
  capScope: "system",
48726
49163
  addonId: null,
48727
49164
  access: "view"
48728
49165
  },
48729
- "recording.pruneFootage": {
48730
- capName: "recording",
49166
+ "recordingArchive.pruneFootage": {
49167
+ capName: "recording-archive",
48731
49168
  capScope: "system",
48732
49169
  addonId: null,
48733
49170
  access: "create"
48734
49171
  },
48735
- "recording.readGopBytes": {
48736
- capName: "recording",
49172
+ "recordingArchive.readGopBytes": {
49173
+ capName: "recording-archive",
48737
49174
  capScope: "system",
48738
49175
  addonId: null,
48739
49176
  access: "view"
48740
49177
  },
48741
- "recording.readSegmentBytes": {
48742
- capName: "recording",
49178
+ "recordingArchive.readSegmentBytes": {
49179
+ capName: "recording-archive",
48743
49180
  capScope: "system",
48744
49181
  addonId: null,
48745
49182
  access: "view"
48746
49183
  },
48747
- "recording.readWindowBytes": {
48748
- capName: "recording",
49184
+ "recordingArchive.readWindowBytes": {
49185
+ capName: "recording-archive",
48749
49186
  capScope: "system",
48750
49187
  addonId: null,
48751
49188
  access: "view"
48752
49189
  },
48753
- "recording.reconcileLedgerAgainstDisk": {
48754
- capName: "recording",
49190
+ "recordingArchive.reconcileLedgerAgainstDisk": {
49191
+ capName: "recording-archive",
48755
49192
  capScope: "system",
48756
49193
  addonId: null,
48757
49194
  access: "create"
48758
49195
  },
48759
- "recording.refreshStorageLocationsForMigration": {
48760
- capName: "recording",
49196
+ "recordingArchive.refreshStorageLocationsForMigration": {
49197
+ capName: "recording-archive",
48761
49198
  capScope: "system",
48762
49199
  addonId: null,
48763
49200
  access: "create"
48764
49201
  },
48765
- "recording.relocateFootage": {
48766
- capName: "recording",
49202
+ "recordingArchive.relocateFootage": {
49203
+ capName: "recording-archive",
48767
49204
  capScope: "system",
48768
49205
  addonId: null,
48769
49206
  access: "create"
48770
49207
  },
48771
- "recording.renderClip": {
48772
- capName: "recording",
49208
+ "recordingArchive.renderClip": {
49209
+ capName: "recording-archive",
48773
49210
  capScope: "system",
48774
49211
  addonId: null,
48775
49212
  access: "create"
48776
49213
  },
48777
- "recording.renderGif": {
48778
- capName: "recording",
49214
+ "recordingArchive.renderGif": {
49215
+ capName: "recording-archive",
48779
49216
  capScope: "system",
48780
49217
  addonId: null,
48781
49218
  access: "create"
48782
49219
  },
48783
- "recording.rescanStorage": {
48784
- capName: "recording",
49220
+ "recordingArchive.rescanStorage": {
49221
+ capName: "recording-archive",
48785
49222
  capScope: "system",
48786
49223
  addonId: null,
48787
49224
  access: "create"
48788
49225
  },
48789
- "recording.resumeForStorageMigration": {
48790
- capName: "recording",
49226
+ "recordingArchive.resumeForStorageMigration": {
49227
+ capName: "recording-archive",
48791
49228
  capScope: "system",
48792
49229
  addonId: null,
48793
49230
  access: "create"
48794
49231
  },
48795
- "recording.setDeviceConfig": {
48796
- capName: "recording",
49232
+ "recordingArchive.setDeviceConfig": {
49233
+ capName: "recording-archive",
48797
49234
  capScope: "system",
48798
49235
  addonId: null,
48799
49236
  access: "create"
48800
49237
  },
48801
- "recording.setDevicePlacement": {
48802
- capName: "recording",
49238
+ "recordingArchive.setDevicePlacement": {
49239
+ capName: "recording-archive",
48803
49240
  capScope: "system",
48804
49241
  addonId: null,
48805
49242
  access: "create"
48806
49243
  },
48807
- "recording.startStorageMigrationMove": {
48808
- capName: "recording",
49244
+ "recordingArchive.startStorageMigrationMove": {
49245
+ capName: "recording-archive",
48809
49246
  capScope: "system",
48810
49247
  addonId: null,
48811
49248
  access: "create"
48812
49249
  },
48813
- "recording.startStorageRebalance": {
48814
- capName: "recording",
49250
+ "recordingArchive.startStorageRebalance": {
49251
+ capName: "recording-archive",
48815
49252
  capScope: "system",
48816
49253
  addonId: null,
48817
49254
  access: "create"
@@ -50340,6 +50777,12 @@ Object.freeze({
50340
50777
  addonId: null,
50341
50778
  access: "view"
50342
50779
  },
50780
+ "videoclips.getPlaybackOptions": {
50781
+ capName: "videoclips",
50782
+ capScope: "device",
50783
+ addonId: null,
50784
+ access: "view"
50785
+ },
50343
50786
  "videoclips.listClips": {
50344
50787
  capName: "videoclips",
50345
50788
  capScope: "device",
@@ -50352,6 +50795,12 @@ Object.freeze({
50352
50795
  addonId: null,
50353
50796
  access: "view"
50354
50797
  },
50798
+ "videoclips.offerClipBytes": {
50799
+ capName: "videoclips",
50800
+ capScope: "device",
50801
+ addonId: null,
50802
+ access: "view"
50803
+ },
50355
50804
  "videoclips.readClipBytes": {
50356
50805
  capName: "videoclips",
50357
50806
  capScope: "device",
@@ -51104,21 +51553,6 @@ Object.freeze({
51104
51553
  form: "single",
51105
51554
  optional: false
51106
51555
  }],
51107
- "events.getEventClipUrl": [{
51108
- name: "deviceId",
51109
- form: "single",
51110
- optional: false
51111
- }],
51112
- "events.getEvents": [{
51113
- name: "deviceId",
51114
- form: "single",
51115
- optional: false
51116
- }],
51117
- "events.getEventThumbnail": [{
51118
- name: "deviceId",
51119
- form: "single",
51120
- optional: false
51121
- }],
51122
51556
  "faceGallery.getFaceByTrack": [{
51123
51557
  name: "deviceId",
51124
51558
  form: "single",
@@ -52037,107 +52471,117 @@ Object.freeze({
52037
52471
  form: "single",
52038
52472
  optional: false
52039
52473
  }],
52040
- "recording.deleteFootprint": [{
52474
+ "recording.getAvailability": [{
52041
52475
  name: "deviceId",
52042
52476
  form: "single",
52043
52477
  optional: false
52044
52478
  }],
52045
- "recording.getAvailability": [{
52479
+ "recording.getDaysWithRecordings": [{
52046
52480
  name: "deviceId",
52047
52481
  form: "single",
52048
52482
  optional: false
52049
52483
  }],
52050
- "recording.getAvailabilityBatch": [{
52051
- name: "deviceIds",
52052
- form: "array",
52484
+ "recording.getPlayback": [{
52485
+ name: "deviceId",
52486
+ form: "single",
52053
52487
  optional: false
52054
52488
  }],
52055
- "recording.getDaysWithRecordings": [{
52489
+ "recording.getPlaybackOptions": [{
52056
52490
  name: "deviceId",
52057
52491
  form: "single",
52058
52492
  optional: false
52059
52493
  }],
52060
- "recording.getDaysWithRecordingsBatch": [{
52061
- name: "deviceIds",
52062
- form: "array",
52494
+ "recording.listSources": [{
52495
+ name: "deviceId",
52496
+ form: "single",
52063
52497
  optional: false
52064
52498
  }],
52065
- "recording.getDeviceConfig": [{
52499
+ "recordingArchive.deleteFootprint": [{
52066
52500
  name: "deviceId",
52067
52501
  form: "single",
52068
52502
  optional: false
52069
52503
  }],
52070
- "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": [{
52071
52515
  name: "deviceId",
52072
52516
  form: "single",
52073
52517
  optional: false
52074
52518
  }],
52075
- "recording.listOpsLog": [{
52519
+ "recordingArchive.listOpsLog": [{
52076
52520
  name: "deviceId",
52077
52521
  form: "single",
52078
52522
  optional: true
52079
52523
  }],
52080
- "recording.locateSegment": [{
52524
+ "recordingArchive.locateSegment": [{
52081
52525
  name: "deviceId",
52082
52526
  form: "single",
52083
52527
  optional: false
52084
52528
  }],
52085
- "recording.pruneFootage": [{
52529
+ "recordingArchive.pruneFootage": [{
52086
52530
  name: "deviceId",
52087
52531
  form: "single",
52088
52532
  optional: false
52089
52533
  }],
52090
- "recording.readGopBytes": [{
52534
+ "recordingArchive.readGopBytes": [{
52091
52535
  name: "deviceId",
52092
52536
  form: "single",
52093
52537
  optional: false
52094
52538
  }],
52095
- "recording.readSegmentBytes": [{
52539
+ "recordingArchive.readSegmentBytes": [{
52096
52540
  name: "deviceId",
52097
52541
  form: "single",
52098
52542
  optional: false
52099
52543
  }],
52100
- "recording.readWindowBytes": [{
52544
+ "recordingArchive.readWindowBytes": [{
52101
52545
  name: "deviceId",
52102
52546
  form: "single",
52103
52547
  optional: false
52104
52548
  }],
52105
- "recording.reconcileLedgerAgainstDisk": [{
52549
+ "recordingArchive.reconcileLedgerAgainstDisk": [{
52106
52550
  name: "deviceId",
52107
52551
  form: "single",
52108
52552
  optional: true
52109
52553
  }],
52110
- "recording.relocateFootage": [{
52554
+ "recordingArchive.relocateFootage": [{
52111
52555
  name: "deviceId",
52112
52556
  form: "single",
52113
52557
  optional: true
52114
52558
  }],
52115
- "recording.renderClip": [{
52559
+ "recordingArchive.renderClip": [{
52116
52560
  name: "deviceId",
52117
52561
  form: "single",
52118
52562
  optional: false
52119
52563
  }],
52120
- "recording.renderGif": [{
52564
+ "recordingArchive.renderGif": [{
52121
52565
  name: "deviceId",
52122
52566
  form: "single",
52123
52567
  optional: false
52124
52568
  }],
52125
- "recording.rescanStorage": [{
52569
+ "recordingArchive.rescanStorage": [{
52126
52570
  name: "deviceId",
52127
52571
  form: "single",
52128
52572
  optional: false
52129
52573
  }],
52130
- "recording.setDeviceConfig": [{
52574
+ "recordingArchive.setDeviceConfig": [{
52131
52575
  name: "deviceId",
52132
52576
  form: "single",
52133
52577
  optional: false
52134
52578
  }],
52135
- "recording.setDevicePlacement": [{
52579
+ "recordingArchive.setDevicePlacement": [{
52136
52580
  name: "deviceId",
52137
52581
  form: "single",
52138
52582
  optional: false
52139
52583
  }],
52140
- "recording.startStorageMigrationMove": [{
52584
+ "recordingArchive.startStorageMigrationMove": [{
52141
52585
  name: "deviceId",
52142
52586
  form: "single",
52143
52587
  optional: true
@@ -52398,6 +52842,11 @@ Object.freeze({
52398
52842
  form: "single",
52399
52843
  optional: false
52400
52844
  }],
52845
+ "videoclips.getPlaybackOptions": [{
52846
+ name: "deviceId",
52847
+ form: "single",
52848
+ optional: false
52849
+ }],
52401
52850
  "videoclips.listClips": [{
52402
52851
  name: "deviceId",
52403
52852
  form: "single",
@@ -52408,6 +52857,11 @@ Object.freeze({
52408
52857
  form: "single",
52409
52858
  optional: false
52410
52859
  }],
52860
+ "videoclips.offerClipBytes": [{
52861
+ name: "deviceId",
52862
+ form: "single",
52863
+ optional: false
52864
+ }],
52411
52865
  "videoclips.readClipBytes": [{
52412
52866
  name: "deviceId",
52413
52867
  form: "single",