@camstack/addon-osd-manager 0.1.132 → 0.1.134

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (18) hide show
  1. package/dist/{MotionZonesSettings-BVKF49rs.mjs → MotionZonesSettings-DjSGOYEZ.mjs} +2 -2
  2. package/dist/{PrivacyMaskSettings-DoIRAuP_.mjs → PrivacyMaskSettings-BrJjxUTV.mjs} +4 -4
  3. package/dist/{SceneMonitorEditor-3ApQNn3a.mjs → SceneMonitorEditor-lFwk-5Ag.mjs} +3 -3
  4. package/dist/_stub.js +939 -889
  5. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-mIgOkpXI.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-D98-RlNY.mjs} +3 -3
  6. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-MNSRdKcG.mjs +26 -0
  7. package/dist/{hostInit-vbrbPLQg.mjs → hostInit-C4Y_aRuZ.mjs} +2 -2
  8. package/dist/index.js +1515 -1004
  9. package/dist/index.mjs +1515 -1004
  10. package/dist/{player-overlays-BPpRI9aB.mjs → player-overlays-8DJJmri3.mjs} +1 -1
  11. package/dist/remoteEntry.js +1 -1
  12. package/dist/{responsive-BEcoyxvC.mjs → responsive-BJTLnN-p.mjs} +1 -1
  13. package/dist/{scene-monitor-copy-1LhlrkfK.mjs → scene-monitor-copy-CXXU8X6V.mjs} +1 -1
  14. package/dist/{square-Bw9o8y6p.mjs → square-CxBZyzy5.mjs} +1 -1
  15. package/dist/{trash-2-ClX9SCS6.mjs → trash-2-CDkpE8nN.mjs} +1 -1
  16. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-BNiwvK5v.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-D5wQZ-Cc.mjs} +1 -1
  17. package/package.json +1 -1
  18. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D47POQ8_.mjs +0 -26
package/dist/index.mjs CHANGED
@@ -5359,7 +5359,7 @@ var ZodIssueCode = {
5359
5359
  var ZodFirstPartyTypeKind;
5360
5360
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5361
5361
  //#endregion
5362
- //#region ../types/dist/sleep-COWaSCAi.mjs
5362
+ //#region ../types/dist/sleep-pM_J7YnY.mjs
5363
5363
  /**
5364
5364
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5365
5365
  * window to float samples (D455).
@@ -6495,6 +6495,24 @@ function normalizeAddonInitResult(result) {
6495
6495
  if (Array.isArray(result)) return { providers: result };
6496
6496
  return result;
6497
6497
  }
6498
+ /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
6499
+ var PeerBytesTicketSchema = object({
6500
+ /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
6501
+ url: string().min(1),
6502
+ /**
6503
+ * The HOST node this URL means something on — the hub or a named agent,
6504
+ * never a runner. {@link AddonPeerBytes.open} compares it to its own and
6505
+ * refuses `cross-node` by name when they differ, without dialling.
6506
+ */
6507
+ hostNodeId: string().min(1),
6508
+ expiresAtMs: number().int().nonnegative(),
6509
+ /**
6510
+ * What the producer DECLARED the body to be, when it knows — `null` when it
6511
+ * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
6512
+ * must be able to tell "the producer did not say" from "the body is empty".
6513
+ */
6514
+ declaredBytes: number().int().nonnegative().nullable()
6515
+ });
6498
6516
  /** Shared Zod schemas used across streaming capabilities. */
6499
6517
  var CamProfileSchema = _enum([
6500
6518
  "high",
@@ -7625,24 +7643,6 @@ object({
7625
7643
  unreachable: number()
7626
7644
  })
7627
7645
  });
7628
- /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
7629
- var PeerBytesTicketSchema = object({
7630
- /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
7631
- url: string().min(1),
7632
- /**
7633
- * The HOST node this URL means something on — the hub or a named agent,
7634
- * never a runner. {@link AddonPeerBytes.open} compares it to its own and
7635
- * refuses `cross-node` by name when they differ, without dialling.
7636
- */
7637
- hostNodeId: string().min(1),
7638
- expiresAtMs: number().int().nonnegative(),
7639
- /**
7640
- * What the producer DECLARED the body to be, when it knows — `null` when it
7641
- * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
7642
- * must be able to tell "the producer did not say" from "the body is empty".
7643
- */
7644
- declaredBytes: number().int().nonnegative().nullable()
7645
- });
7646
7646
  /**
7647
7647
  * Adoption job — the background form of `device-adoption.adopt`.
7648
7648
  *
@@ -7754,7 +7754,7 @@ var AdoptionJobSchema = object({
7754
7754
  * component's original options — detection to the detection-pipeline wrapper
7755
7755
  * binding, audio analysis to its own, recording to `RecordingConfig.enabled`
7756
7756
  * (which was always first-class; the switch was a veneer over
7757
- * `recording.setDeviceConfig`), notifications to a notification-center
7757
+ * `recordingArchive.setDeviceConfig`), notifications to a notification-center
7758
7758
  * per-device setting, the two camera planes to their own components.
7759
7759
  *
7760
7760
  * What survives is {@link composeSwitchedOff}: `CameraStatus.switchedOff`, the
@@ -7780,7 +7780,7 @@ var AdoptionJobSchema = object({
7780
7780
  * | `stream-broker` | `deviceManager.setDisabled` | `StreamBrokerManager.reconcileAllCatalogs` releases the brokers; `ensureBroker` refuses re-creation |
7781
7781
  * | `object-detection` | `deviceManager.setWrapperActive('detection-pipeline')` | `PipelineSettingsStore.resolvePipelineForDevice` returns `{ steps: [], audio: null }` |
7782
7782
  * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
7783
- * | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
7783
+ * | `recording` | `recordingArchive.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
7784
7784
  * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
7785
7785
  * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
7786
7786
  * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
@@ -12100,87 +12100,6 @@ var cameraPipelineConfigCapability = {
12100
12100
  exposesDeviceSettings: true,
12101
12101
  methods: {}
12102
12102
  };
12103
- /**
12104
- * The signals a device can emit to WAKE its own stream.
12105
- *
12106
- * A camera whose stream is built on demand sleeps until something asks for it,
12107
- * and "something" cannot be a consumer that is merely attached — a Frigate-style
12108
- * puller holds a session open for ever, and treating that as demand would keep
12109
- * a battery camera awake for ever, which is the whole thing the battery is for
12110
- * (D173). So the wake has to come from the CAMERA: an event it noticed by
12111
- * itself, with no stream running.
12112
- *
12113
- * ## The vocabulary is the PROVIDER'S, not ours
12114
- *
12115
- * Like `consumables`, this cap declares no vocabulary of its own. A provider
12116
- * names each signal with a `code` it chooses and a `label` an operator reads.
12117
- * Reolink offers motion and camera-native detection; another provider may offer
12118
- * a tamper, a doorbell press, a PIR, or something no camera in this fleet has
12119
- * yet. A fixed enum here would mean every new signal is a framework release.
12120
- *
12121
- * It is deliberately NOT derived from the caps a device already binds. Whether
12122
- * a camera CAN push firmware motion is expressed by `motionSources` containing
12123
- * `'onboard'`, and whether it does AI on-camera by the `native-object-detection`
12124
- * binding — but both answer "what drives the detection pipeline", which is a
12125
- * different question from "what may wake a sleeping stream". A camera can do
12126
- * the first and not be trusted with the second, and the operator picks per
12127
- * camera. Two questions, two authorities.
12128
- *
12129
- * ## Availability is not permission
12130
- *
12131
- * `listSignals` says what the device CAN emit. Whether a given signal actually
12132
- * wakes the stream is the operator's per-camera choice, held by the broker
12133
- * alongside the cooldown — see the stream-broker cap's wake settings. A
12134
- * provider declaring a signal is not a provider enabling it.
12135
- */
12136
- /** One signal a device can emit. */
12137
- var StreamSignalSchema = object({
12138
- /** Stable id chosen by the provider, e.g. `'motion'`, `'person'`, `'tamper'`. */
12139
- code: string().min(1),
12140
- /** What an operator reads in the picker. The provider's own wording. */
12141
- label: string().min(1),
12142
- /**
12143
- * Whether the provider recommends this signal ON when a camera is first set
12144
- * up. A provider knows which of its signals are cheap and reliable; an
12145
- * operator should not have to discover that by trial. Reolink recommends
12146
- * both of its own.
12147
- */
12148
- recommended: boolean()
12149
- });
12150
- var StreamSignalsStatusSchema = object({
12151
- signals: array(StreamSignalSchema),
12152
- lastFetchedAt: number()
12153
- });
12154
- var streamSignalsCapability = {
12155
- name: "stream-signals",
12156
- scope: "device",
12157
- deviceNative: true,
12158
- mode: "singleton",
12159
- deviceTypes: Object.values(DeviceType),
12160
- runtimeState: StreamSignalsStatusSchema,
12161
- /**
12162
- * Runtime-state durability: **session** — mirrored in RAM, never written.
12163
- *
12164
- * The slice holds what the DEVICE says it can emit. That is a probed fact,
12165
- * not an operator choice: the provider re-declares it on every registration,
12166
- * so losing it loses nothing and persisting it would freeze an answer the
12167
- * camera is entitled to change. Measured the same day on the sibling case —
12168
- * `native-object-detection.supportedClasses` was persisted, and a firmware
12169
- * class the camera really detected stayed missing for the life of the row
12170
- * because the fix could not reach it.
12171
- *
12172
- * See `RuntimeStateDurability`. Enforced by
12173
- * `scripts/check-runtime-state-durability.ts`.
12174
- */
12175
- durability: "session",
12176
- methods: {
12177
- /**
12178
- * What this device can emit. Empty is a valid and common answer — most
12179
- * cameras have nothing to offer here, and an empty list is what makes the
12180
- * broker's picker show nothing rather than a false choice.
12181
- */
12182
- listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
12183
- };
12184
12103
  /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
12185
12104
  var StreamFormatSchema = _enum([
12186
12105
  "webrtc",
@@ -14305,6 +14224,118 @@ var detectionPipelineCapability = {
14305
14224
  methods: {}
14306
14225
  };
14307
14226
  /**
14227
+ * device-admin-link — "this device has a management page of its own, and here
14228
+ * is its address".
14229
+ *
14230
+ * ## Why this is not a `deviceConfig` cap
14231
+ *
14232
+ * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
14233
+ * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
14234
+ * patch back through a setter; it costs a `builderId` reducer in
14235
+ * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
14236
+ * renders a form section. This cap answers ONE question with ONE read and
14237
+ * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
14238
+ * block, no `settings`, no `runtimeState` and no reducer — exactly like
14239
+ * `reboot`, the other pure-RPC device-native cap.
14240
+ *
14241
+ * ## Absent, and the difference between "no page" and "we cannot say"
14242
+ *
14243
+ * The two are answered at DIFFERENT layers, on purpose:
14244
+ *
14245
+ * - **"We cannot say"** → the provider never registers the cap for that
14246
+ * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
14247
+ * fan are reached only through a vendor cloud; there is no address to hand
14248
+ * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
14249
+ * conditioner DO have a LAN IP, and still have no HTTP management page
14250
+ * behind it. None of them register, so `deviceManager.getBindings` never
14251
+ * lists the cap and no surface asks.
14252
+ * - **"This device has no page, and I know that"** → the provider registers
14253
+ * and `getAdminLink` returns `null`. This is the answer for a device whose
14254
+ * sibling DOES have a page: a Reolink battery camera reached over UDP by
14255
+ * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
14256
+ * transport, a Home Assistant broker authenticated by supervisor token
14257
+ * (which carries no `baseUrl` at all).
14258
+ *
14259
+ * Both draw NOTHING. A button that opens a browser error is worse than no
14260
+ * button, and D62 is the same rule from the other side: an off switch is
14261
+ * reported off, never made to look broken. There is no third state where the
14262
+ * UI renders a disabled button "because the device might have a page".
14263
+ *
14264
+ * ## The URL never carries credentials
14265
+ *
14266
+ * Not in userinfo, not in a query string. Every provider builds through
14267
+ * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
14268
+ * scheme and path as separate arguments — there is no parameter a secret could
14269
+ * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
14270
+ * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
14271
+ * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
14272
+ * keeps providers from hand-rolling one anyway.
14273
+ *
14274
+ * This matters here more than anywhere else in the repo, because every provider
14275
+ * that knows a device's host knows its PASSWORD too: `{ host, port, username,
14276
+ * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
14277
+ * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
14278
+ * camera's own page will ask for its own login. That is correct, and pre-
14279
+ * filling it is the operator's business, not ours.
14280
+ *
14281
+ * ## It is a LAN fact
14282
+ *
14283
+ * The URL addresses the device where the NODE can see it. It is not proxied,
14284
+ * not made reachable from outside, and not sent anywhere. A surface renders it
14285
+ * as a link the operator's own browser follows, on the operator's own network,
14286
+ * or renders nothing.
14287
+ */
14288
+ /**
14289
+ * Whose page is it. The distinction is for the OPERATOR, who needs to know
14290
+ * before clicking whether he is about to land on a camera's own web server or
14291
+ * inside Home Assistant.
14292
+ */
14293
+ var AdminLinkTargetEnum = _enum(["device", "integration"]);
14294
+ var DeviceAdminLinkSchema = object({
14295
+ /**
14296
+ * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
14297
+ * free of userinfo and of any credential-shaped query key.
14298
+ */
14299
+ url: string(),
14300
+ /**
14301
+ * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
14302
+ * The PROVIDER names it, because only the provider knows what the page is;
14303
+ * a UI that invented the label from the addon id would call the Home
14304
+ * Assistant device page "Provider Homeassistant".
14305
+ */
14306
+ label: string(),
14307
+ target: AdminLinkTargetEnum,
14308
+ /**
14309
+ * Host the URL points at, without scheme, port or path — for the tooltip, so
14310
+ * an operator can see WHERE the button goes before he follows it. Redundant
14311
+ * with `url` by construction; carried separately so no surface has to parse
14312
+ * a URL to show it.
14313
+ */
14314
+ host: string()
14315
+ });
14316
+ var deviceAdminLinkCapability = {
14317
+ name: "device-admin-link",
14318
+ scope: "device",
14319
+ deviceNative: true,
14320
+ mode: "singleton",
14321
+ methods: {
14322
+ /**
14323
+ * The device's management page, or `null` when this device has none.
14324
+ *
14325
+ * `auth: 'admin'` deliberately. This is administration, not actuation —
14326
+ * the same bucket as `reboot` and `camera-credentials`, and explicitly NOT
14327
+ * the actuation set `scripts/check-actuation-not-admin.ts` protects (D403).
14328
+ * The URL is also a statement about the LAN, which a household member with
14329
+ * a `view` grant on a light has no reason to be handed.
14330
+ *
14331
+ * The surfaces gate on the QUERY, never on a role they guessed: a caller
14332
+ * without the right loses the query and draws nothing, which is the same
14333
+ * thing a device with no page draws. There is no path on which a button
14334
+ * appears and then fails — the D403 failure mode, from the other end.
14335
+ */
14336
+ getAdminLink: method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
14337
+ };
14338
+ /**
14308
14339
  * Identity envelope for a device's upstream-system metadata.
14309
14340
  *
14310
14341
  * Two jobs:
@@ -14696,118 +14727,6 @@ var deviceAdoptionCapability = {
14696
14727
  }
14697
14728
  };
14698
14729
  /**
14699
- * device-admin-link — "this device has a management page of its own, and here
14700
- * is its address".
14701
- *
14702
- * ## Why this is not a `deviceConfig` cap
14703
- *
14704
- * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
14705
- * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
14706
- * patch back through a setter; it costs a `builderId` reducer in
14707
- * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
14708
- * renders a form section. This cap answers ONE question with ONE read and
14709
- * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
14710
- * block, no `settings`, no `runtimeState` and no reducer — exactly like
14711
- * `reboot`, the other pure-RPC device-native cap.
14712
- *
14713
- * ## Absent, and the difference between "no page" and "we cannot say"
14714
- *
14715
- * The two are answered at DIFFERENT layers, on purpose:
14716
- *
14717
- * - **"We cannot say"** → the provider never registers the cap for that
14718
- * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
14719
- * fan are reached only through a vendor cloud; there is no address to hand
14720
- * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
14721
- * conditioner DO have a LAN IP, and still have no HTTP management page
14722
- * behind it. None of them register, so `deviceManager.getBindings` never
14723
- * lists the cap and no surface asks.
14724
- * - **"This device has no page, and I know that"** → the provider registers
14725
- * and `getAdminLink` returns `null`. This is the answer for a device whose
14726
- * sibling DOES have a page: a Reolink battery camera reached over UDP by
14727
- * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
14728
- * transport, a Home Assistant broker authenticated by supervisor token
14729
- * (which carries no `baseUrl` at all).
14730
- *
14731
- * Both draw NOTHING. A button that opens a browser error is worse than no
14732
- * button, and D62 is the same rule from the other side: an off switch is
14733
- * reported off, never made to look broken. There is no third state where the
14734
- * UI renders a disabled button "because the device might have a page".
14735
- *
14736
- * ## The URL never carries credentials
14737
- *
14738
- * Not in userinfo, not in a query string. Every provider builds through
14739
- * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
14740
- * scheme and path as separate arguments — there is no parameter a secret could
14741
- * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
14742
- * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
14743
- * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
14744
- * keeps providers from hand-rolling one anyway.
14745
- *
14746
- * This matters here more than anywhere else in the repo, because every provider
14747
- * that knows a device's host knows its PASSWORD too: `{ host, port, username,
14748
- * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
14749
- * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
14750
- * camera's own page will ask for its own login. That is correct, and pre-
14751
- * filling it is the operator's business, not ours.
14752
- *
14753
- * ## It is a LAN fact
14754
- *
14755
- * The URL addresses the device where the NODE can see it. It is not proxied,
14756
- * not made reachable from outside, and not sent anywhere. A surface renders it
14757
- * as a link the operator's own browser follows, on the operator's own network,
14758
- * or renders nothing.
14759
- */
14760
- /**
14761
- * Whose page is it. The distinction is for the OPERATOR, who needs to know
14762
- * before clicking whether he is about to land on a camera's own web server or
14763
- * inside Home Assistant.
14764
- */
14765
- var AdminLinkTargetEnum = _enum(["device", "integration"]);
14766
- var DeviceAdminLinkSchema = object({
14767
- /**
14768
- * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
14769
- * free of userinfo and of any credential-shaped query key.
14770
- */
14771
- url: string(),
14772
- /**
14773
- * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
14774
- * The PROVIDER names it, because only the provider knows what the page is;
14775
- * a UI that invented the label from the addon id would call the Home
14776
- * Assistant device page "Provider Homeassistant".
14777
- */
14778
- label: string(),
14779
- target: AdminLinkTargetEnum,
14780
- /**
14781
- * Host the URL points at, without scheme, port or path — for the tooltip, so
14782
- * an operator can see WHERE the button goes before he follows it. Redundant
14783
- * with `url` by construction; carried separately so no surface has to parse
14784
- * a URL to show it.
14785
- */
14786
- host: string()
14787
- });
14788
- var deviceAdminLinkCapability = {
14789
- name: "device-admin-link",
14790
- scope: "device",
14791
- deviceNative: true,
14792
- mode: "singleton",
14793
- methods: {
14794
- /**
14795
- * The device's management page, or `null` when this device has none.
14796
- *
14797
- * `auth: 'admin'` deliberately. This is administration, not actuation —
14798
- * the same bucket as `reboot` and `camera-credentials`, and explicitly NOT
14799
- * the actuation set `scripts/check-actuation-not-admin.ts` protects (D403).
14800
- * The URL is also a statement about the LAN, which a household member with
14801
- * a `view` grant on a light has no reason to be handed.
14802
- *
14803
- * The surfaces gate on the QUERY, never on a role they guessed: a caller
14804
- * without the right loses the query and draws nothing, which is the same
14805
- * thing a device with no page draws. There is no path on which a button
14806
- * appears and then fails — the D403 failure mode, from the other end.
14807
- */
14808
- getAdminLink: method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
14809
- };
14810
- /**
14811
14730
  * `device-export` — collection cap for addons that export camstack
14812
14731
  * devices to external ecosystems (HomeAssistant via MQTT discovery,
14813
14732
  * HomeKit/HAP, Alexa Smart Home, …).
@@ -19084,6 +19003,7 @@ sub("presence", "sensor", "sensor", "#22c55e", "presence", "Presence");
19084
19003
  sub("enum-sensor", "sensor", "sensor", TAXONOMY_COLORS.sensor, "generic", "Sensor state");
19085
19004
  sub("device-event", "sensor", "sensor", "#10b981", "button", "Device event");
19086
19005
  sub("lock", "control", "control", "#0ea5e9", "lock", "Lock");
19006
+ sub("cover", "control", "control", "#0ea5e9", "door", "Cover");
19087
19007
  sub("switch", "control", "control", TAXONOMY_COLORS.control, "switch", "Switch");
19088
19008
  sub("siren", "control", "control", "#dc2626", "siren", "Siren");
19089
19009
  sub("button", "control", "control", "#10b981", "button", "Button");
@@ -19402,6 +19322,10 @@ var NcSystemEventKindSchema = _enum([
19402
19322
  "alarm-disarmed",
19403
19323
  "alarm-arming",
19404
19324
  "alarm-arm-refused",
19325
+ "alarm-pending",
19326
+ "alarm-rearmed",
19327
+ "alarm-sensor-bypass",
19328
+ "alarm-check-failed",
19405
19329
  "addon-updated",
19406
19330
  "server-updated",
19407
19331
  "export-completed",
@@ -19997,6 +19921,19 @@ var NcMediaFrameSchema = _enum([
19997
19921
  "full",
19998
19922
  "boxed"
19999
19923
  ]);
19924
+ /**
19925
+ * How a SENSOR notification shows the cameras that link the sensor.
19926
+ *
19927
+ * `mosaic` — one image composed of every linked camera's photograph, taken at
19928
+ * the trigger. `each` — one image per camera where the target takes several
19929
+ * attachments; a target that takes one keeps the first camera's (the degrade
19930
+ * engine drops the rest by order). Either way ONE notification per event.
19931
+ * Absent = `mosaic`, applied by the Notification Center and deliberately NOT
19932
+ * a Zod default (see NcRulePatchSchema: a default materialises on every
19933
+ * partial patch). Meaningless for a rule whose subject is not a sensor, and a
19934
+ * sensor linked to one camera ships that camera's plain snapshot either way.
19935
+ */
19936
+ var NcLinkedCameraModeSchema = _enum(["mosaic", "each"]);
20000
19937
  var NcMediaPolicySchema = object({
20001
19938
  attach: _enum([
20002
19939
  "best",
@@ -20042,7 +19979,9 @@ var NcMediaPolicySchema = object({
20042
19979
  * A profile that is not assigned falls back to the cheapest, and the render
20043
19980
  * reports which one actually ran.
20044
19981
  */
20045
- profile: CamProfileSchema.optional()
19982
+ profile: CamProfileSchema.optional(),
19983
+ /** See {@link NcLinkedCameraModeSchema}. */
19984
+ linkedCameras: NcLinkedCameraModeSchema.optional()
20046
19985
  });
20047
19986
  /**
20048
19987
  * Cooldown GRANULARITY over the subject's class — how much a fired
@@ -20162,7 +20101,7 @@ var NcRuleInputSchema = object({
20162
20101
  cooldownSec: 60,
20163
20102
  scope: "rule-device"
20164
20103
  }),
20165
- /** `{{var}}` templating over camera/class/label/zones/confidence/time. */
20104
+ /** `{{var}}` title/body. The variables are declared in `NC_TEMPLATE_VARS` (@camstack/types) and depend on the rule kind — see `templateVarsFor`. */
20166
20105
  template: object({
20167
20106
  title: string().max(500).optional(),
20168
20107
  body: string().max(2e3).optional()
@@ -20342,6 +20281,22 @@ var NcRulePatchSchema = NcRuleInputSchema.partial().extend({
20342
20281
  throttle: NcThrottleSchema.optional(),
20343
20282
  priority: number().int().min(1).max(5).optional()
20344
20283
  });
20284
+ var NcRuleClearableKeySchema = _enum([
20285
+ "template",
20286
+ "schedule",
20287
+ "targetUsers",
20288
+ "snoozeOptions",
20289
+ "snoozeAllowGlobal",
20290
+ "waitForEnhancement",
20291
+ "actions",
20292
+ "confirm",
20293
+ "groupIdleSec"
20294
+ ]);
20295
+ var NcRuleUpdateInputSchema = object({
20296
+ ruleId: string(),
20297
+ patch: NcRulePatchSchema,
20298
+ clear: array(NcRuleClearableKeySchema).optional()
20299
+ });
20345
20300
  /** A persisted rule. */
20346
20301
  var NcRuleSchema = NcRuleInputSchema.extend({
20347
20302
  id: string(),
@@ -20373,6 +20328,85 @@ var NcTestResultSchema = object({
20373
20328
  className: string().optional(),
20374
20329
  label: string().optional()
20375
20330
  });
20331
+ /** The five rule kinds, as data. `NcRuleKind` in `notification/rule-kinds.ts`
20332
+ * is the same union; the template-vars spec pins the two together. */
20333
+ var NcRuleKindSchema = _enum([
20334
+ "detection",
20335
+ "sensor",
20336
+ "occupancy",
20337
+ "sound",
20338
+ "system"
20339
+ ]);
20340
+ /** Which producer renders a template: an ordinary rule, a timelapse, a digest. */
20341
+ var NcTemplateFamilySchema = _enum([
20342
+ "rule",
20343
+ "timelapse",
20344
+ "summary"
20345
+ ]);
20346
+ /** Which text field of that producer. `previewText` is the timelapse frame
20347
+ * caption, `captionText` the digest mosaic caption — both see fewer vars. */
20348
+ var NcTemplateFieldSchema = _enum([
20349
+ "title",
20350
+ "body",
20351
+ "previewText",
20352
+ "captionText"
20353
+ ]);
20354
+ var NcTemplateVarGroupSchema = _enum([
20355
+ "subject",
20356
+ "place",
20357
+ "time",
20358
+ "rule",
20359
+ "occupancy",
20360
+ "sound",
20361
+ "sensor",
20362
+ "system",
20363
+ "digest",
20364
+ "ai"
20365
+ ]);
20366
+ /**
20367
+ * ONE `{{var}}` a notification template may name.
20368
+ *
20369
+ * `families` / `fields` / `kinds` / `deliveries` / `systemEventKinds` say WHERE
20370
+ * it has a value; absent = no restriction on that axis. `systemEventKinds` is
20371
+ * read only for a `system` rule. `dynamic` declares a FAMILY of names
20372
+ * (`count_<class>`): the descriptor's `name` is the example member.
20373
+ */
20374
+ var NcTemplateVarDescriptorSchema = object({
20375
+ name: string().regex(/^\w+$/),
20376
+ label: string(),
20377
+ description: string().optional(),
20378
+ example: string(),
20379
+ group: NcTemplateVarGroupSchema,
20380
+ families: array(NcTemplateFamilySchema).min(1),
20381
+ fields: array(NcTemplateFieldSchema).optional(),
20382
+ kinds: array(NcRuleKindSchema).optional(),
20383
+ deliveries: array(NcDeliverySchema).optional(),
20384
+ systemEventKinds: array(NcSystemEventKindSchema).optional(),
20385
+ dynamic: object({
20386
+ prefix: literal("count_"),
20387
+ from: literal("classes")
20388
+ }).optional()
20389
+ });
20390
+ /** Input to `previewTemplate` — the editor's own context, plus the draft text. */
20391
+ var NcTemplatePreviewInputSchema = object({
20392
+ context: object({
20393
+ family: NcTemplateFamilySchema,
20394
+ field: NcTemplateFieldSchema,
20395
+ kind: NcRuleKindSchema.optional(),
20396
+ delivery: NcDeliverySchema.optional(),
20397
+ systemEventKinds: array(NcSystemEventKindSchema).optional()
20398
+ }),
20399
+ template: object({
20400
+ title: string().max(500).optional(),
20401
+ body: string().max(2e3).optional()
20402
+ })
20403
+ });
20404
+ var NcTemplatePreviewSchema = object({
20405
+ title: string().nullable(),
20406
+ body: string().nullable(),
20407
+ /** Names the template uses that this context never fills — they render empty. */
20408
+ unknown: array(string())
20409
+ });
20376
20410
  var NcConditionDescriptorSchema = object({
20377
20411
  /** Field id inside `NcConditions` (or `'schedule'` for the rule-level group). */
20378
20412
  id: string(),
@@ -20536,6 +20570,14 @@ var NC_CONDITION_CATALOG = [
20536
20570
  value: "backup-completed",
20537
20571
  label: "Backup completed"
20538
20572
  },
20573
+ {
20574
+ value: "alarm-arm-refused",
20575
+ label: "Alarm arm refused"
20576
+ },
20577
+ {
20578
+ value: "alarm-check-failed",
20579
+ label: "Alarm check failed (armed anyway)"
20580
+ },
20539
20581
  {
20540
20582
  value: "alarm-arming",
20541
20583
  label: "Alarm arming (exit delay)"
@@ -20544,10 +20586,22 @@ var NC_CONDITION_CATALOG = [
20544
20586
  value: "alarm-armed",
20545
20587
  label: "Alarm armed"
20546
20588
  },
20589
+ {
20590
+ value: "alarm-pending",
20591
+ label: "Alarm entry delay started"
20592
+ },
20547
20593
  {
20548
20594
  value: "alarm-triggered",
20549
20595
  label: "Alarm triggered"
20550
20596
  },
20597
+ {
20598
+ value: "alarm-rearmed",
20599
+ label: "Alarm re-armed after siren"
20600
+ },
20601
+ {
20602
+ value: "alarm-sensor-bypass",
20603
+ label: "Alarm sensor bypass"
20604
+ },
20551
20605
  {
20552
20606
  value: "alarm-disarmed",
20553
20607
  label: "Alarm disarmed"
@@ -21097,8 +21151,17 @@ object({
21097
21151
  lastAt: number()
21098
21152
  });
21099
21153
  /**
21100
- * The three durations the panel's state machine runs on, plus who hears about
21101
- * an arm.
21154
+ * The per-mode lists, at most one per mode. Two lists for one mode have no
21155
+ * meaning a reader could agree on (union? the last one?), so they are refused
21156
+ * at the boundary rather than interpreted.
21157
+ */
21158
+ var NcAlarmNonBlockingListSchema = array(object({
21159
+ mode: AlarmArmModeSchema,
21160
+ deviceIds: array(number().int()).max(200)
21161
+ })).max(8).refine((lists) => new Set(lists.map((l) => l.mode)).size === lists.length, { message: "nonBlocking: at most one list per arm mode" });
21162
+ /**
21163
+ * The three durations the panel's state machine runs on, who hears about its
21164
+ * transitions, and which openings each mode tolerates.
21102
21165
  *
21103
21166
  * They live on the NOTIFICATION-RULES cap, not on `alarm-panel`, on purpose:
21104
21167
  * `alarm-panel` is `deviceNative` and its other provider mirrors somebody
@@ -21117,21 +21180,39 @@ var NcAlarmSettingsSchema = object({
21117
21180
  * existed, and therefore what an untouched install keeps doing.
21118
21181
  */
21119
21182
  triggeredDurationSec: number().int().min(0).max(3600),
21120
- /** Send a notification when a mode takes effect. */
21121
- announceArm: boolean(),
21122
21183
  /**
21123
- * Where that notification goes. Target ids from `notification-output`.
21184
+ * Deprecated by D630 — every transition is announced. Parsed, never read;
21185
+ * remove after one release. Optional so an older admin's patch (which still
21186
+ * sends it) parses rather than failing the whole save.
21187
+ */
21188
+ announceArm: boolean().optional(),
21189
+ /**
21190
+ * Target ids from `notification-output`: every alarm transition goes here.
21124
21191
  *
21125
- * Explicit rather than "everyone": an arm announcement is a household
21192
+ * Explicit rather than "everyone": an alarm announcement is a household
21126
21193
  * message, and broadcasting it to every configured endpoint (including a
21127
21194
  * webhook wired to something else) is not a default anybody would choose.
21128
- * Empty with `announceArm: true` sends nothing, and the server logs that —
21129
- * silence must be attributable.
21195
+ * Empty sends nothing, and the server logs that — silence must be
21196
+ * attributable.
21130
21197
  */
21131
- announceTargets: array(string().min(1)).max(16)
21198
+ announceTargets: array(string().min(1)).max(16),
21199
+ /**
21200
+ * Per arm mode, the covered devices whose being OPEN does not refuse the
21201
+ * arm (a window left ajar for the cat under `home`, say). At most one entry
21202
+ * per mode is meaningful. Defaulted to `[]` so a blob stored before D630
21203
+ * parses as "every opening blocks" — the behaviour it was written under.
21204
+ */
21205
+ nonBlocking: NcAlarmNonBlockingListSchema.default([])
21132
21206
  });
21133
- /** Every field optional — a tab edits one control at a time. */
21134
- var NcAlarmSettingsPatchSchema = NcAlarmSettingsSchema.partial();
21207
+ /**
21208
+ * Every field optional — a tab edits one control at a time.
21209
+ *
21210
+ * `nonBlocking` is re-declared WITHOUT its default: `.partial()` does not
21211
+ * remove an inner `.default()`, so a patch that never named the field would
21212
+ * parse to `nonBlocking: []` and wipe every mode's list on an unrelated edit
21213
+ * (the same trap {@link NcRulePatchSchema} documents).
21214
+ */
21215
+ var NcAlarmSettingsPatchSchema = NcAlarmSettingsSchema.extend({ nonBlocking: NcAlarmNonBlockingListSchema }).partial();
21135
21216
  /**
21136
21217
  * What one arm mode actually arms, DERIVED from the enabled rules gated on it.
21137
21218
  * Never authored, never stored — see `alarm-mode-coverage.ts` for why a stored
@@ -21176,6 +21257,36 @@ var NcAlarmModeCoverageSchema = object({
21176
21257
  */
21177
21258
  skippedDevices: array(NcAlarmSkippedDeviceSchema).default([])
21178
21259
  });
21260
+ /** One covered device that reads open, and whether that refuses the arm. */
21261
+ var NcAlarmOpeningSchema = object({
21262
+ deviceId: number().int(),
21263
+ name: string(),
21264
+ /** The device's state word as read (`open`, `unlocked`, …). */
21265
+ state: string(),
21266
+ /** False when the mode's non-blocking list names this device. */
21267
+ blocking: boolean()
21268
+ });
21269
+ /** A device the panel is currently ignoring, and which side of it it is on. */
21270
+ var NcAlarmExclusionViewSchema = object({
21271
+ deviceId: number().int(),
21272
+ name: string(),
21273
+ phase: _enum(["open", "closed"])
21274
+ });
21275
+ /** One mode's openings: devices read open, plus those whose state is unknown. */
21276
+ var NcAlarmModeOpeningsSchema = object({
21277
+ mode: AlarmArmModeSchema,
21278
+ devices: array(NcAlarmOpeningSchema),
21279
+ /** Covered devices whose state could not be read (D49): neither open nor closed. */
21280
+ unknown: array(number().int())
21281
+ });
21282
+ var NcAlarmLiveSchema = object({
21283
+ state: AlarmStateSchema,
21284
+ /** The mode being armed into (exit delay) or held; null when disarmed. */
21285
+ targetMode: AlarmArmModeSchema.nullable(),
21286
+ availableModes: array(AlarmArmModeSchema),
21287
+ openings: array(NcAlarmModeOpeningsSchema),
21288
+ exclusions: array(NcAlarmExclusionViewSchema)
21289
+ });
21179
21290
  var NcAlarmConfigSchema = object({
21180
21291
  /**
21181
21292
  * The panel's device id, or null when this install has no panel (the ensure
@@ -21185,7 +21296,13 @@ var NcAlarmConfigSchema = object({
21185
21296
  */
21186
21297
  deviceId: number().int().nullable(),
21187
21298
  settings: NcAlarmSettingsSchema,
21188
- coverage: array(NcAlarmModeCoverageSchema)
21299
+ coverage: array(NcAlarmModeCoverageSchema),
21300
+ /**
21301
+ * The panel as it is NOW — state, what each mode would find open, what is
21302
+ * excluded. Optional: a panel-less hub answers without it, and a client
21303
+ * must not read its absence as "nothing is open".
21304
+ */
21305
+ live: NcAlarmLiveSchema.optional()
21189
21306
  });
21190
21307
  /**
21191
21308
  * ONE rule's demand on ONE camera's clip ring.
@@ -21241,10 +21358,7 @@ var notificationRulesCapability = {
21241
21358
  auth: "admin",
21242
21359
  caller: "required"
21243
21360
  }),
21244
- updateRule: method(object({
21245
- ruleId: string(),
21246
- patch: NcRulePatchSchema
21247
- }), object({ rule: NcRuleSchema }), {
21361
+ updateRule: method(NcRuleUpdateInputSchema, object({ rule: NcRuleSchema }), {
21248
21362
  kind: "mutation",
21249
21363
  auth: "admin",
21250
21364
  caller: "required"
@@ -21332,6 +21446,18 @@ var notificationRulesCapability = {
21332
21446
  catalog: array(NcConditionDescriptorSchema),
21333
21447
  taxonomy: NcTaxonomySchema.optional()
21334
21448
  })),
21449
+ /** The declared `{{var}}` vocabulary; editors filter it with `templateVarsFor`. */
21450
+ getTemplateCatalog: method(object({}), object({ vars: array(NcTemplateVarDescriptorSchema) })),
21451
+ /**
21452
+ * Render a template with example values, through the hub's text catalog.
21453
+ * Pure: sends nothing. A MUTATION only for transport — a 2.5k-char draft in
21454
+ * a batched GET URL risks length limits — so access stays `view`, the same
21455
+ * as `getTemplateCatalog`.
21456
+ */
21457
+ previewTemplate: method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
21458
+ kind: "mutation",
21459
+ access: "view"
21460
+ }),
21335
21461
  /**
21336
21462
  * Queryable delivery history — a read-only view over the durable outbox
21337
21463
  * (fired rule, subject summary, target, status, timestamps, error on a
@@ -21386,7 +21512,10 @@ var notificationRulesCapability = {
21386
21512
  caller: "required"
21387
21513
  }),
21388
21514
  /**
21389
- * The alarm panel's durations, plus what each mode ACTUALLY arms.
21515
+ * The alarm panel's settings, what each mode ACTUALLY arms, and — when a
21516
+ * panel exists — its `live` state: current state and target mode, what
21517
+ * each mode would find open (blocking or not, and what is unknown), and
21518
+ * the devices currently excluded.
21390
21519
  *
21391
21520
  * Coverage is returned by the same call as the settings on purpose: they
21392
21521
  * are read together or not at all. An editor that showed the delays
@@ -21396,7 +21525,9 @@ var notificationRulesCapability = {
21396
21525
  */
21397
21526
  getAlarmConfig: method(object({}), NcAlarmConfigSchema, { auth: "admin" }),
21398
21527
  /**
21399
- * Patch the durations. Returns the WHOLE config, coverage included, so a
21528
+ * Patch the settings (durations, recipients, per-mode non-blocking lists;
21529
+ * `announceArm` is accepted and ignored — D630). Returns the WHOLE config,
21530
+ * coverage and `live` included, so a
21400
21531
  * client never has to guess what the server settled on — the panel clamps
21401
21532
  * and normalises, and a form that re-rendered from its own input would
21402
21533
  * show a value the alarm is not using.
@@ -27253,6 +27384,7 @@ var CAP_TO_KIND = {
27253
27384
  "enum-sensor": "enum-sensor",
27254
27385
  "event-emitter": "device-event",
27255
27386
  "lock-control": "lock",
27387
+ cover: "cover",
27256
27388
  switch: "switch",
27257
27389
  button: "button",
27258
27390
  doorbell: "doorbell"
@@ -28448,6 +28580,87 @@ var storageProviderCapability = {
28448
28580
  })
28449
28581
  }
28450
28582
  };
28583
+ /**
28584
+ * The signals a device can emit to WAKE its own stream.
28585
+ *
28586
+ * A camera whose stream is built on demand sleeps until something asks for it,
28587
+ * and "something" cannot be a consumer that is merely attached — a Frigate-style
28588
+ * puller holds a session open for ever, and treating that as demand would keep
28589
+ * a battery camera awake for ever, which is the whole thing the battery is for
28590
+ * (D173). So the wake has to come from the CAMERA: an event it noticed by
28591
+ * itself, with no stream running.
28592
+ *
28593
+ * ## The vocabulary is the PROVIDER'S, not ours
28594
+ *
28595
+ * Like `consumables`, this cap declares no vocabulary of its own. A provider
28596
+ * names each signal with a `code` it chooses and a `label` an operator reads.
28597
+ * Reolink offers motion and camera-native detection; another provider may offer
28598
+ * a tamper, a doorbell press, a PIR, or something no camera in this fleet has
28599
+ * yet. A fixed enum here would mean every new signal is a framework release.
28600
+ *
28601
+ * It is deliberately NOT derived from the caps a device already binds. Whether
28602
+ * a camera CAN push firmware motion is expressed by `motionSources` containing
28603
+ * `'onboard'`, and whether it does AI on-camera by the `native-object-detection`
28604
+ * binding — but both answer "what drives the detection pipeline", which is a
28605
+ * different question from "what may wake a sleeping stream". A camera can do
28606
+ * the first and not be trusted with the second, and the operator picks per
28607
+ * camera. Two questions, two authorities.
28608
+ *
28609
+ * ## Availability is not permission
28610
+ *
28611
+ * `listSignals` says what the device CAN emit. Whether a given signal actually
28612
+ * wakes the stream is the operator's per-camera choice, held by the broker
28613
+ * alongside the cooldown — see the stream-broker cap's wake settings. A
28614
+ * provider declaring a signal is not a provider enabling it.
28615
+ */
28616
+ /** One signal a device can emit. */
28617
+ var StreamSignalSchema = object({
28618
+ /** Stable id chosen by the provider, e.g. `'motion'`, `'person'`, `'tamper'`. */
28619
+ code: string().min(1),
28620
+ /** What an operator reads in the picker. The provider's own wording. */
28621
+ label: string().min(1),
28622
+ /**
28623
+ * Whether the provider recommends this signal ON when a camera is first set
28624
+ * up. A provider knows which of its signals are cheap and reliable; an
28625
+ * operator should not have to discover that by trial. Reolink recommends
28626
+ * both of its own.
28627
+ */
28628
+ recommended: boolean()
28629
+ });
28630
+ var StreamSignalsStatusSchema = object({
28631
+ signals: array(StreamSignalSchema),
28632
+ lastFetchedAt: number()
28633
+ });
28634
+ var streamSignalsCapability = {
28635
+ name: "stream-signals",
28636
+ scope: "device",
28637
+ deviceNative: true,
28638
+ mode: "singleton",
28639
+ deviceTypes: Object.values(DeviceType),
28640
+ runtimeState: StreamSignalsStatusSchema,
28641
+ /**
28642
+ * Runtime-state durability: **session** — mirrored in RAM, never written.
28643
+ *
28644
+ * The slice holds what the DEVICE says it can emit. That is a probed fact,
28645
+ * not an operator choice: the provider re-declares it on every registration,
28646
+ * so losing it loses nothing and persisting it would freeze an answer the
28647
+ * camera is entitled to change. Measured the same day on the sibling case —
28648
+ * `native-object-detection.supportedClasses` was persisted, and a firmware
28649
+ * class the camera really detected stayed missing for the life of the row
28650
+ * because the fix could not reach it.
28651
+ *
28652
+ * See `RuntimeStateDurability`. Enforced by
28653
+ * `scripts/check-runtime-state-durability.ts`.
28654
+ */
28655
+ durability: "session",
28656
+ methods: {
28657
+ /**
28658
+ * What this device can emit. Empty is a valid and common answer — most
28659
+ * cameras have nothing to offer here, and an empty list is what makes the
28660
+ * broker's picker show nothing rather than a false choice.
28661
+ */
28662
+ listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
28663
+ };
28451
28664
  /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
28452
28665
  var ProfileSettingsSchemaBridge = unknown().nullable();
28453
28666
  var ProfileSettingsBagSchema = record(string(), unknown());
@@ -31681,6 +31894,179 @@ getCredentials: method(object({ deviceId: number() }), CameraCredentialsSchema.n
31681
31894
  }
31682
31895
  };
31683
31896
  /**
31897
+ * camera-grid-layout — the geometry of a COMPOSITE camera, on the camera's own
31898
+ * page.
31899
+ *
31900
+ * ## Why this is a capability and not an addon settings schema
31901
+ *
31902
+ * It was one, and it did not render. The addon declared the editor as a
31903
+ * `type: 'widget'` field inside its own `deviceSettingsSchema()`; the hub
31904
+ * returned that section correctly and `ConfigFormField` renders `type:'widget'`
31905
+ * perfectly well — and nothing ever asked camera-grid for it. The Cluster →
31906
+ * Pipeline → Device Overrides page interrogates a HAND-WRITTEN list of four
31907
+ * addons (`PIPELINE_CLUSTER_DEVICE_ADDONS`), whose own comment says an addon
31908
+ * not on it "falls off silently".
31909
+ *
31910
+ * Adding a fifth name to that list would have been the wrong fix twice over:
31911
+ * that page is per-camera DETECTION tuning, and a grid's geometry belongs
31912
+ * beside PTZ and motion zones on the camera itself. The device page is
31913
+ * BINDING-driven (D12), so the way in is a capability bound to the device —
31914
+ * and this cap carries its section the way `recording` does, by RETURNING it
31915
+ * from `getDeviceSettingsContribution`.
31916
+ *
31917
+ * Seven other widgets are still declared the other way, through a
31918
+ * `deviceConfig.ui` block the framework derives a section from. That route
31919
+ * gives the addon no say in where its own panel lands and no way to decline
31920
+ * for a device the panel does not suit, which is why this one does not use it.
31921
+ *
31922
+ * ## Why one addon may implement it
31923
+ *
31924
+ * It is a device-scoped NATIVE cap, registered by the grid camera device
31925
+ * itself. Nothing else declares a composite camera, so nothing else has a
31926
+ * layout — and the device-scoped route means the widget asks THE camera, not
31927
+ * "the camera-grid addon", which is what let the old custom-action pair be
31928
+ * reached only by a caller that already knew the addon id.
31929
+ *
31930
+ * ## The tab
31931
+ *
31932
+ * `streaming`, not a top-tab of its own. A grid's geometry IS what its stream
31933
+ * is, so the Streaming tab is where it belongs; a `grid` top-tab would need an
31934
+ * entry in `WELL_KNOWN_TAB_MAP` or the device page renders the raw id as the
31935
+ * label (measured on the robot camera, 2026-09-06 — a tab called "navigation"
31936
+ * next to "PTZ").
31937
+ */
31938
+ /** A rectangle in normalized [0,1] coordinates of whatever contains it. */
31939
+ var GridNormalizedRectSchema = object({
31940
+ x: number().min(0).max(1),
31941
+ y: number().min(0).max(1),
31942
+ width: number().gt(0).max(1),
31943
+ height: number().gt(0).max(1)
31944
+ });
31945
+ /**
31946
+ * One source camera, the part of its picture taken, and where that part lands.
31947
+ *
31948
+ * Both rectangles are NORMALIZED (D519): a source camera can change resolution
31949
+ * — a profile switch, a firmware update, a substream that comes back different
31950
+ * — and a stored PIXEL rectangle would quietly start cutting the wrong region,
31951
+ * which is the class of bug nobody files.
31952
+ */
31953
+ var GridLayoutCellSchema = object({
31954
+ deviceId: number().int().positive(),
31955
+ /** The part of the SOURCE taken, normalized against the source. */
31956
+ source: GridNormalizedRectSchema,
31957
+ /** Where it lands, normalized against the CANVAS. */
31958
+ cell: GridNormalizedRectSchema
31959
+ });
31960
+ /**
31961
+ * Which profiles this grid can actually compose, and why not.
31962
+ *
31963
+ * A grid's `high` composes its sources' `high` and its `low` their `low`, so a
31964
+ * profile is on offer only when EVERY source can serve it. The refusal NAMES
31965
+ * the sources, because "this grid has no low" is not a finding — "615 has no
31966
+ * low" is, and it is the one an operator can act on.
31967
+ */
31968
+ var GridProfileOfferSchema = object({
31969
+ profile: _enum([
31970
+ "high",
31971
+ "mid",
31972
+ "low"
31973
+ ]),
31974
+ offered: boolean(),
31975
+ /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
31976
+ missingSources: array(number().int().positive()),
31977
+ /**
31978
+ * The canvas this profile composes onto, `WxH`, or empty when it is not
31979
+ * offered. DERIVED from the cells and the sources' own size at this profile —
31980
+ * it is reported because nothing else in the system would ever say what the
31981
+ * grid came out as, and because it is the number an operator would otherwise
31982
+ * expect to type.
31983
+ */
31984
+ canvas: string(),
31985
+ /**
31986
+ * Whether this profile is PUBLISHED, of the ones the grid could serve.
31987
+ *
31988
+ * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
31989
+ * a 4K canvas built from 4K decodes — something to opt into, not something a
31990
+ * viewer's adaptive should be handed by climbing to the top rung it can see.
31991
+ * Default is `mid` + `low`.
31992
+ */
31993
+ published: boolean()
31994
+ });
31995
+ var GridLayoutViewSchema = object({
31996
+ /** The persisted grid row this camera was declared from. */
31997
+ instanceId: string(),
31998
+ deviceId: number().int().nonnegative(),
31999
+ name: string(),
32000
+ /**
32001
+ * NO canvas size. A grid's resolution is not authored: each profile derives
32002
+ * its own from the cells and its sources' dimensions. The two numbers that
32003
+ * used to be here were a text field that silently decided both how much the
32004
+ * composite cost and how sharp it was — see `profiles[].canvas` for what it
32005
+ * came out as.
32006
+ */
32007
+ fps: number().int(),
32008
+ cells: array(GridLayoutCellSchema),
32009
+ /** What the catalog will publish, and what it refuses to. Read-only. */
32010
+ profiles: array(GridProfileOfferSchema)
32011
+ });
32012
+ var GridLayoutPatchSchema = object({
32013
+ deviceId: number().int().nonnegative(),
32014
+ name: string().min(1).max(160).optional(),
32015
+ fps: number().int().min(1).max(60).optional(),
32016
+ /** Which profiles to publish. See `GridProfileOffer.published`. */
32017
+ publishedProfiles: array(_enum([
32018
+ "high",
32019
+ "mid",
32020
+ "low"
32021
+ ])).max(3).optional(),
32022
+ /**
32023
+ * The whole cell list at once. A per-cell patch would need an ordering the
32024
+ * editor does not have, and a half-applied layout is a picture nobody asked
32025
+ * for.
32026
+ */
32027
+ cells: array(GridLayoutCellSchema).max(16)
32028
+ });
32029
+ var cameraGridLayoutCapability = {
32030
+ name: "camera-grid-layout",
32031
+ scope: "device",
32032
+ deviceNative: true,
32033
+ mode: "singleton",
32034
+ deviceTypes: [DeviceType.Camera],
32035
+ /**
32036
+ * The section is built by the ADDON and returned from
32037
+ * `getDeviceSettingsContribution`, not derived by the framework from a
32038
+ * `deviceConfig.ui` block.
32039
+ *
32040
+ * Both mechanisms render the same widget. This one hands the addon two
32041
+ * things the framework-derived route cannot give it:
32042
+ *
32043
+ * - it chooses its own section, `tab`, `location` and `order`, the way any
32044
+ * other setting does, instead of receiving them from a cap declaration;
32045
+ * - it can DECLINE per device. A camera that is not a grid gets no section
32046
+ * at all, rather than a widget that renders its own "not a grid" state.
32047
+ *
32048
+ * `recording` is the precedent (`recorder/recording-device-settings.ts`): it
32049
+ * returns `null` for anything that is not a camera, so the Recording tab
32050
+ * never appears there.
32051
+ */
32052
+ exposesDeviceSettings: true,
32053
+ methods: {
32054
+ /**
32055
+ * The grid behind this device.
32056
+ *
32057
+ * `null` means ANSWERED and this camera is not a grid — not "not yet
32058
+ * known". The widget renders its "this is not a grid camera" state only
32059
+ * from this answer, never from an unresolved query (D315).
32060
+ */
32061
+ getLayout: method(object({ deviceId: number().int().nonnegative() }), GridLayoutViewSchema.nullable(), { auth: "admin" }),
32062
+ /** Write the geometry back. Returns the grid as it now stands, profiles included. */
32063
+ saveLayout: method(GridLayoutPatchSchema, GridLayoutViewSchema, {
32064
+ kind: "mutation",
32065
+ auth: "admin"
32066
+ })
32067
+ }
32068
+ };
32069
+ /**
31684
32070
  * Carbon-monoxide alarm sensor. Drives Home Assistant `binary_sensor`
31685
32071
  * entries with `device_class: carbon_monoxide`. Push-driven.
31686
32072
  */
@@ -32491,346 +32877,6 @@ var dayNightCapability = {
32491
32877
  volatileStateFields: ["lastFetchedAt"]
32492
32878
  };
32493
32879
  /**
32494
- * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
32495
- * writes to the CAMERA's own card, on the camera's own schedule.
32496
- *
32497
- * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
32498
- * footage ledger, our storage locations, our retention. This one has a
32499
- * different authority — the camera's firmware — and per D62 it stores
32500
- * nothing of its own. Every value here is read from the camera and every
32501
- * write goes back to the camera; there is no CamStack-side mirror that
32502
- * could disagree with the device.
32503
- *
32504
- * ## One shape, two firmwares
32505
- *
32506
- * Measured 2026-09-22 against the live fleet:
32507
- *
32508
- * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
32509
- * | --- | --- | --- |
32510
- * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
32511
- * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
32512
- * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
32513
- * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
32514
- * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
32515
- * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
32516
- * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
32517
- * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
32518
- *
32519
- * The two schedule models look different and are the same thing in
32520
- * different coordinates: both answer "for this trigger, during which
32521
- * weekly windows does the camera record". {@link RecordWindow} is that
32522
- * question in one shape — Hikvision's ranges map straight onto it,
32523
- * Reolink's mask expands into hour-aligned windows.
32524
- *
32525
- * ## Union, not intersection
32526
- *
32527
- * **The same fields exist on every camera.** What differs per device is
32528
- * which VALUES that device accepts, and that is what {@link
32529
- * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
32530
- * per field plus the schedule's own limits. A control a camera cannot
32531
- * honour is rendered DISABLED WITH ITS REASON, never missing and never
32532
- * dead: disabled must not look like broken.
32533
- *
32534
- * ## Refusal by name
32535
- *
32536
- * A write a camera cannot honour is refused with a sentence the operator
32537
- * can read — never accepted and dropped. Both providers refuse through
32538
- * {@link describeOnboardRefusal}, so the vocabulary is one function and
32539
- * one test, not two hand-written vendor opinions.
32540
- *
32541
- * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
32542
- * `getOptions` advertises per-camera availability, `getStatus` (auto-
32543
- * injected from `status`) reports the live values, and a single
32544
- * `setSettings` mutation applies a partial change. No hand-written
32545
- * settings-contribution methods.
32546
- */
32547
- /**
32548
- * What makes the camera start recording during a window.
32549
- *
32550
- * The union of both vendors' vocabularies. `continuous` is Hikvision's
32551
- * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
32552
- * object-class triggers are Reolink-only today and the smart-event ones
32553
- * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
32554
- * firmwares measured — a camera that cannot record on a trigger simply
32555
- * does not list it in `options.schedule.triggers`, and a window naming
32556
- * it is REFUSED, not dropped.
32557
- */
32558
- var RecordTriggerSchema = _enum([
32559
- "continuous",
32560
- "motion",
32561
- "person",
32562
- "vehicle",
32563
- "animal",
32564
- "lineCrossing",
32565
- "intrusion",
32566
- "loitering",
32567
- "alarmInput"
32568
- ]);
32569
- /**
32570
- * One weekly recording window: "on `day`, from `startMinute` to
32571
- * `endMinute`, record on `trigger`".
32572
- *
32573
- * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
32574
- * both firmwares enumerate). Minutes are local camera time since
32575
- * midnight; `endMinute` may be 1440, meaning end of day — that is
32576
- * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
32577
- * collapsing it to 0 would turn a whole-day window into an empty one.
32578
- */
32579
- var RecordWindowSchema = object({
32580
- trigger: RecordTriggerSchema,
32581
- day: number().int().min(0).max(6),
32582
- startMinute: number().int().min(0).max(1439),
32583
- endMinute: number().int().min(1).max(1440)
32584
- });
32585
- /** Status of one physical volume, as the camera itself describes it. */
32586
- var OnboardStorageVolumeSchema = object({
32587
- /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
32588
- id: string(),
32589
- /** The camera's own name for it, when it gives one (`hddName`). */
32590
- label: string().optional(),
32591
- status: _enum([
32592
- "ok",
32593
- "unformatted",
32594
- "error",
32595
- "offline",
32596
- "unknown"
32597
- ]),
32598
- /**
32599
- * Total size in MB, or **null when the camera did not say**.
32600
- *
32601
- * Never 0 for an unreadable value: a measurement that failed is not a
32602
- * measurement (D393), and a card whose size is unknown must not be
32603
- * rendered as a card of size zero.
32604
- */
32605
- capacityMb: number().nullable(),
32606
- /**
32607
- * Free space in MB, or null when unknown.
32608
- *
32609
- * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
32610
- * 1439 both report exactly 11776 MB free — the fixed reserve a looping
32611
- * card converges on once it has wrapped. At loop steady state the
32612
- * number is identical whether the camera recorded yesterday or stopped
32613
- * a month ago.
32614
- */
32615
- freeMb: number().nullable(),
32616
- /** True when the camera reports the volume writable (`property` RW). */
32617
- writable: boolean().optional()
32618
- });
32619
- /**
32620
- * What the camera is doing with its own storage, right now.
32621
- *
32622
- * Every scalar is nullable and **null means the camera did not answer**,
32623
- * never a default. A form that seeds `0` from an unanswered read invites
32624
- * the operator to save that 0 back onto the camera.
32625
- */
32626
- var RecordingOnboardStatusSchema = object({
32627
- storage: discriminatedUnion("kind", [
32628
- object({
32629
- kind: literal("present"),
32630
- volumes: array(OnboardStorageVolumeSchema)
32631
- }),
32632
- object({
32633
- kind: literal("absent"),
32634
- reason: string()
32635
- }),
32636
- object({
32637
- kind: literal("unknown"),
32638
- reason: string()
32639
- })
32640
- ]),
32641
- tracks: array(object({
32642
- id: string(),
32643
- enabled: boolean(),
32644
- isVideo: boolean(),
32645
- /** From the camera's own track description. Null when it does not say. */
32646
- codec: string().nullable(),
32647
- resolution: string().nullable(),
32648
- /** Per-track overwrite flag, where the firmware keeps it per track. */
32649
- overwriteWhenFull: boolean().nullable()
32650
- })),
32651
- /**
32652
- * The track the write path targets — the enabled VIDEO one. Null when
32653
- * no track could be identified, which is itself a refusal reason.
32654
- */
32655
- primaryTrackId: string().nullable(),
32656
- /** Master "record to the card at all" switch. */
32657
- enabled: boolean().nullable(),
32658
- overwriteWhenFull: boolean().nullable(),
32659
- preRecordSec: number().nullable(),
32660
- postRecordSec: number().nullable(),
32661
- /** Length of one recorded file, in minutes. */
32662
- segmentMinutes: number().nullable(),
32663
- /** The primary track's weekly windows, flattened. */
32664
- windows: array(RecordWindowSchema),
32665
- /**
32666
- * How many windows the camera described that CamStack could NOT read —
32667
- * an unrecognised trigger, an unparseable clock, a weekday it does not
32668
- * name.
32669
- *
32670
- * A dropped window is work the reader threw away, and a schedule that
32671
- * silently shows fewer rows than the camera holds is how an operator
32672
- * saves back a schedule shorter than the one they were looking at
32673
- * (D391). Non-zero means the window list is INCOMPLETE and a write
32674
- * that replaces it would delete what was not shown — which is why a
32675
- * provider reporting a non-zero count also reports the schedule as not
32676
- * writable.
32677
- */
32678
- unreadableWindows: number(),
32679
- /**
32680
- * The camera is scheduled to record and has NO usable storage.
32681
- *
32682
- * A first-class fact because it is the fleet's most common silent
32683
- * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
32684
- * to a card that is not there. Neither the schedule nor the storage
32685
- * read says anything wrong on its own; only the pair does.
32686
- */
32687
- recordingToNowhere: boolean(),
32688
- lastFetchedAt: number()
32689
- });
32690
- /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
32691
- var RangeSchema = object({
32692
- min: number(),
32693
- max: number(),
32694
- step: number()
32695
- });
32696
- /**
32697
- * The values a camera actually takes for a numeric field, when they are a SET
32698
- * rather than a range.
32699
- *
32700
- * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
32701
- * (I91DN) on 2026-09-22 by writing each value and reading it back:
32702
- *
32703
- * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
32704
- * camera's "no limit" — `-1` and `4294967295` both land on it);
32705
- * - post-record: `5, 10, 30, 60, 120, 300, 600`.
32706
- *
32707
- * Neither is expressible as a step: the first has a sentinel two billion away
32708
- * from its neighbours, the second doubles and then jumps. A range that tried
32709
- * would forbid values the camera takes AND permit values it silently replaces
32710
- * with 5 — wrong in both directions at once.
32711
- *
32712
- * `sentinel` names the member that is not a duration, so a surface can render
32713
- * "no limit" instead of `2147483647` seconds.
32714
- */
32715
- var AllowedValuesSchema = object({
32716
- values: array(number()).min(1),
32717
- sentinel: object({
32718
- value: number(),
32719
- meaning: _enum(["no-limit", "disabled"])
32720
- }).optional()
32721
- });
32722
- /**
32723
- * Per-field availability on ONE camera.
32724
- *
32725
- * The field exists on every camera — this says whether this one can be
32726
- * read and whether it can be written, and `reason` says why not when
32727
- * either is false. The UI renders the control DISABLED with the reason
32728
- * rather than hiding it, so a limitation is legible instead of looking
32729
- * like a missing feature.
32730
- */
32731
- var OnboardFieldSupportSchema = object({
32732
- readable: boolean(),
32733
- writable: boolean(),
32734
- /** Required whenever `readable` or `writable` is false. */
32735
- reason: string().optional()
32736
- });
32737
- /** What this camera's schedule model can express. */
32738
- var OnboardScheduleSupportSchema = object({
32739
- support: OnboardFieldSupportSchema,
32740
- /**
32741
- * The smallest time step the camera can express, in minutes.
32742
- *
32743
- * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
32744
- * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
32745
- * window whose edges are not a multiple of this is REFUSED rather than
32746
- * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
32747
- * and nothing says so.
32748
- */
32749
- granularityMinutes: number(),
32750
- /** Triggers this camera can record on. A window naming another is refused. */
32751
- triggers: array(RecordTriggerSchema),
32752
- /**
32753
- * False when the camera stores ONE trigger per time range, so two
32754
- * windows overlapping on the same day cannot carry different triggers.
32755
- * True on Reolink, whose mask is per-trigger and independent.
32756
- */
32757
- supportsOverlappingTriggers: boolean()
32758
- });
32759
- var RecordingOnboardOptionsSchema = object({
32760
- enabled: OnboardFieldSupportSchema,
32761
- overwriteWhenFull: OnboardFieldSupportSchema,
32762
- preRecordSec: OnboardFieldSupportSchema,
32763
- preRecordSecRange: RangeSchema.optional(),
32764
- /** Preferred over the range when the camera takes a SET, not a span. */
32765
- preRecordSecAllowed: AllowedValuesSchema.optional(),
32766
- postRecordSec: OnboardFieldSupportSchema,
32767
- postRecordSecRange: RangeSchema.optional(),
32768
- /** Preferred over the range when the camera takes a SET, not a span. */
32769
- postRecordSecAllowed: AllowedValuesSchema.optional(),
32770
- segmentMinutes: OnboardFieldSupportSchema,
32771
- segmentMinutesRange: RangeSchema.optional(),
32772
- /** Preferred over the range when the camera takes a SET, not a span. */
32773
- segmentMinutesAllowed: AllowedValuesSchema.optional(),
32774
- schedule: OnboardScheduleSupportSchema
32775
- });
32776
- /**
32777
- * A partial change. Every field optional.
32778
- *
32779
- * Unlike the other `deviceConfig` caps, a provider here does **NOT**
32780
- * silently ignore a field it cannot support — it refuses, by name,
32781
- * through {@link describeOnboardRefusal}. Silence on a recording setting
32782
- * is the failure D62 exists to prevent: the operator believes the camera
32783
- * is recording the way the form says, and it is not.
32784
- */
32785
- var RecordingOnboardPatchSchema = object({
32786
- enabled: boolean().optional(),
32787
- overwriteWhenFull: boolean().optional(),
32788
- preRecordSec: number().optional(),
32789
- postRecordSec: number().optional(),
32790
- segmentMinutes: number().optional(),
32791
- /** The complete new window set for the primary track — not a delta. */
32792
- windows: array(RecordWindowSchema).optional()
32793
- });
32794
- var recordingOnboardCapability = {
32795
- name: "recording-onboard",
32796
- scope: "device",
32797
- deviceNative: true,
32798
- mode: "singleton",
32799
- deviceTypes: [DeviceType.Camera],
32800
- deviceConfig: { ui: {
32801
- kind: "derived-form",
32802
- builderId: "recording-onboard",
32803
- tab: "recording"
32804
- } },
32805
- methods: {
32806
- getOptions: method(object({ deviceId: number() }), RecordingOnboardOptionsSchema),
32807
- setSettings: method(object({
32808
- deviceId: number(),
32809
- settings: RecordingOnboardPatchSchema
32810
- }), _void(), {
32811
- kind: "mutation",
32812
- auth: "admin"
32813
- })
32814
- },
32815
- status: {
32816
- schema: RecordingOnboardStatusSchema,
32817
- kind: "poll"
32818
- },
32819
- runtimeState: RecordingOnboardStatusSchema,
32820
- /**
32821
- * Runtime-state durability: **restored** — operator-set camera-side
32822
- * recording config; mutation-driven, and the storage half is the last
32823
- * thing the camera said about its own card.
32824
- *
32825
- * See `RuntimeStateDurability`. Enforced by
32826
- * `scripts/check-runtime-state-durability.ts`.
32827
- */
32828
- durability: "restored",
32829
- /** Clock fields: written, but excluded from the compare that decides
32830
- * whether persisting is worth a SQLite commit. */
32831
- volatileStateFields: ["lastFetchedAt"]
32832
- };
32833
- /**
32834
32880
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
32835
32881
  * for every device, regardless of provider — the kernel needs a uniform
32836
32882
  * cap-keyed slice for the basic device flags every consumer expects to
@@ -33049,40 +33095,6 @@ var eventEmitterCapability = {
33049
33095
  */
33050
33096
  durability: "session"
33051
33097
  };
33052
- var EventItemSchema = object({
33053
- id: string(),
33054
- type: string(),
33055
- timestamp: number(),
33056
- label: string().optional(),
33057
- thumbnailUrl: string().optional(),
33058
- clipUrl: string().optional(),
33059
- metadata: record(string(), unknown()).optional()
33060
- });
33061
- var eventsCapability = {
33062
- name: "events",
33063
- scope: "device",
33064
- mode: "singleton",
33065
- deviceTypes: [DeviceType.Camera],
33066
- methods: {
33067
- getEvents: method(object({
33068
- deviceId: number(),
33069
- from: number().optional(),
33070
- to: number().optional(),
33071
- limit: number().optional()
33072
- }), array(EventItemSchema)),
33073
- getEventThumbnail: method(object({
33074
- deviceId: number(),
33075
- eventId: string()
33076
- }), object({
33077
- base64: string(),
33078
- contentType: string()
33079
- }).nullable()),
33080
- getEventClipUrl: method(object({
33081
- deviceId: number(),
33082
- eventId: string()
33083
- }), string().nullable())
33084
- }
33085
- };
33086
33098
  var IdentitySchema = object({
33087
33099
  id: string(),
33088
33100
  name: string(),
@@ -35570,179 +35582,6 @@ var motionTriggerCapability = {
35570
35582
  durability: "session"
35571
35583
  };
35572
35584
  /**
35573
- * camera-grid-layout — the geometry of a COMPOSITE camera, on the camera's own
35574
- * page.
35575
- *
35576
- * ## Why this is a capability and not an addon settings schema
35577
- *
35578
- * It was one, and it did not render. The addon declared the editor as a
35579
- * `type: 'widget'` field inside its own `deviceSettingsSchema()`; the hub
35580
- * returned that section correctly and `ConfigFormField` renders `type:'widget'`
35581
- * perfectly well — and nothing ever asked camera-grid for it. The Cluster →
35582
- * Pipeline → Device Overrides page interrogates a HAND-WRITTEN list of four
35583
- * addons (`PIPELINE_CLUSTER_DEVICE_ADDONS`), whose own comment says an addon
35584
- * not on it "falls off silently".
35585
- *
35586
- * Adding a fifth name to that list would have been the wrong fix twice over:
35587
- * that page is per-camera DETECTION tuning, and a grid's geometry belongs
35588
- * beside PTZ and motion zones on the camera itself. The device page is
35589
- * BINDING-driven (D12), so the way in is a capability bound to the device —
35590
- * and this cap carries its section the way `recording` does, by RETURNING it
35591
- * from `getDeviceSettingsContribution`.
35592
- *
35593
- * Seven other widgets are still declared the other way, through a
35594
- * `deviceConfig.ui` block the framework derives a section from. That route
35595
- * gives the addon no say in where its own panel lands and no way to decline
35596
- * for a device the panel does not suit, which is why this one does not use it.
35597
- *
35598
- * ## Why one addon may implement it
35599
- *
35600
- * It is a device-scoped NATIVE cap, registered by the grid camera device
35601
- * itself. Nothing else declares a composite camera, so nothing else has a
35602
- * layout — and the device-scoped route means the widget asks THE camera, not
35603
- * "the camera-grid addon", which is what let the old custom-action pair be
35604
- * reached only by a caller that already knew the addon id.
35605
- *
35606
- * ## The tab
35607
- *
35608
- * `streaming`, not a top-tab of its own. A grid's geometry IS what its stream
35609
- * is, so the Streaming tab is where it belongs; a `grid` top-tab would need an
35610
- * entry in `WELL_KNOWN_TAB_MAP` or the device page renders the raw id as the
35611
- * label (measured on the robot camera, 2026-09-06 — a tab called "navigation"
35612
- * next to "PTZ").
35613
- */
35614
- /** A rectangle in normalized [0,1] coordinates of whatever contains it. */
35615
- var GridNormalizedRectSchema = object({
35616
- x: number().min(0).max(1),
35617
- y: number().min(0).max(1),
35618
- width: number().gt(0).max(1),
35619
- height: number().gt(0).max(1)
35620
- });
35621
- /**
35622
- * One source camera, the part of its picture taken, and where that part lands.
35623
- *
35624
- * Both rectangles are NORMALIZED (D519): a source camera can change resolution
35625
- * — a profile switch, a firmware update, a substream that comes back different
35626
- * — and a stored PIXEL rectangle would quietly start cutting the wrong region,
35627
- * which is the class of bug nobody files.
35628
- */
35629
- var GridLayoutCellSchema = object({
35630
- deviceId: number().int().positive(),
35631
- /** The part of the SOURCE taken, normalized against the source. */
35632
- source: GridNormalizedRectSchema,
35633
- /** Where it lands, normalized against the CANVAS. */
35634
- cell: GridNormalizedRectSchema
35635
- });
35636
- /**
35637
- * Which profiles this grid can actually compose, and why not.
35638
- *
35639
- * A grid's `high` composes its sources' `high` and its `low` their `low`, so a
35640
- * profile is on offer only when EVERY source can serve it. The refusal NAMES
35641
- * the sources, because "this grid has no low" is not a finding — "615 has no
35642
- * low" is, and it is the one an operator can act on.
35643
- */
35644
- var GridProfileOfferSchema = object({
35645
- profile: _enum([
35646
- "high",
35647
- "mid",
35648
- "low"
35649
- ]),
35650
- offered: boolean(),
35651
- /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
35652
- missingSources: array(number().int().positive()),
35653
- /**
35654
- * The canvas this profile composes onto, `WxH`, or empty when it is not
35655
- * offered. DERIVED from the cells and the sources' own size at this profile —
35656
- * it is reported because nothing else in the system would ever say what the
35657
- * grid came out as, and because it is the number an operator would otherwise
35658
- * expect to type.
35659
- */
35660
- canvas: string(),
35661
- /**
35662
- * Whether this profile is PUBLISHED, of the ones the grid could serve.
35663
- *
35664
- * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
35665
- * a 4K canvas built from 4K decodes — something to opt into, not something a
35666
- * viewer's adaptive should be handed by climbing to the top rung it can see.
35667
- * Default is `mid` + `low`.
35668
- */
35669
- published: boolean()
35670
- });
35671
- var GridLayoutViewSchema = object({
35672
- /** The persisted grid row this camera was declared from. */
35673
- instanceId: string(),
35674
- deviceId: number().int().nonnegative(),
35675
- name: string(),
35676
- /**
35677
- * NO canvas size. A grid's resolution is not authored: each profile derives
35678
- * its own from the cells and its sources' dimensions. The two numbers that
35679
- * used to be here were a text field that silently decided both how much the
35680
- * composite cost and how sharp it was — see `profiles[].canvas` for what it
35681
- * came out as.
35682
- */
35683
- fps: number().int(),
35684
- cells: array(GridLayoutCellSchema),
35685
- /** What the catalog will publish, and what it refuses to. Read-only. */
35686
- profiles: array(GridProfileOfferSchema)
35687
- });
35688
- var GridLayoutPatchSchema = object({
35689
- deviceId: number().int().nonnegative(),
35690
- name: string().min(1).max(160).optional(),
35691
- fps: number().int().min(1).max(60).optional(),
35692
- /** Which profiles to publish. See `GridProfileOffer.published`. */
35693
- publishedProfiles: array(_enum([
35694
- "high",
35695
- "mid",
35696
- "low"
35697
- ])).max(3).optional(),
35698
- /**
35699
- * The whole cell list at once. A per-cell patch would need an ordering the
35700
- * editor does not have, and a half-applied layout is a picture nobody asked
35701
- * for.
35702
- */
35703
- cells: array(GridLayoutCellSchema).max(16)
35704
- });
35705
- var cameraGridLayoutCapability = {
35706
- name: "camera-grid-layout",
35707
- scope: "device",
35708
- deviceNative: true,
35709
- mode: "singleton",
35710
- deviceTypes: [DeviceType.Camera],
35711
- /**
35712
- * The section is built by the ADDON and returned from
35713
- * `getDeviceSettingsContribution`, not derived by the framework from a
35714
- * `deviceConfig.ui` block.
35715
- *
35716
- * Both mechanisms render the same widget. This one hands the addon two
35717
- * things the framework-derived route cannot give it:
35718
- *
35719
- * - it chooses its own section, `tab`, `location` and `order`, the way any
35720
- * other setting does, instead of receiving them from a cap declaration;
35721
- * - it can DECLINE per device. A camera that is not a grid gets no section
35722
- * at all, rather than a widget that renders its own "not a grid" state.
35723
- *
35724
- * `recording` is the precedent (`recorder/recording-device-settings.ts`): it
35725
- * returns `null` for anything that is not a camera, so the Recording tab
35726
- * never appears there.
35727
- */
35728
- exposesDeviceSettings: true,
35729
- methods: {
35730
- /**
35731
- * The grid behind this device.
35732
- *
35733
- * `null` means ANSWERED and this camera is not a grid — not "not yet
35734
- * known". The widget renders its "this is not a grid camera" state only
35735
- * from this answer, never from an unresolved query (D315).
35736
- */
35737
- getLayout: method(object({ deviceId: number().int().nonnegative() }), GridLayoutViewSchema.nullable(), { auth: "admin" }),
35738
- /** Write the geometry back. Returns the grid as it now stands, profiles included. */
35739
- saveLayout: method(GridLayoutPatchSchema, GridLayoutViewSchema, {
35740
- kind: "mutation",
35741
- auth: "admin"
35742
- })
35743
- }
35744
- };
35745
- /**
35746
35585
  * Motion-zones share the same MaskShape vocabulary as privacy-mask — the
35747
35586
  * on-camera motion-detection mask is a single `grid` region (a row-major
35748
35587
  * boolean cell lattice the camera's onboard VMD evaluates). Composing it as
@@ -38412,37 +38251,37 @@ var rebootCapability = {
38412
38251
  auth: "admin"
38413
38252
  }) }
38414
38253
  };
38415
- /**
38416
- * `recording` cap — footage availability + HLS playback manifests + per-device
38417
- * recording config. NOTE on events (source of truth, R5/C3): this cap carries
38418
- * NO event surface — `getPlaybackManifest` returns playlist URLs only. Timeline
38419
- * events (motion/object/audio) come from `pipelineAnalytics` (durable SQLite
38420
- * rows) and are the ONLY event surface — the recorder has none. The in-RAM
38421
- * playback markers it used to build were deleted on 2026-08-29 because nothing
38422
- * ever read them. Event<->footage joins are by time, padded with the shared
38423
- * `EVENT_PAD_MS` (`interfaces/recording-config.ts`).
38424
- */
38425
- var RecordingStatusSchema = object({
38426
- deviceId: number(),
38427
- enabled: boolean(),
38428
- /** THE derived storage mode, from the one definition
38429
- * (`deriveRecordingMode`) — never a second enum. A duplicated list is how
38430
- * `on-device-decision` could have reached the recorder and not the status. */
38431
- activeMode: RecordingStorageModeSchema,
38432
- nodeId: string(),
38433
- storageBytes: number()
38434
- });
38435
38254
  var RecordingRangeSchema = object({
38436
38255
  profile: string(),
38437
38256
  startMs: number(),
38438
38257
  endMs: number()
38439
38258
  });
38259
+ /**
38260
+ * How a source ANSWERED, on every singular read of this cap.
38261
+ *
38262
+ * `'read'` — it looked. `ranges: []` / `days: []` is then a real claim: this
38263
+ * source has no coverage in the window. `'unreadable'` — nobody could look
38264
+ * (the camera was unreachable, the calendar rung threw, the location is
38265
+ * unmounted, the node is still on the old build), and the emptiness beside it
38266
+ * means NOTHING.
38267
+ *
38268
+ * The batch rows have carried this since the grid existed; the SINGULAR
38269
+ * answers gained it with the collection (D625 §10.4), because they are the
38270
+ * ones the single-camera picker uses and because a half-converted fleet makes
38271
+ * "nobody looked" common for the length of a deploy. Without it the timeline
38272
+ * has no vocabulary for it, and `(data ?? [])` in a viewer turns a rollout into
38273
+ * a fleet of cameras that appear to have lost their recordings (D315, D393).
38274
+ */
38275
+ var RecordingReadSchema = _enum(["read", "unreadable"]);
38440
38276
  var RecordingAvailabilitySchema = object({
38441
38277
  deviceId: number(),
38278
+ /** See {@link RecordingReadSchema}. An `'unreadable'` answer carries an empty
38279
+ * `ranges` that means nothing — never draw it as "no footage". */
38280
+ read: RecordingReadSchema,
38442
38281
  ranges: array(RecordingRangeSchema),
38443
38282
  /**
38444
- * Every profile this camera has footage in — not only the one `ranges`
38445
- * describes (D433).
38283
+ * Every profile this camera has footage in AT THIS SOURCE — not only the one
38284
+ * `ranges` describes (D433).
38446
38285
  *
38447
38286
  * `ranges` answers for ONE profile by design: the timeline is a single bar,
38448
38287
  * and enumerating all of them triples the directory reads for a bar that
@@ -38459,15 +38298,344 @@ var RecordingAvailabilitySchema = object({
38459
38298
  });
38460
38299
  var RecordingDaysSchema = object({
38461
38300
  deviceId: number(),
38301
+ /** See {@link RecordingReadSchema}. `days: []` on an `'unreadable'` answer is
38302
+ * "nobody could look", and the date-picker must not spell it the same as
38303
+ * "no footage this month". */
38304
+ read: RecordingReadSchema,
38462
38305
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
38463
38306
  days: array(number())
38464
38307
  });
38308
+ var RecordingManifestSchema = object({
38309
+ deviceId: number(),
38310
+ /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
38311
+ localMasterPath: string().nullable(),
38312
+ /** HTTP(S) URL to the master playlist on the recording node's playback server
38313
+ * (the PRIMARY candidate); null when no recording / server. Carries the
38314
+ * scoped playback token in its path. */
38315
+ playbackUrl: string().nullable(),
38316
+ /**
38317
+ * Candidate master-playlist URLs the client tries in order (LAN first, then
38318
+ * remote — Tailscale/Cloudflare if the operator configured extra hosts), each
38319
+ * carrying the same scoped token. `playbackUrl` is the first entry. Empty when
38320
+ * there is no recording / server.
38321
+ */
38322
+ playbackEndpoints: array(string())
38323
+ });
38324
+ var RecordingSourceAvailabilitySchema = object({
38325
+ state: _enum([
38326
+ "ok",
38327
+ "sleeping",
38328
+ "unreachable",
38329
+ "no-storage",
38330
+ "index-empty"
38331
+ ]),
38332
+ /** Free text, shown verbatim. Names the camera's own refusal when there is one. */
38333
+ reason: string().optional(),
38334
+ /** When this source's coverage was last CONFIRMED. A cached answer is never
38335
+ * drawn as current: the surface shows the age whenever it is older than the
38336
+ * refresh interval. The clip catalog's `catalogAsOf`, under the name the
38337
+ * timeline uses for it. */
38338
+ coverageAsOf: number().optional()
38339
+ });
38340
+ /**
38341
+ * One SOURCE of recorded coverage for a camera — a row of the picker.
38342
+ *
38343
+ * A provider lists the sources IT serves for that device, and answers for each
38344
+ * of them whether it can answer at all. A provider with nothing to offer on a
38345
+ * camera returns `[]` — it is not that camera's business. The five availability
38346
+ * states are `ClipSourceAvailability`'s verbatim: they mean exactly the same
38347
+ * things about a coverage index as about a clip catalog, and `sleeping` in
38348
+ * particular is what stops a battery camera being woken to paint a bar.
38349
+ */
38350
+ var RecordingSourceSchema = object({
38351
+ /** The source id. {@link RECORDING_SOURCE_CAMSTACK} for ours (RESERVED), a
38352
+ * vendor namespace (`native:reolink:onboard`, …) for a camera's own store. */
38353
+ source: string(),
38354
+ /** Operator-facing name of the source ("CamStack recordings", "SD card"). */
38355
+ label: string(),
38356
+ /**
38357
+ * The addon that SERVES this row, and the value a later call passes as
38358
+ * `provider`.
38359
+ *
38360
+ * Optional for version skew only. The collection dispatcher stamps it from
38361
+ * the registry, so a row that travelled through the fan-out carries the
38362
+ * authoritative id whatever the provider filled in (D557 §4).
38363
+ */
38364
+ addonId: string().optional(),
38365
+ availability: RecordingSourceAvailabilitySchema
38366
+ });
38367
+ /**
38368
+ * What a surface may DRAW for this (camera, source) — D612's rule applied to a
38369
+ * timeline: **the source declares what it can do, and the surface draws what
38370
+ * was declared. It never assumes, and never offers a gesture it will then
38371
+ * refuse.** D612 exists because `8` and `16` were offered as clip rates, the
38372
+ * broker clamped them to `4`, and no line anywhere said so.
38373
+ *
38374
+ * Asked once per (camera, source) before anything is drawn — never replaced by
38375
+ * a constant the surface keeps, which is the second authority D612 ends.
38376
+ */
38377
+ var RecordingSourceOptionsSchema = object({
38378
+ /** How this source's media reaches the player.
38379
+ * `archive` = our own indexed segment tree; `stream` = the provider's
38380
+ * forward-only fMP4 (D597); `realtime` = a replay bound to wall clock. */
38381
+ transport: _enum([
38382
+ "archive",
38383
+ "stream",
38384
+ "realtime"
38385
+ ]),
38386
+ /** What the BAR means. `continuous` = gaps are holes in a recording;
38387
+ * `sparse` = gaps are the absence of one, and must be drawn as such.
38388
+ *
38389
+ * Not an onboard-only concession: measured 2026-09-24, OUR bar covers 98.8 %
38390
+ * of 592's day and 1.2 % of 1436's. It is a fact about a (source, camera)
38391
+ * pair, and ours answers it per camera from `deriveRecordingMode`. */
38392
+ coverage: _enum(["continuous", "sparse"]),
38393
+ /** Where the playhead may be put.
38394
+ * `free` — anywhere, to the frame.
38395
+ * `forward` — only ahead of the current position.
38396
+ * `segment` — a position SNAPS to the head of the covering segment; a finer
38397
+ * ask is accepted by the camera and SILENTLY IGNORED. Measured
38398
+ * on 1436 (Hikvision V5.7.1, 2026-09-23): a window-narrowed
38399
+ * `ContentMgmt/search` returns a row and a `playbackURI`, the
38400
+ * replay opens 200 and delivers media — and the burned-in OSD of
38401
+ * the first frame reads the SEGMENT HEAD every time. Calling
38402
+ * that `forward` would tell the surface it may move the playhead
38403
+ * ahead within a loaded segment, which it may not. */
38404
+ seek: _enum([
38405
+ "free",
38406
+ "forward",
38407
+ "segment"
38408
+ ]),
38409
+ /** Frame-step BACKWARD is meaningful. */
38410
+ stepBack: boolean(),
38411
+ /** Whether the drag-scrub gesture is served, as opposed to refused by name. */
38412
+ scrub: boolean(),
38413
+ /** Deliverable rates, ascending, always containing `1`. The surface draws its
38414
+ * picker from this and from NOTHING else (D612, D620, D621). `0` is not a
38415
+ * member: pause is the absence of a rate. */
38416
+ rates: array(number().positive()).min(1).readonly(),
38417
+ /** TRUE when a read of this source HOLDS the camera's only playback session.
38418
+ * A surface with this set makes at most ONE read at a time and draws no
38419
+ * scrub-thumbnail strip, no hover preview, no prefetch and no background
38420
+ * refresh. The precedent is exact and expensive: filling one screen of
38421
+ * Hikvision thumbnails at 1.01× realtime consumed fifteen minutes of that
38422
+ * camera's only playback session (1.2.126, reported within minutes), and a
38423
+ * timeline is a screenful of reads by construction. */
38424
+ exclusive: boolean()
38425
+ });
38426
+ /**
38427
+ * How to PLAY the instant that was asked for, from the chosen source.
38428
+ *
38429
+ * No new media transport is built for onboard sources: the `clip` arm is a
38430
+ * DELEGATION to the `videoclips` transport that vendor already has (D597 /
38431
+ * D616 / D617). The onboard half of this collection is a PROJECTION of
38432
+ * `videoclips` for coverage and a delegation to it for bytes.
38433
+ */
38434
+ var RecordingPlaybackSchema = discriminatedUnion("kind", [
38435
+ object({
38436
+ kind: literal("hls"),
38437
+ manifest: RecordingManifestSchema
38438
+ }),
38439
+ object({
38440
+ kind: literal("clip"),
38441
+ /** The `videoclips` source namespace this clip id belongs to. */
38442
+ source: string(),
38443
+ clipId: string(),
38444
+ /** Where this clip actually STARTS. On a `seek: 'segment'` source the
38445
+ * playhead lands here, not at the requested instant — the surface must be
38446
+ * TOLD, not left to discover it from a burned-in OSD. */
38447
+ startsAtMs: number()
38448
+ }),
38449
+ object({
38450
+ kind: literal("none"),
38451
+ reason: string()
38452
+ })
38453
+ ]);
38454
+ var recordingCapability = {
38455
+ name: "recording",
38456
+ scope: "device",
38457
+ /** Several sources per camera, listed beside each other. The mount stays
38458
+ * `device-scoped` — see `resolveCapMount`'s ordering and D554: per-device
38459
+ * wins over the global collection fan-out. */
38460
+ mode: "collection",
38461
+ kind: "wrapper",
38462
+ defaultActive: true,
38463
+ /** Recorded coverage is a property of a camera — the cap is meaningless on a
38464
+ * sensor, a button or an event emitter, and the `defaultActive` auto-bind
38465
+ * reads this to decide which devices it may claim. */
38466
+ deviceTypes: [DeviceType.Camera],
38467
+ methods: {
38468
+ /**
38469
+ * The sources this camera has, WITH the reason any of them cannot answer.
38470
+ *
38471
+ * Asked separately from `getAvailability` because an empty bar is
38472
+ * ambiguous and this is the only place the ambiguity is resolved: every
38473
+ * bound provider contributes its own rows, and a provider that could not be
38474
+ * reached at all still produces one row saying so. A surface that draws "no
38475
+ * recordings" without reading this is drawing a guess.
38476
+ *
38477
+ * The ONLY method here without a `provider` — it is the call that tells the
38478
+ * caller what to put there.
38479
+ */
38480
+ listSources: method(object({ deviceId: number() }), array(RecordingSourceSchema).readonly(), {
38481
+ kind: "query",
38482
+ auth: "protected"
38483
+ }),
38484
+ /**
38485
+ * Recorded coverage of `[fromMs, toMs)` at ONE source — the timeline bar.
38486
+ *
38487
+ * `protected`, not `admin`: a per-camera read is exactly what a camera
38488
+ * viewer is FOR, and the device-scoped mount routes through
38489
+ * `getProviderForDevice`, so a camera outside the caller's scope is refused
38490
+ * before a provider is reached.
38491
+ */
38492
+ getAvailability: method(object({
38493
+ deviceId: number(),
38494
+ /**
38495
+ * WHICH provider to ask — the `addonId` a {@link RecordingSourceSchema}
38496
+ * row carries, never a source id and never a list. **REQUIRED**, in the
38497
+ * schema, where the generated types make it unomittable rather than
38498
+ * merely discouraged (D554 amended).
38499
+ *
38500
+ * It was learned the expensive way on `videoclips.listClips`: measured
38501
+ * on the live hub 2026-09-20, device 592 bound to `recorder` AND
38502
+ * `provider-reolink`, a bare call with `limit: 3` answered SIX rows,
38503
+ * three from each source, merged — `device-collection-dispatch.ts`
38504
+ * leaves an unpinned fan-out un-narrowed, so absence buys the union the
38505
+ * method exists not to be. An un-narrowed `getAvailability` would do
38506
+ * that to a TIMELINE: our ranges and the card's clips unioned into one
38507
+ * bar, which is "two sources are never drawn together" broken in the
38508
+ * one place it matters most.
38509
+ *
38510
+ * A provider the device is not bound to is refused BY NAME (D552's
38511
+ * `rejectUnresolvedAddonPin`), never answered by another one.
38512
+ */
38513
+ provider: string().min(1),
38514
+ fromMs: number(),
38515
+ toMs: number(),
38516
+ /**
38517
+ * Answer for THIS profile instead of the source's preferred one (D433).
38518
+ * Absent keeps the timeline's behaviour — one bar, one profile, one set
38519
+ * of reads. `profilesWithFootage` on the answer says what may be asked
38520
+ * for.
38521
+ */
38522
+ profile: string().optional()
38523
+ }), RecordingAvailabilitySchema, {
38524
+ kind: "query",
38525
+ auth: "protected"
38526
+ }),
38527
+ /** Which calendar days in [fromMs,toMs) this source has ≥1 recording in,
38528
+ * bucketed by the client's local day (`tzOffsetMinutes` = minutes to add
38529
+ * to UTC). Drives the theater date-picker's day dots. `provider` is
38530
+ * REQUIRED for the reason `getAvailability` states. */
38531
+ getDaysWithRecordings: method(object({
38532
+ deviceId: number(),
38533
+ provider: string().min(1),
38534
+ fromMs: number(),
38535
+ toMs: number(),
38536
+ tzOffsetMinutes: number()
38537
+ }), RecordingDaysSchema, {
38538
+ kind: "query",
38539
+ auth: "protected"
38540
+ }),
38541
+ /**
38542
+ * How to PLAY `[fromMs, toMs)` at this source.
38543
+ *
38544
+ * It was `getPlaybackManifest`, and the rename is not cosmetic: a
38545
+ * "manifest" is an HLS master playlist, which is a property of OUR recorder
38546
+ * and of nothing else. Keeping the name would make every onboard
38547
+ * implementation a lie in its signature. The old shape survives verbatim
38548
+ * inside the union's `hls` arm, so the recorder's implementation is
38549
+ * unchanged behind it.
38550
+ */
38551
+ getPlayback: method(object({
38552
+ deviceId: number(),
38553
+ provider: string().min(1),
38554
+ fromMs: number(),
38555
+ toMs: number(),
38556
+ profile: CamProfileSchema.optional()
38557
+ }), RecordingPlaybackSchema, {
38558
+ kind: "query",
38559
+ auth: "protected"
38560
+ }),
38561
+ /**
38562
+ * What this (camera, source) can actually DO — asked before anything is
38563
+ * drawn. See {@link RecordingSourceOptionsSchema}; a constant the surface
38564
+ * keeps instead is the second authority D612 exists to end.
38565
+ */
38566
+ getPlaybackOptions: method(object({
38567
+ deviceId: number(),
38568
+ provider: string().min(1)
38569
+ }), RecordingSourceOptionsSchema, {
38570
+ kind: "query",
38571
+ auth: "protected"
38572
+ })
38573
+ }
38574
+ };
38575
+ /**
38576
+ * `recording-archive` — OUR archive, and the intent that fills it.
38577
+ *
38578
+ * The system-singleton half of the 2026-09-24 cut (D625). `recording` used to
38579
+ * be one 33-method system singleton holding two unrelated subjects: three
38580
+ * per-camera READS about coverage and playback, and everything else — storage
38581
+ * locations, retention, relocation, rebalance, the ops log, the placement
38582
+ * table and the byte-plane primitives our scrub and export are built on.
38583
+ *
38584
+ * The reads became a device-scoped COLLECTION, so a camera's own card can be a
38585
+ * source beside ours (`recording.cap.ts`). Everything that is about OUR store,
38586
+ * or unimplementable by a camera, stayed here.
38587
+ *
38588
+ * ## On the name
38589
+ *
38590
+ * `recording-storage` was the obvious choice and is wrong: this cap also holds
38591
+ * `getDeviceConfig`/`setDeviceConfig`, which are recording INTENT — bands,
38592
+ * retention, the D62 switch authority — and a name that says "storage" invites
38593
+ * the next reader to move them out again. An archive is a thing we keep, and
38594
+ * what we keep it under is a policy; the name covers both halves honestly and
38595
+ * sits in the existing family (`recording-onboard`, `recording-export`,
38596
+ * `recording-signal`).
38597
+ *
38598
+ * ## What must NOT happen to it
38599
+ *
38600
+ * It stays a SINGLETON. It is registered by `recorder`, which is
38601
+ * `placement: 'any-node'` and runs on every recording node; the hub dispatches
38602
+ * to one of them. Putting the ledger, the placement table or the relocation
38603
+ * jobs behind a fan-out is the one genuinely dangerous move in this cut.
38604
+ *
38605
+ * `getDeviceConfig` / `setDeviceConfig` in particular are the D62 recording
38606
+ * authority (`CameraSwitch.authority`). If a write reached a different provider
38607
+ * than the read — which a collection fan-out permits — two authorities would
38608
+ * decide when one camera records, and the symptom (recording silently off, or
38609
+ * a `bands` array clobbered by a partial write) is durable and silent. Keeping
38610
+ * them here means the worst case during a rollout is a 412: the switch refuses
38611
+ * to flip and SAYS so. **Do not move them into the collection, at any point,
38612
+ * for any reason.**
38613
+ *
38614
+ * ## The two batch reads
38615
+ *
38616
+ * `getAvailabilityBatch` / `getDaysWithRecordingsBatch` take `deviceIds:
38617
+ * number[]` with no single `deviceId`, and a device-scoped mount routes
38618
+ * through `getProviderForDevice(deviceId)` — there is nothing for it to route
38619
+ * on. They stay here, and on this cap the batch is explicitly OURS: a grid has
38620
+ * no per-camera picker, and a caller that wants another source's coverage asks
38621
+ * `recording.getAvailability` per device with that source's `provider`.
38622
+ */
38623
+ var RecordingStatusSchema = object({
38624
+ deviceId: number(),
38625
+ enabled: boolean(),
38626
+ /** THE derived storage mode, from the one definition
38627
+ * (`deriveRecordingMode`) — never a second enum. A duplicated list is how
38628
+ * `on-device-decision` could have reached the recorder and not the status. */
38629
+ activeMode: RecordingStorageModeSchema,
38630
+ nodeId: string(),
38631
+ storageBytes: number()
38632
+ });
38465
38633
  /**
38466
38634
  * One camera's row in a `getAvailabilityBatch` answer.
38467
38635
  *
38468
- * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
38469
- * batch collapses the transport, not the work — plus the one thing the singular
38470
- * method never had to say:
38636
+ * `ranges` is EXACTLY what `recording.getAvailability` returns for that camera
38637
+ * at OUR source — the batch collapses the transport, not the work — plus the
38638
+ * `read` mark the singular answer now carries too (D625):
38471
38639
  *
38472
38640
  * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
38473
38641
  * no footage in the window", which is a real claim.
@@ -38499,22 +38667,6 @@ var RecordingDaysForDeviceSchema = object({
38499
38667
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
38500
38668
  days: array(number()).readonly()
38501
38669
  });
38502
- var RecordingManifestSchema = object({
38503
- deviceId: number(),
38504
- /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
38505
- localMasterPath: string().nullable(),
38506
- /** HTTP(S) URL to the master playlist on the recording node's playback server
38507
- * (the PRIMARY candidate); null when no recording / server. Carries the
38508
- * scoped playback token in its path. */
38509
- playbackUrl: string().nullable(),
38510
- /**
38511
- * Candidate master-playlist URLs the client tries in order (LAN first, then
38512
- * remote — Tailscale/Cloudflare if the operator configured extra hosts), each
38513
- * carrying the same scoped token. `playbackUrl` is the first entry. Empty when
38514
- * there is no recording / server.
38515
- */
38516
- playbackEndpoints: array(string())
38517
- });
38518
38670
  /**
38519
38671
  * Recording storage usage for one camera — what the ARCHIVE holds for it,
38520
38672
  * across every profile and every resolvable location on this node.
@@ -38772,33 +38924,22 @@ var ReadWindowBytesResultSchema = discriminatedUnion("kind", [object({
38772
38924
  * not a retry hint (retrying the same window would refuse again). */
38773
38925
  segmentEndMs: number()
38774
38926
  })]);
38775
- var recordingCapability = {
38776
- name: "recording",
38927
+ var recordingArchiveCapability = {
38928
+ name: "recording-archive",
38777
38929
  scope: "system",
38778
38930
  mode: "singleton",
38931
+ /** Moves here with the config pair — the derived Recording settings
38932
+ * section is a view over `getDeviceConfig`/`setDeviceConfig` (D14), and
38933
+ * those did not move (D625). */
38779
38934
  exposesDeviceSettings: true,
38780
38935
  status: {
38781
38936
  schema: RecordingStatusSchema,
38782
38937
  kind: "command-driven"
38783
38938
  },
38784
38939
  methods: {
38785
- getAvailability: method(object({
38786
- deviceId: number(),
38787
- fromMs: number(),
38788
- toMs: number(),
38789
- /**
38790
- * Answer for THIS profile instead of the preferred one (D433). Absent
38791
- * keeps the timeline's behaviour — one bar, one profile, one set of
38792
- * reads. `profilesWithFootage` on the answer says what may be asked
38793
- * for.
38794
- */
38795
- profile: string().optional()
38796
- }), RecordingAvailabilitySchema, {
38797
- kind: "query",
38798
- auth: "protected"
38799
- }),
38800
38940
  /**
38801
- * `getAvailability` for a SET of cameras, in one round trip.
38941
+ * `recording.getAvailability` for a SET of cameras, in one round trip, at
38942
+ * OUR source.
38802
38943
  *
38803
38944
  * A multi-camera timeline re-asks availability for every camera in the grid
38804
38945
  * on every day change; fanned out that is one request per camera for N
@@ -38806,6 +38947,13 @@ var recordingCapability = {
38806
38947
  * `availabilityProfileFor` + `rangesIn`, run concurrently inside the
38807
38948
  * recorder) — only the transport collapses.
38808
38949
  *
38950
+ * It lives on the ARCHIVE, not on the `recording` collection, because it
38951
+ * takes `deviceIds` with no single `deviceId` and a device-scoped mount has
38952
+ * nothing to route on (D625 §10.2). The consequence is stated rather than
38953
+ * hidden: the batch answers for OURS. A caller that needs another source's
38954
+ * coverage asks `recording.getAvailability` per device, naming that
38955
+ * source's provider.
38956
+ *
38809
38957
  * `protected` for the same reason the singular method is: every id in
38810
38958
  * `deviceIds` is a device reference, so the F1 #3 gate refuses any camera
38811
38959
  * outside the caller's scope — one id out of scope refuses the CALL, it
@@ -38823,20 +38971,10 @@ var recordingCapability = {
38823
38971
  kind: "query",
38824
38972
  auth: "protected"
38825
38973
  }),
38826
- /** Which calendar days in [fromMs,toMs) have ≥1 recorded segment, bucketed by
38827
- * the client's local day (`tzOffsetMinutes` = minutes to add to UTC). Drives
38828
- * the theater date-picker's day dots. */
38829
- getDaysWithRecordings: method(object({
38830
- deviceId: number(),
38831
- fromMs: number(),
38832
- toMs: number(),
38833
- tzOffsetMinutes: number()
38834
- }), RecordingDaysSchema, {
38835
- kind: "query",
38836
- auth: "protected"
38837
- }),
38838
38974
  /**
38839
- * `getDaysWithRecordings` for a SET of cameras, in one round trip.
38975
+ * `recording.getDaysWithRecordings` for a SET of cameras, in one round
38976
+ * trip, at OUR source. Same placement argument as
38977
+ * {@link getAvailabilityBatch}.
38840
38978
  *
38841
38979
  * The cheapest question in the product, asked once per camera per month
38842
38980
  * change. One directory read per day per camera at the owner, unchanged;
@@ -38855,14 +38993,6 @@ var recordingCapability = {
38855
38993
  kind: "query",
38856
38994
  auth: "protected"
38857
38995
  }),
38858
- getPlaybackManifest: method(object({
38859
- deviceId: number(),
38860
- fromMs: number(),
38861
- toMs: number()
38862
- }), RecordingManifestSchema, {
38863
- kind: "query",
38864
- auth: "protected"
38865
- }),
38866
38996
  getStorageUsage: method(object({}), RecordingStorageUsageSchema, {
38867
38997
  kind: "query",
38868
38998
  auth: "admin"
@@ -38878,6 +39008,10 @@ var recordingCapability = {
38878
39008
  * value (D315, D393, D590). It carries schedules and retention, no secret,
38879
39009
  * and the per-device gate in `scope-access.ts` still applies; `setDeviceConfig`
38880
39010
  * stays `admin`.
39011
+ *
39012
+ * It is a SINGLETON method and stays one (D625 §16.2): it is the D62
39013
+ * recording authority, and an authority that several providers could answer
39014
+ * is the "two knobs over one decision" D62 forbids.
38881
39015
  */
38882
39016
  getDeviceConfig: method(object({ deviceId: number() }), RecordingConfigSchema, {
38883
39017
  kind: "query",
@@ -38885,7 +39019,13 @@ var recordingCapability = {
38885
39019
  }),
38886
39020
  /** Locate footage at a wall-clock instant: the covering segment's window,
38887
39021
  * or a gap with the forward nearest covered edge. Used by a feeder running
38888
- * in another addon process to seek recorded footage over tRPC. */
39022
+ * in another addon process to seek recorded footage over tRPC.
39023
+ *
39024
+ * BYTE PLANE: defined by `mfra` byte ranges over OUR own MP4 segment tree.
39025
+ * There is no vendor-neutral statement of it, its only callers are feeders
39026
+ * in other addon processes (the stream broker, the replay-clip source),
39027
+ * and an onboard source that ever needs bytes delegates to the
39028
+ * `videoclips` transport its vendor already has (D625 §9.3, §10.5). */
38889
39029
  locateSegment: method(object({
38890
39030
  deviceId: number(),
38891
39031
  profile: string(),
@@ -39500,6 +39640,346 @@ var recordingExportCapability = {
39500
39640
  }
39501
39641
  };
39502
39642
  /**
39643
+ * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
39644
+ * writes to the CAMERA's own card, on the camera's own schedule.
39645
+ *
39646
+ * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
39647
+ * footage ledger, our storage locations, our retention. This one has a
39648
+ * different authority — the camera's firmware — and per D62 it stores
39649
+ * nothing of its own. Every value here is read from the camera and every
39650
+ * write goes back to the camera; there is no CamStack-side mirror that
39651
+ * could disagree with the device.
39652
+ *
39653
+ * ## One shape, two firmwares
39654
+ *
39655
+ * Measured 2026-09-22 against the live fleet:
39656
+ *
39657
+ * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
39658
+ * | --- | --- | --- |
39659
+ * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
39660
+ * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
39661
+ * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
39662
+ * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
39663
+ * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
39664
+ * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
39665
+ * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
39666
+ * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
39667
+ *
39668
+ * The two schedule models look different and are the same thing in
39669
+ * different coordinates: both answer "for this trigger, during which
39670
+ * weekly windows does the camera record". {@link RecordWindow} is that
39671
+ * question in one shape — Hikvision's ranges map straight onto it,
39672
+ * Reolink's mask expands into hour-aligned windows.
39673
+ *
39674
+ * ## Union, not intersection
39675
+ *
39676
+ * **The same fields exist on every camera.** What differs per device is
39677
+ * which VALUES that device accepts, and that is what {@link
39678
+ * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
39679
+ * per field plus the schedule's own limits. A control a camera cannot
39680
+ * honour is rendered DISABLED WITH ITS REASON, never missing and never
39681
+ * dead: disabled must not look like broken.
39682
+ *
39683
+ * ## Refusal by name
39684
+ *
39685
+ * A write a camera cannot honour is refused with a sentence the operator
39686
+ * can read — never accepted and dropped. Both providers refuse through
39687
+ * {@link describeOnboardRefusal}, so the vocabulary is one function and
39688
+ * one test, not two hand-written vendor opinions.
39689
+ *
39690
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
39691
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
39692
+ * injected from `status`) reports the live values, and a single
39693
+ * `setSettings` mutation applies a partial change. No hand-written
39694
+ * settings-contribution methods.
39695
+ */
39696
+ /**
39697
+ * What makes the camera start recording during a window.
39698
+ *
39699
+ * The union of both vendors' vocabularies. `continuous` is Hikvision's
39700
+ * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
39701
+ * object-class triggers are Reolink-only today and the smart-event ones
39702
+ * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
39703
+ * firmwares measured — a camera that cannot record on a trigger simply
39704
+ * does not list it in `options.schedule.triggers`, and a window naming
39705
+ * it is REFUSED, not dropped.
39706
+ */
39707
+ var RecordTriggerSchema = _enum([
39708
+ "continuous",
39709
+ "motion",
39710
+ "person",
39711
+ "vehicle",
39712
+ "animal",
39713
+ "lineCrossing",
39714
+ "intrusion",
39715
+ "loitering",
39716
+ "alarmInput"
39717
+ ]);
39718
+ /**
39719
+ * One weekly recording window: "on `day`, from `startMinute` to
39720
+ * `endMinute`, record on `trigger`".
39721
+ *
39722
+ * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
39723
+ * both firmwares enumerate). Minutes are local camera time since
39724
+ * midnight; `endMinute` may be 1440, meaning end of day — that is
39725
+ * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
39726
+ * collapsing it to 0 would turn a whole-day window into an empty one.
39727
+ */
39728
+ var RecordWindowSchema = object({
39729
+ trigger: RecordTriggerSchema,
39730
+ day: number().int().min(0).max(6),
39731
+ startMinute: number().int().min(0).max(1439),
39732
+ endMinute: number().int().min(1).max(1440)
39733
+ });
39734
+ /** Status of one physical volume, as the camera itself describes it. */
39735
+ var OnboardStorageVolumeSchema = object({
39736
+ /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
39737
+ id: string(),
39738
+ /** The camera's own name for it, when it gives one (`hddName`). */
39739
+ label: string().optional(),
39740
+ status: _enum([
39741
+ "ok",
39742
+ "unformatted",
39743
+ "error",
39744
+ "offline",
39745
+ "unknown"
39746
+ ]),
39747
+ /**
39748
+ * Total size in MB, or **null when the camera did not say**.
39749
+ *
39750
+ * Never 0 for an unreadable value: a measurement that failed is not a
39751
+ * measurement (D393), and a card whose size is unknown must not be
39752
+ * rendered as a card of size zero.
39753
+ */
39754
+ capacityMb: number().nullable(),
39755
+ /**
39756
+ * Free space in MB, or null when unknown.
39757
+ *
39758
+ * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
39759
+ * 1439 both report exactly 11776 MB free — the fixed reserve a looping
39760
+ * card converges on once it has wrapped. At loop steady state the
39761
+ * number is identical whether the camera recorded yesterday or stopped
39762
+ * a month ago.
39763
+ */
39764
+ freeMb: number().nullable(),
39765
+ /** True when the camera reports the volume writable (`property` RW). */
39766
+ writable: boolean().optional()
39767
+ });
39768
+ /**
39769
+ * What the camera is doing with its own storage, right now.
39770
+ *
39771
+ * Every scalar is nullable and **null means the camera did not answer**,
39772
+ * never a default. A form that seeds `0` from an unanswered read invites
39773
+ * the operator to save that 0 back onto the camera.
39774
+ */
39775
+ var RecordingOnboardStatusSchema = object({
39776
+ storage: discriminatedUnion("kind", [
39777
+ object({
39778
+ kind: literal("present"),
39779
+ volumes: array(OnboardStorageVolumeSchema)
39780
+ }),
39781
+ object({
39782
+ kind: literal("absent"),
39783
+ reason: string()
39784
+ }),
39785
+ object({
39786
+ kind: literal("unknown"),
39787
+ reason: string()
39788
+ })
39789
+ ]),
39790
+ tracks: array(object({
39791
+ id: string(),
39792
+ enabled: boolean(),
39793
+ isVideo: boolean(),
39794
+ /** From the camera's own track description. Null when it does not say. */
39795
+ codec: string().nullable(),
39796
+ resolution: string().nullable(),
39797
+ /** Per-track overwrite flag, where the firmware keeps it per track. */
39798
+ overwriteWhenFull: boolean().nullable()
39799
+ })),
39800
+ /**
39801
+ * The track the write path targets — the enabled VIDEO one. Null when
39802
+ * no track could be identified, which is itself a refusal reason.
39803
+ */
39804
+ primaryTrackId: string().nullable(),
39805
+ /** Master "record to the card at all" switch. */
39806
+ enabled: boolean().nullable(),
39807
+ overwriteWhenFull: boolean().nullable(),
39808
+ preRecordSec: number().nullable(),
39809
+ postRecordSec: number().nullable(),
39810
+ /** Length of one recorded file, in minutes. */
39811
+ segmentMinutes: number().nullable(),
39812
+ /** The primary track's weekly windows, flattened. */
39813
+ windows: array(RecordWindowSchema),
39814
+ /**
39815
+ * How many windows the camera described that CamStack could NOT read —
39816
+ * an unrecognised trigger, an unparseable clock, a weekday it does not
39817
+ * name.
39818
+ *
39819
+ * A dropped window is work the reader threw away, and a schedule that
39820
+ * silently shows fewer rows than the camera holds is how an operator
39821
+ * saves back a schedule shorter than the one they were looking at
39822
+ * (D391). Non-zero means the window list is INCOMPLETE and a write
39823
+ * that replaces it would delete what was not shown — which is why a
39824
+ * provider reporting a non-zero count also reports the schedule as not
39825
+ * writable.
39826
+ */
39827
+ unreadableWindows: number(),
39828
+ /**
39829
+ * The camera is scheduled to record and has NO usable storage.
39830
+ *
39831
+ * A first-class fact because it is the fleet's most common silent
39832
+ * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
39833
+ * to a card that is not there. Neither the schedule nor the storage
39834
+ * read says anything wrong on its own; only the pair does.
39835
+ */
39836
+ recordingToNowhere: boolean(),
39837
+ lastFetchedAt: number()
39838
+ });
39839
+ /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
39840
+ var RangeSchema = object({
39841
+ min: number(),
39842
+ max: number(),
39843
+ step: number()
39844
+ });
39845
+ /**
39846
+ * The values a camera actually takes for a numeric field, when they are a SET
39847
+ * rather than a range.
39848
+ *
39849
+ * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
39850
+ * (I91DN) on 2026-09-22 by writing each value and reading it back:
39851
+ *
39852
+ * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
39853
+ * camera's "no limit" — `-1` and `4294967295` both land on it);
39854
+ * - post-record: `5, 10, 30, 60, 120, 300, 600`.
39855
+ *
39856
+ * Neither is expressible as a step: the first has a sentinel two billion away
39857
+ * from its neighbours, the second doubles and then jumps. A range that tried
39858
+ * would forbid values the camera takes AND permit values it silently replaces
39859
+ * with 5 — wrong in both directions at once.
39860
+ *
39861
+ * `sentinel` names the member that is not a duration, so a surface can render
39862
+ * "no limit" instead of `2147483647` seconds.
39863
+ */
39864
+ var AllowedValuesSchema = object({
39865
+ values: array(number()).min(1),
39866
+ sentinel: object({
39867
+ value: number(),
39868
+ meaning: _enum(["no-limit", "disabled"])
39869
+ }).optional()
39870
+ });
39871
+ /**
39872
+ * Per-field availability on ONE camera.
39873
+ *
39874
+ * The field exists on every camera — this says whether this one can be
39875
+ * read and whether it can be written, and `reason` says why not when
39876
+ * either is false. The UI renders the control DISABLED with the reason
39877
+ * rather than hiding it, so a limitation is legible instead of looking
39878
+ * like a missing feature.
39879
+ */
39880
+ var OnboardFieldSupportSchema = object({
39881
+ readable: boolean(),
39882
+ writable: boolean(),
39883
+ /** Required whenever `readable` or `writable` is false. */
39884
+ reason: string().optional()
39885
+ });
39886
+ /** What this camera's schedule model can express. */
39887
+ var OnboardScheduleSupportSchema = object({
39888
+ support: OnboardFieldSupportSchema,
39889
+ /**
39890
+ * The smallest time step the camera can express, in minutes.
39891
+ *
39892
+ * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
39893
+ * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
39894
+ * window whose edges are not a multiple of this is REFUSED rather than
39895
+ * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
39896
+ * and nothing says so.
39897
+ */
39898
+ granularityMinutes: number(),
39899
+ /** Triggers this camera can record on. A window naming another is refused. */
39900
+ triggers: array(RecordTriggerSchema),
39901
+ /**
39902
+ * False when the camera stores ONE trigger per time range, so two
39903
+ * windows overlapping on the same day cannot carry different triggers.
39904
+ * True on Reolink, whose mask is per-trigger and independent.
39905
+ */
39906
+ supportsOverlappingTriggers: boolean()
39907
+ });
39908
+ var RecordingOnboardOptionsSchema = object({
39909
+ enabled: OnboardFieldSupportSchema,
39910
+ overwriteWhenFull: OnboardFieldSupportSchema,
39911
+ preRecordSec: OnboardFieldSupportSchema,
39912
+ preRecordSecRange: RangeSchema.optional(),
39913
+ /** Preferred over the range when the camera takes a SET, not a span. */
39914
+ preRecordSecAllowed: AllowedValuesSchema.optional(),
39915
+ postRecordSec: OnboardFieldSupportSchema,
39916
+ postRecordSecRange: RangeSchema.optional(),
39917
+ /** Preferred over the range when the camera takes a SET, not a span. */
39918
+ postRecordSecAllowed: AllowedValuesSchema.optional(),
39919
+ segmentMinutes: OnboardFieldSupportSchema,
39920
+ segmentMinutesRange: RangeSchema.optional(),
39921
+ /** Preferred over the range when the camera takes a SET, not a span. */
39922
+ segmentMinutesAllowed: AllowedValuesSchema.optional(),
39923
+ schedule: OnboardScheduleSupportSchema
39924
+ });
39925
+ /**
39926
+ * A partial change. Every field optional.
39927
+ *
39928
+ * Unlike the other `deviceConfig` caps, a provider here does **NOT**
39929
+ * silently ignore a field it cannot support — it refuses, by name,
39930
+ * through {@link describeOnboardRefusal}. Silence on a recording setting
39931
+ * is the failure D62 exists to prevent: the operator believes the camera
39932
+ * is recording the way the form says, and it is not.
39933
+ */
39934
+ var RecordingOnboardPatchSchema = object({
39935
+ enabled: boolean().optional(),
39936
+ overwriteWhenFull: boolean().optional(),
39937
+ preRecordSec: number().optional(),
39938
+ postRecordSec: number().optional(),
39939
+ segmentMinutes: number().optional(),
39940
+ /** The complete new window set for the primary track — not a delta. */
39941
+ windows: array(RecordWindowSchema).optional()
39942
+ });
39943
+ var recordingOnboardCapability = {
39944
+ name: "recording-onboard",
39945
+ scope: "device",
39946
+ deviceNative: true,
39947
+ mode: "singleton",
39948
+ deviceTypes: [DeviceType.Camera],
39949
+ deviceConfig: { ui: {
39950
+ kind: "derived-form",
39951
+ builderId: "recording-onboard",
39952
+ tab: "recording"
39953
+ } },
39954
+ methods: {
39955
+ getOptions: method(object({ deviceId: number() }), RecordingOnboardOptionsSchema),
39956
+ setSettings: method(object({
39957
+ deviceId: number(),
39958
+ settings: RecordingOnboardPatchSchema
39959
+ }), _void(), {
39960
+ kind: "mutation",
39961
+ auth: "admin"
39962
+ })
39963
+ },
39964
+ status: {
39965
+ schema: RecordingOnboardStatusSchema,
39966
+ kind: "poll"
39967
+ },
39968
+ runtimeState: RecordingOnboardStatusSchema,
39969
+ /**
39970
+ * Runtime-state durability: **restored** — operator-set camera-side
39971
+ * recording config; mutation-driven, and the storage half is the last
39972
+ * thing the camera said about its own card.
39973
+ *
39974
+ * See `RuntimeStateDurability`. Enforced by
39975
+ * `scripts/check-runtime-state-durability.ts`.
39976
+ */
39977
+ durability: "restored",
39978
+ /** Clock fields: written, but excluded from the compare that decides
39979
+ * whether persisting is worth a SQLite commit. */
39980
+ volatileStateFields: ["lastFetchedAt"]
39981
+ };
39982
+ /**
39503
39983
  * A camera's own "record me NOW" LEVEL — a signal the device raises while
39504
39984
  * something it knows about is happening (a robot vacuum cleaning, a machine
39505
39985
  * running, a gate open) and lowers when it stops.
@@ -42115,9 +42595,8 @@ var DEVICE_STATE_READERS = [
42115
42595
  field: "state"
42116
42596
  },
42117
42597
  {
42118
- cap: "lock",
42119
- field: "locked",
42120
- booleanWords: ["locked", "unlocked"]
42598
+ cap: "lock-control",
42599
+ field: "state"
42121
42600
  },
42122
42601
  {
42123
42602
  cap: "contact",
@@ -43583,7 +44062,6 @@ var ALL_CAPABILITY_DEFINITIONS = [
43583
44062
  embeddingEncoderCapability,
43584
44063
  enumSensorCapability,
43585
44064
  eventEmitterCapability,
43586
- eventsCapability,
43587
44065
  faceGalleryCapability,
43588
44066
  failureContributionCapability,
43589
44067
  fanControlCapability,
@@ -43644,6 +44122,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
43644
44122
  ptzAutotrackCapability,
43645
44123
  rebootCapability,
43646
44124
  recordingCapability,
44125
+ recordingArchiveCapability,
43647
44126
  recordingExportCapability,
43648
44127
  recordingOnboardCapability,
43649
44128
  recordingSignalCapability,
@@ -45878,24 +46357,6 @@ Object.freeze({
45878
46357
  addonId: null,
45879
46358
  access: "view"
45880
46359
  },
45881
- "events.getEventClipUrl": {
45882
- capName: "events",
45883
- capScope: "device",
45884
- addonId: null,
45885
- access: "view"
45886
- },
45887
- "events.getEvents": {
45888
- capName: "events",
45889
- capScope: "device",
45890
- addonId: null,
45891
- access: "view"
45892
- },
45893
- "events.getEventThumbnail": {
45894
- capName: "events",
45895
- capScope: "device",
45896
- addonId: null,
45897
- access: "view"
45898
- },
45899
46360
  "faceGallery.assignFace": {
45900
46361
  capName: "face-gallery",
45901
46362
  capScope: "system",
@@ -47216,6 +47677,12 @@ Object.freeze({
47216
47677
  addonId: null,
47217
47678
  access: "view"
47218
47679
  },
47680
+ "notificationRules.getTemplateCatalog": {
47681
+ capName: "notification-rules",
47682
+ capScope: "system",
47683
+ addonId: null,
47684
+ access: "view"
47685
+ },
47219
47686
  "notificationRules.listDeviceMutes": {
47220
47687
  capName: "notification-rules",
47221
47688
  capScope: "system",
@@ -47234,6 +47701,12 @@ Object.freeze({
47234
47701
  addonId: null,
47235
47702
  access: "view"
47236
47703
  },
47704
+ "notificationRules.previewTemplate": {
47705
+ capName: "notification-rules",
47706
+ capScope: "system",
47707
+ addonId: null,
47708
+ access: "view"
47709
+ },
47237
47710
  "notificationRules.resolveArtifactUrl": {
47238
47711
  capName: "notification-rules",
47239
47712
  capScope: "system",
@@ -48770,224 +49243,236 @@ Object.freeze({
48770
49243
  addonId: null,
48771
49244
  access: "create"
48772
49245
  },
48773
- "recording.applyDeviceSettingsPatch": {
49246
+ "recording.getAvailability": {
48774
49247
  capName: "recording",
48775
- capScope: "system",
49248
+ capScope: "device",
48776
49249
  addonId: null,
48777
- access: "create"
49250
+ access: "view"
48778
49251
  },
48779
- "recording.cancelRelocateJob": {
49252
+ "recording.getDaysWithRecordings": {
48780
49253
  capName: "recording",
48781
- capScope: "system",
49254
+ capScope: "device",
48782
49255
  addonId: null,
48783
- access: "create"
49256
+ access: "view"
48784
49257
  },
48785
- "recording.cancelStorageMigrationMove": {
49258
+ "recording.getPlayback": {
48786
49259
  capName: "recording",
48787
- capScope: "system",
49260
+ capScope: "device",
48788
49261
  addonId: null,
48789
- access: "create"
49262
+ access: "view"
48790
49263
  },
48791
- "recording.deleteFootprint": {
49264
+ "recording.getPlaybackOptions": {
48792
49265
  capName: "recording",
48793
- capScope: "system",
49266
+ capScope: "device",
48794
49267
  addonId: null,
48795
- access: "delete"
49268
+ access: "view"
48796
49269
  },
48797
- "recording.getAvailability": {
49270
+ "recording.listSources": {
48798
49271
  capName: "recording",
48799
- capScope: "system",
49272
+ capScope: "device",
48800
49273
  addonId: null,
48801
49274
  access: "view"
48802
49275
  },
48803
- "recording.getAvailabilityBatch": {
48804
- capName: "recording",
49276
+ "recordingArchive.applyDeviceSettingsPatch": {
49277
+ capName: "recording-archive",
48805
49278
  capScope: "system",
48806
49279
  addonId: null,
48807
- access: "view"
49280
+ access: "create"
48808
49281
  },
48809
- "recording.getDaysWithRecordings": {
48810
- capName: "recording",
49282
+ "recordingArchive.cancelRelocateJob": {
49283
+ capName: "recording-archive",
48811
49284
  capScope: "system",
48812
49285
  addonId: null,
48813
- access: "view"
49286
+ access: "create"
48814
49287
  },
48815
- "recording.getDaysWithRecordingsBatch": {
48816
- capName: "recording",
49288
+ "recordingArchive.cancelStorageMigrationMove": {
49289
+ capName: "recording-archive",
49290
+ capScope: "system",
49291
+ addonId: null,
49292
+ access: "create"
49293
+ },
49294
+ "recordingArchive.deleteFootprint": {
49295
+ capName: "recording-archive",
49296
+ capScope: "system",
49297
+ addonId: null,
49298
+ access: "delete"
49299
+ },
49300
+ "recordingArchive.getAvailabilityBatch": {
49301
+ capName: "recording-archive",
48817
49302
  capScope: "system",
48818
49303
  addonId: null,
48819
49304
  access: "view"
48820
49305
  },
48821
- "recording.getDeviceConfig": {
48822
- capName: "recording",
49306
+ "recordingArchive.getDaysWithRecordingsBatch": {
49307
+ capName: "recording-archive",
48823
49308
  capScope: "system",
48824
49309
  addonId: null,
48825
49310
  access: "view"
48826
49311
  },
48827
- "recording.getDeviceLiveContribution": {
48828
- capName: "recording",
49312
+ "recordingArchive.getDeviceConfig": {
49313
+ capName: "recording-archive",
48829
49314
  capScope: "system",
48830
49315
  addonId: null,
48831
49316
  access: "view"
48832
49317
  },
48833
- "recording.getDeviceSettingsContribution": {
48834
- capName: "recording",
49318
+ "recordingArchive.getDeviceLiveContribution": {
49319
+ capName: "recording-archive",
48835
49320
  capScope: "system",
48836
49321
  addonId: null,
48837
49322
  access: "view"
48838
49323
  },
48839
- "recording.getPlacement": {
48840
- capName: "recording",
49324
+ "recordingArchive.getDeviceSettingsContribution": {
49325
+ capName: "recording-archive",
48841
49326
  capScope: "system",
48842
49327
  addonId: null,
48843
49328
  access: "view"
48844
49329
  },
48845
- "recording.getPlaybackManifest": {
48846
- capName: "recording",
49330
+ "recordingArchive.getPlacement": {
49331
+ capName: "recording-archive",
48847
49332
  capScope: "system",
48848
49333
  addonId: null,
48849
49334
  access: "view"
48850
49335
  },
48851
- "recording.getRelocateResidue": {
48852
- capName: "recording",
49336
+ "recordingArchive.getRelocateResidue": {
49337
+ capName: "recording-archive",
48853
49338
  capScope: "system",
48854
49339
  addonId: null,
48855
49340
  access: "view"
48856
49341
  },
48857
- "recording.getStatus": {
48858
- capName: "recording",
49342
+ "recordingArchive.getStatus": {
49343
+ capName: "recording-archive",
48859
49344
  capScope: "system",
48860
49345
  addonId: null,
48861
49346
  access: "view"
48862
49347
  },
48863
- "recording.getStorageMigrationMoveStatus": {
48864
- capName: "recording",
49348
+ "recordingArchive.getStorageMigrationMoveStatus": {
49349
+ capName: "recording-archive",
48865
49350
  capScope: "system",
48866
49351
  addonId: null,
48867
49352
  access: "view"
48868
49353
  },
48869
- "recording.getStorageUsage": {
48870
- capName: "recording",
49354
+ "recordingArchive.getStorageUsage": {
49355
+ capName: "recording-archive",
48871
49356
  capScope: "system",
48872
49357
  addonId: null,
48873
49358
  access: "view"
48874
49359
  },
48875
- "recording.listOpsLog": {
48876
- capName: "recording",
49360
+ "recordingArchive.listOpsLog": {
49361
+ capName: "recording-archive",
48877
49362
  capScope: "system",
48878
49363
  addonId: null,
48879
49364
  access: "view"
48880
49365
  },
48881
- "recording.listRelocateJobs": {
48882
- capName: "recording",
49366
+ "recordingArchive.listRelocateJobs": {
49367
+ capName: "recording-archive",
48883
49368
  capScope: "system",
48884
49369
  addonId: null,
48885
49370
  access: "view"
48886
49371
  },
48887
- "recording.locateSegment": {
48888
- capName: "recording",
49372
+ "recordingArchive.locateSegment": {
49373
+ capName: "recording-archive",
48889
49374
  capScope: "system",
48890
49375
  addonId: null,
48891
49376
  access: "view"
48892
49377
  },
48893
- "recording.pauseForStorageMigration": {
48894
- capName: "recording",
49378
+ "recordingArchive.pauseForStorageMigration": {
49379
+ capName: "recording-archive",
48895
49380
  capScope: "system",
48896
49381
  addonId: null,
48897
49382
  access: "create"
48898
49383
  },
48899
- "recording.planStorageRebalance": {
48900
- capName: "recording",
49384
+ "recordingArchive.planStorageRebalance": {
49385
+ capName: "recording-archive",
48901
49386
  capScope: "system",
48902
49387
  addonId: null,
48903
49388
  access: "view"
48904
49389
  },
48905
- "recording.pruneFootage": {
48906
- capName: "recording",
49390
+ "recordingArchive.pruneFootage": {
49391
+ capName: "recording-archive",
48907
49392
  capScope: "system",
48908
49393
  addonId: null,
48909
49394
  access: "create"
48910
49395
  },
48911
- "recording.readGopBytes": {
48912
- capName: "recording",
49396
+ "recordingArchive.readGopBytes": {
49397
+ capName: "recording-archive",
48913
49398
  capScope: "system",
48914
49399
  addonId: null,
48915
49400
  access: "view"
48916
49401
  },
48917
- "recording.readSegmentBytes": {
48918
- capName: "recording",
49402
+ "recordingArchive.readSegmentBytes": {
49403
+ capName: "recording-archive",
48919
49404
  capScope: "system",
48920
49405
  addonId: null,
48921
49406
  access: "view"
48922
49407
  },
48923
- "recording.readWindowBytes": {
48924
- capName: "recording",
49408
+ "recordingArchive.readWindowBytes": {
49409
+ capName: "recording-archive",
48925
49410
  capScope: "system",
48926
49411
  addonId: null,
48927
49412
  access: "view"
48928
49413
  },
48929
- "recording.reconcileLedgerAgainstDisk": {
48930
- capName: "recording",
49414
+ "recordingArchive.reconcileLedgerAgainstDisk": {
49415
+ capName: "recording-archive",
48931
49416
  capScope: "system",
48932
49417
  addonId: null,
48933
49418
  access: "create"
48934
49419
  },
48935
- "recording.refreshStorageLocationsForMigration": {
48936
- capName: "recording",
49420
+ "recordingArchive.refreshStorageLocationsForMigration": {
49421
+ capName: "recording-archive",
48937
49422
  capScope: "system",
48938
49423
  addonId: null,
48939
49424
  access: "create"
48940
49425
  },
48941
- "recording.relocateFootage": {
48942
- capName: "recording",
49426
+ "recordingArchive.relocateFootage": {
49427
+ capName: "recording-archive",
48943
49428
  capScope: "system",
48944
49429
  addonId: null,
48945
49430
  access: "create"
48946
49431
  },
48947
- "recording.renderClip": {
48948
- capName: "recording",
49432
+ "recordingArchive.renderClip": {
49433
+ capName: "recording-archive",
48949
49434
  capScope: "system",
48950
49435
  addonId: null,
48951
49436
  access: "create"
48952
49437
  },
48953
- "recording.renderGif": {
48954
- capName: "recording",
49438
+ "recordingArchive.renderGif": {
49439
+ capName: "recording-archive",
48955
49440
  capScope: "system",
48956
49441
  addonId: null,
48957
49442
  access: "create"
48958
49443
  },
48959
- "recording.rescanStorage": {
48960
- capName: "recording",
49444
+ "recordingArchive.rescanStorage": {
49445
+ capName: "recording-archive",
48961
49446
  capScope: "system",
48962
49447
  addonId: null,
48963
49448
  access: "create"
48964
49449
  },
48965
- "recording.resumeForStorageMigration": {
48966
- capName: "recording",
49450
+ "recordingArchive.resumeForStorageMigration": {
49451
+ capName: "recording-archive",
48967
49452
  capScope: "system",
48968
49453
  addonId: null,
48969
49454
  access: "create"
48970
49455
  },
48971
- "recording.setDeviceConfig": {
48972
- capName: "recording",
49456
+ "recordingArchive.setDeviceConfig": {
49457
+ capName: "recording-archive",
48973
49458
  capScope: "system",
48974
49459
  addonId: null,
48975
49460
  access: "create"
48976
49461
  },
48977
- "recording.setDevicePlacement": {
48978
- capName: "recording",
49462
+ "recordingArchive.setDevicePlacement": {
49463
+ capName: "recording-archive",
48979
49464
  capScope: "system",
48980
49465
  addonId: null,
48981
49466
  access: "create"
48982
49467
  },
48983
- "recording.startStorageMigrationMove": {
48984
- capName: "recording",
49468
+ "recordingArchive.startStorageMigrationMove": {
49469
+ capName: "recording-archive",
48985
49470
  capScope: "system",
48986
49471
  addonId: null,
48987
49472
  access: "create"
48988
49473
  },
48989
- "recording.startStorageRebalance": {
48990
- capName: "recording",
49474
+ "recordingArchive.startStorageRebalance": {
49475
+ capName: "recording-archive",
48991
49476
  capScope: "system",
48992
49477
  addonId: null,
48993
49478
  access: "create"
@@ -51292,21 +51777,6 @@ Object.freeze({
51292
51777
  form: "single",
51293
51778
  optional: false
51294
51779
  }],
51295
- "events.getEventClipUrl": [{
51296
- name: "deviceId",
51297
- form: "single",
51298
- optional: false
51299
- }],
51300
- "events.getEvents": [{
51301
- name: "deviceId",
51302
- form: "single",
51303
- optional: false
51304
- }],
51305
- "events.getEventThumbnail": [{
51306
- name: "deviceId",
51307
- form: "single",
51308
- optional: false
51309
- }],
51310
51780
  "faceGallery.getFaceByTrack": [{
51311
51781
  name: "deviceId",
51312
51782
  form: "single",
@@ -52225,107 +52695,117 @@ Object.freeze({
52225
52695
  form: "single",
52226
52696
  optional: false
52227
52697
  }],
52228
- "recording.deleteFootprint": [{
52698
+ "recording.getAvailability": [{
52229
52699
  name: "deviceId",
52230
52700
  form: "single",
52231
52701
  optional: false
52232
52702
  }],
52233
- "recording.getAvailability": [{
52703
+ "recording.getDaysWithRecordings": [{
52234
52704
  name: "deviceId",
52235
52705
  form: "single",
52236
52706
  optional: false
52237
52707
  }],
52238
- "recording.getAvailabilityBatch": [{
52239
- name: "deviceIds",
52240
- form: "array",
52708
+ "recording.getPlayback": [{
52709
+ name: "deviceId",
52710
+ form: "single",
52241
52711
  optional: false
52242
52712
  }],
52243
- "recording.getDaysWithRecordings": [{
52713
+ "recording.getPlaybackOptions": [{
52244
52714
  name: "deviceId",
52245
52715
  form: "single",
52246
52716
  optional: false
52247
52717
  }],
52248
- "recording.getDaysWithRecordingsBatch": [{
52249
- name: "deviceIds",
52250
- form: "array",
52718
+ "recording.listSources": [{
52719
+ name: "deviceId",
52720
+ form: "single",
52251
52721
  optional: false
52252
52722
  }],
52253
- "recording.getDeviceConfig": [{
52723
+ "recordingArchive.deleteFootprint": [{
52254
52724
  name: "deviceId",
52255
52725
  form: "single",
52256
52726
  optional: false
52257
52727
  }],
52258
- "recording.getPlaybackManifest": [{
52728
+ "recordingArchive.getAvailabilityBatch": [{
52729
+ name: "deviceIds",
52730
+ form: "array",
52731
+ optional: false
52732
+ }],
52733
+ "recordingArchive.getDaysWithRecordingsBatch": [{
52734
+ name: "deviceIds",
52735
+ form: "array",
52736
+ optional: false
52737
+ }],
52738
+ "recordingArchive.getDeviceConfig": [{
52259
52739
  name: "deviceId",
52260
52740
  form: "single",
52261
52741
  optional: false
52262
52742
  }],
52263
- "recording.listOpsLog": [{
52743
+ "recordingArchive.listOpsLog": [{
52264
52744
  name: "deviceId",
52265
52745
  form: "single",
52266
52746
  optional: true
52267
52747
  }],
52268
- "recording.locateSegment": [{
52748
+ "recordingArchive.locateSegment": [{
52269
52749
  name: "deviceId",
52270
52750
  form: "single",
52271
52751
  optional: false
52272
52752
  }],
52273
- "recording.pruneFootage": [{
52753
+ "recordingArchive.pruneFootage": [{
52274
52754
  name: "deviceId",
52275
52755
  form: "single",
52276
52756
  optional: false
52277
52757
  }],
52278
- "recording.readGopBytes": [{
52758
+ "recordingArchive.readGopBytes": [{
52279
52759
  name: "deviceId",
52280
52760
  form: "single",
52281
52761
  optional: false
52282
52762
  }],
52283
- "recording.readSegmentBytes": [{
52763
+ "recordingArchive.readSegmentBytes": [{
52284
52764
  name: "deviceId",
52285
52765
  form: "single",
52286
52766
  optional: false
52287
52767
  }],
52288
- "recording.readWindowBytes": [{
52768
+ "recordingArchive.readWindowBytes": [{
52289
52769
  name: "deviceId",
52290
52770
  form: "single",
52291
52771
  optional: false
52292
52772
  }],
52293
- "recording.reconcileLedgerAgainstDisk": [{
52773
+ "recordingArchive.reconcileLedgerAgainstDisk": [{
52294
52774
  name: "deviceId",
52295
52775
  form: "single",
52296
52776
  optional: true
52297
52777
  }],
52298
- "recording.relocateFootage": [{
52778
+ "recordingArchive.relocateFootage": [{
52299
52779
  name: "deviceId",
52300
52780
  form: "single",
52301
52781
  optional: true
52302
52782
  }],
52303
- "recording.renderClip": [{
52783
+ "recordingArchive.renderClip": [{
52304
52784
  name: "deviceId",
52305
52785
  form: "single",
52306
52786
  optional: false
52307
52787
  }],
52308
- "recording.renderGif": [{
52788
+ "recordingArchive.renderGif": [{
52309
52789
  name: "deviceId",
52310
52790
  form: "single",
52311
52791
  optional: false
52312
52792
  }],
52313
- "recording.rescanStorage": [{
52793
+ "recordingArchive.rescanStorage": [{
52314
52794
  name: "deviceId",
52315
52795
  form: "single",
52316
52796
  optional: false
52317
52797
  }],
52318
- "recording.setDeviceConfig": [{
52798
+ "recordingArchive.setDeviceConfig": [{
52319
52799
  name: "deviceId",
52320
52800
  form: "single",
52321
52801
  optional: false
52322
52802
  }],
52323
- "recording.setDevicePlacement": [{
52803
+ "recordingArchive.setDevicePlacement": [{
52324
52804
  name: "deviceId",
52325
52805
  form: "single",
52326
52806
  optional: false
52327
52807
  }],
52328
- "recording.startStorageMigrationMove": [{
52808
+ "recordingArchive.startStorageMigrationMove": [{
52329
52809
  name: "deviceId",
52330
52810
  form: "single",
52331
52811
  optional: true
@@ -52854,6 +53334,37 @@ function isScheduleActive(schedule, atMs) {
52854
53334
  }
52855
53335
  return schedule.invert === true ? !inside : inside;
52856
53336
  }
53337
+ var RULE_BODY_ONLY = ["body"];
53338
+ var RULE_TEXT = ["title", "body"];
53339
+ var BATTERY = ["device-battery-low", "device-battery-normal"];
53340
+ var CONSUMABLE = ["device-consumable-low", "device-consumable-normal"];
53341
+ var UPDATES = [
53342
+ "addon-update-available",
53343
+ "server-update-available",
53344
+ "wrapper-update-available"
53345
+ ];
53346
+ var PACKAGE_UPDATES = ["addon-update-available", "server-update-available"];
53347
+ var ALARM = [
53348
+ "alarm-triggered",
53349
+ "alarm-armed",
53350
+ "alarm-disarmed",
53351
+ "alarm-arming",
53352
+ "alarm-arm-refused"
53353
+ ];
53354
+ var COMBINED_ALARM = ["alarm-triggered"];
53355
+ var DEVICE_EVENTS = [
53356
+ "device-online",
53357
+ "device-offline",
53358
+ "device-disabled",
53359
+ "device-enabled",
53360
+ ...BATTERY,
53361
+ ...CONSUMABLE,
53362
+ "stream-online",
53363
+ "stream-offline",
53364
+ "detection-blind",
53365
+ ...ALARM
53366
+ ];
53367
+ [...DEVICE_EVENTS], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...PACKAGE_UPDATES], [...RULE_TEXT], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_BODY_ONLY], [...RULE_TEXT], [...RULE_TEXT], [...DEVICE_EVENTS], [...RULE_TEXT], [...DEVICE_EVENTS], [...RULE_TEXT], [...BATTERY, ...CONSUMABLE], [...RULE_TEXT], [...CONSUMABLE], [...RULE_TEXT], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...PACKAGE_UPDATES], [...RULE_TEXT], [...PACKAGE_UPDATES], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...PACKAGE_UPDATES], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT];
52857
53368
  /**
52858
53369
  * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
52859
53370
  *