@camstack/system 1.2.289 → 1.2.291

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 (62) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/autotrack/index.js +1 -1
  10. package/dist/builtins/autotrack/index.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  13. package/dist/builtins/camera-grid/index.js +1 -1
  14. package/dist/builtins/camera-grid/index.mjs +1 -1
  15. package/dist/builtins/console-logging/index.js +1 -1
  16. package/dist/builtins/console-logging/index.mjs +1 -1
  17. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  18. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  19. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  20. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  21. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  22. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  23. package/dist/builtins/hub-forwarder/index.js +1 -1
  24. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  25. package/dist/builtins/liveness-monitor/liveness-monitor.addon.d.ts +9 -0
  26. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +18 -2
  27. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +18 -2
  28. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  29. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  30. package/dist/builtins/local-network/local-network.addon.js +1 -1
  31. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  32. package/dist/builtins/loki-logging/index.js +1 -1
  33. package/dist/builtins/loki-logging/index.mjs +1 -1
  34. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  35. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  36. package/dist/builtins/platform-probe/index.js +1 -1
  37. package/dist/builtins/platform-probe/index.mjs +1 -1
  38. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  39. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  40. package/dist/builtins/snapshot/index.js +1 -1
  41. package/dist/builtins/snapshot/index.mjs +1 -1
  42. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  43. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  44. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  45. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  46. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +11 -11
  47. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +11 -11
  48. package/dist/builtins/system-config/system-config.addon.js +1 -1
  49. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  50. package/dist/builtins/winston-logging/index.js +1 -1
  51. package/dist/builtins/winston-logging/index.mjs +1 -1
  52. package/dist/{dist-DKFb2936.mjs → dist-DiMNxNDh.mjs} +1474 -1017
  53. package/dist/{dist-BYnHJU96.js → dist-XnunBp0n.js} +1479 -1016
  54. package/dist/index.d.ts +1 -0
  55. package/dist/index.js +5 -3
  56. package/dist/index.mjs +4 -4
  57. package/dist/kernel/moleculer/addon-peer-bytes-facility.d.ts +25 -0
  58. package/dist/{manifest-system-deps-B8ZUclN4.js → manifest-system-deps-DTgkfBJy.js} +591 -0
  59. package/dist/{manifest-system-deps-C9T7784z.mjs → manifest-system-deps-ghRqcy_-.mjs} +585 -6
  60. package/dist/{retired-settings-keys-C6rFqHR7.js → retired-settings-keys-DQn9s_AI.js} +1 -1
  61. package/dist/{retired-settings-keys-tuNxNNpR.mjs → retired-settings-keys-DnCcgrCG.mjs} +1 -1
  62. package/package.json +1 -1
@@ -691,7 +691,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
691
691
  return EventCategory;
692
692
  }({});
693
693
  //#endregion
694
- //#region ../types/dist/sleep-i3eUVc-d.mjs
694
+ //#region ../types/dist/sleep-PEo0-Fz9.mjs
695
695
  /**
696
696
  * The audio chunk plane's byte format, and the ONE expansion from a coded
697
697
  * window to float samples (D455).
@@ -1851,6 +1851,24 @@ function normalizeAddonInitResult(result) {
1851
1851
  * core proxy and the kernel listener) to keep both off a kernel↔core import.
1852
1852
  */
1853
1853
  var DATAPLANE_SECRET_HEADER = "x-camstack-dataplane-secret";
1854
+ /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
1855
+ var PeerBytesTicketSchema = z.object({
1856
+ /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
1857
+ url: z.string().min(1),
1858
+ /**
1859
+ * The HOST node this URL means something on — the hub or a named agent,
1860
+ * never a runner. {@link AddonPeerBytes.open} compares it to its own and
1861
+ * refuses `cross-node` by name when they differ, without dialling.
1862
+ */
1863
+ hostNodeId: z.string().min(1),
1864
+ expiresAtMs: z.number().int().nonnegative(),
1865
+ /**
1866
+ * What the producer DECLARED the body to be, when it knows — `null` when it
1867
+ * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
1868
+ * must be able to tell "the producer did not say" from "the body is empty".
1869
+ */
1870
+ declaredBytes: z.number().int().nonnegative().nullable()
1871
+ });
1854
1872
  /** Shared Zod schemas used across streaming capabilities. */
1855
1873
  var CamProfileSchema = z.enum([
1856
1874
  "high",
@@ -4160,7 +4178,7 @@ var AdoptionJobSchema = z.object({
4160
4178
  * component's original options — detection to the detection-pipeline wrapper
4161
4179
  * binding, audio analysis to its own, recording to `RecordingConfig.enabled`
4162
4180
  * (which was always first-class; the switch was a veneer over
4163
- * `recording.setDeviceConfig`), notifications to a notification-center
4181
+ * `recordingArchive.setDeviceConfig`), notifications to a notification-center
4164
4182
  * per-device setting, the two camera planes to their own components.
4165
4183
  *
4166
4184
  * What survives is {@link composeSwitchedOff}: `CameraStatus.switchedOff`, the
@@ -4186,7 +4204,7 @@ var AdoptionJobSchema = z.object({
4186
4204
  * | `stream-broker` | `deviceManager.setDisabled` | `StreamBrokerManager.reconcileAllCatalogs` releases the brokers; `ensureBroker` refuses re-creation |
4187
4205
  * | `object-detection` | `deviceManager.setWrapperActive('detection-pipeline')` | `PipelineSettingsStore.resolvePipelineForDevice` returns `{ steps: [], audio: null }` |
4188
4206
  * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
4189
- * | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
4207
+ * | `recording` | `recordingArchive.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
4190
4208
  * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
4191
4209
  * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
4192
4210
  * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
@@ -9018,87 +9036,6 @@ var cameraPipelineConfigCapability = {
9018
9036
  exposesDeviceSettings: true,
9019
9037
  methods: {}
9020
9038
  };
9021
- /**
9022
- * The signals a device can emit to WAKE its own stream.
9023
- *
9024
- * A camera whose stream is built on demand sleeps until something asks for it,
9025
- * and "something" cannot be a consumer that is merely attached — a Frigate-style
9026
- * puller holds a session open for ever, and treating that as demand would keep
9027
- * a battery camera awake for ever, which is the whole thing the battery is for
9028
- * (D173). So the wake has to come from the CAMERA: an event it noticed by
9029
- * itself, with no stream running.
9030
- *
9031
- * ## The vocabulary is the PROVIDER'S, not ours
9032
- *
9033
- * Like `consumables`, this cap declares no vocabulary of its own. A provider
9034
- * names each signal with a `code` it chooses and a `label` an operator reads.
9035
- * Reolink offers motion and camera-native detection; another provider may offer
9036
- * a tamper, a doorbell press, a PIR, or something no camera in this fleet has
9037
- * yet. A fixed enum here would mean every new signal is a framework release.
9038
- *
9039
- * It is deliberately NOT derived from the caps a device already binds. Whether
9040
- * a camera CAN push firmware motion is expressed by `motionSources` containing
9041
- * `'onboard'`, and whether it does AI on-camera by the `native-object-detection`
9042
- * binding — but both answer "what drives the detection pipeline", which is a
9043
- * different question from "what may wake a sleeping stream". A camera can do
9044
- * the first and not be trusted with the second, and the operator picks per
9045
- * camera. Two questions, two authorities.
9046
- *
9047
- * ## Availability is not permission
9048
- *
9049
- * `listSignals` says what the device CAN emit. Whether a given signal actually
9050
- * wakes the stream is the operator's per-camera choice, held by the broker
9051
- * alongside the cooldown — see the stream-broker cap's wake settings. A
9052
- * provider declaring a signal is not a provider enabling it.
9053
- */
9054
- /** One signal a device can emit. */
9055
- var StreamSignalSchema = z.object({
9056
- /** Stable id chosen by the provider, e.g. `'motion'`, `'person'`, `'tamper'`. */
9057
- code: z.string().min(1),
9058
- /** What an operator reads in the picker. The provider's own wording. */
9059
- label: z.string().min(1),
9060
- /**
9061
- * Whether the provider recommends this signal ON when a camera is first set
9062
- * up. A provider knows which of its signals are cheap and reliable; an
9063
- * operator should not have to discover that by trial. Reolink recommends
9064
- * both of its own.
9065
- */
9066
- recommended: z.boolean()
9067
- });
9068
- var StreamSignalsStatusSchema = z.object({
9069
- signals: z.array(StreamSignalSchema),
9070
- lastFetchedAt: z.number()
9071
- });
9072
- var streamSignalsCapability = {
9073
- name: "stream-signals",
9074
- scope: "device",
9075
- deviceNative: true,
9076
- mode: "singleton",
9077
- deviceTypes: Object.values(DeviceType),
9078
- runtimeState: StreamSignalsStatusSchema,
9079
- /**
9080
- * Runtime-state durability: **session** — mirrored in RAM, never written.
9081
- *
9082
- * The slice holds what the DEVICE says it can emit. That is a probed fact,
9083
- * not an operator choice: the provider re-declares it on every registration,
9084
- * so losing it loses nothing and persisting it would freeze an answer the
9085
- * camera is entitled to change. Measured the same day on the sibling case —
9086
- * `native-object-detection.supportedClasses` was persisted, and a firmware
9087
- * class the camera really detected stayed missing for the life of the row
9088
- * because the fix could not reach it.
9089
- *
9090
- * See `RuntimeStateDurability`. Enforced by
9091
- * `scripts/check-runtime-state-durability.ts`.
9092
- */
9093
- durability: "session",
9094
- methods: {
9095
- /**
9096
- * What this device can emit. Empty is a valid and common answer — most
9097
- * cameras have nothing to offer here, and an empty list is what makes the
9098
- * broker's picker show nothing rather than a false choice.
9099
- */
9100
- listSignals: method(z.void(), z.array(StreamSignalSchema).readonly()) }
9101
- };
9102
9039
  /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
9103
9040
  var StreamFormatSchema = z.enum([
9104
9041
  "webrtc",
@@ -11477,6 +11414,118 @@ var detectionPipelineCapability = {
11477
11414
  methods: {}
11478
11415
  };
11479
11416
  /**
11417
+ * device-admin-link — "this device has a management page of its own, and here
11418
+ * is its address".
11419
+ *
11420
+ * ## Why this is not a `deviceConfig` cap
11421
+ *
11422
+ * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
11423
+ * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
11424
+ * patch back through a setter; it costs a `builderId` reducer in
11425
+ * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
11426
+ * renders a form section. This cap answers ONE question with ONE read and
11427
+ * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
11428
+ * block, no `settings`, no `runtimeState` and no reducer — exactly like
11429
+ * `reboot`, the other pure-RPC device-native cap.
11430
+ *
11431
+ * ## Absent, and the difference between "no page" and "we cannot say"
11432
+ *
11433
+ * The two are answered at DIFFERENT layers, on purpose:
11434
+ *
11435
+ * - **"We cannot say"** → the provider never registers the cap for that
11436
+ * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
11437
+ * fan are reached only through a vendor cloud; there is no address to hand
11438
+ * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
11439
+ * conditioner DO have a LAN IP, and still have no HTTP management page
11440
+ * behind it. None of them register, so `deviceManager.getBindings` never
11441
+ * lists the cap and no surface asks.
11442
+ * - **"This device has no page, and I know that"** → the provider registers
11443
+ * and `getAdminLink` returns `null`. This is the answer for a device whose
11444
+ * sibling DOES have a page: a Reolink battery camera reached over UDP by
11445
+ * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
11446
+ * transport, a Home Assistant broker authenticated by supervisor token
11447
+ * (which carries no `baseUrl` at all).
11448
+ *
11449
+ * Both draw NOTHING. A button that opens a browser error is worse than no
11450
+ * button, and D62 is the same rule from the other side: an off switch is
11451
+ * reported off, never made to look broken. There is no third state where the
11452
+ * UI renders a disabled button "because the device might have a page".
11453
+ *
11454
+ * ## The URL never carries credentials
11455
+ *
11456
+ * Not in userinfo, not in a query string. Every provider builds through
11457
+ * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
11458
+ * scheme and path as separate arguments — there is no parameter a secret could
11459
+ * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
11460
+ * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
11461
+ * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
11462
+ * keeps providers from hand-rolling one anyway.
11463
+ *
11464
+ * This matters here more than anywhere else in the repo, because every provider
11465
+ * that knows a device's host knows its PASSWORD too: `{ host, port, username,
11466
+ * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
11467
+ * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
11468
+ * camera's own page will ask for its own login. That is correct, and pre-
11469
+ * filling it is the operator's business, not ours.
11470
+ *
11471
+ * ## It is a LAN fact
11472
+ *
11473
+ * The URL addresses the device where the NODE can see it. It is not proxied,
11474
+ * not made reachable from outside, and not sent anywhere. A surface renders it
11475
+ * as a link the operator's own browser follows, on the operator's own network,
11476
+ * or renders nothing.
11477
+ */
11478
+ /**
11479
+ * Whose page is it. The distinction is for the OPERATOR, who needs to know
11480
+ * before clicking whether he is about to land on a camera's own web server or
11481
+ * inside Home Assistant.
11482
+ */
11483
+ var AdminLinkTargetEnum = z.enum(["device", "integration"]);
11484
+ var DeviceAdminLinkSchema = z.object({
11485
+ /**
11486
+ * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
11487
+ * free of userinfo and of any credential-shaped query key.
11488
+ */
11489
+ url: z.string(),
11490
+ /**
11491
+ * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
11492
+ * The PROVIDER names it, because only the provider knows what the page is;
11493
+ * a UI that invented the label from the addon id would call the Home
11494
+ * Assistant device page "Provider Homeassistant".
11495
+ */
11496
+ label: z.string(),
11497
+ target: AdminLinkTargetEnum,
11498
+ /**
11499
+ * Host the URL points at, without scheme, port or path — for the tooltip, so
11500
+ * an operator can see WHERE the button goes before he follows it. Redundant
11501
+ * with `url` by construction; carried separately so no surface has to parse
11502
+ * a URL to show it.
11503
+ */
11504
+ host: z.string()
11505
+ });
11506
+ var deviceAdminLinkCapability = {
11507
+ name: "device-admin-link",
11508
+ scope: "device",
11509
+ deviceNative: true,
11510
+ mode: "singleton",
11511
+ methods: {
11512
+ /**
11513
+ * The device's management page, or `null` when this device has none.
11514
+ *
11515
+ * `auth: 'admin'` deliberately. This is administration, not actuation —
11516
+ * the same bucket as `reboot` and `camera-credentials`, and explicitly NOT
11517
+ * the actuation set `scripts/check-actuation-not-admin.ts` protects (D403).
11518
+ * The URL is also a statement about the LAN, which a household member with
11519
+ * a `view` grant on a light has no reason to be handed.
11520
+ *
11521
+ * The surfaces gate on the QUERY, never on a role they guessed: a caller
11522
+ * without the right loses the query and draws nothing, which is the same
11523
+ * thing a device with no page draws. There is no path on which a button
11524
+ * appears and then fails — the D403 failure mode, from the other end.
11525
+ */
11526
+ getAdminLink: method(z.object({ deviceId: z.number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
11527
+ };
11528
+ /**
11480
11529
  * Identity envelope for a device's upstream-system metadata.
11481
11530
  *
11482
11531
  * Two jobs:
@@ -11973,118 +12022,6 @@ var deviceAdoptionCapability = {
11973
12022
  }
11974
12023
  };
11975
12024
  /**
11976
- * device-admin-link — "this device has a management page of its own, and here
11977
- * is its address".
11978
- *
11979
- * ## Why this is not a `deviceConfig` cap
11980
- *
11981
- * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
11982
- * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
11983
- * patch back through a setter; it costs a `builderId` reducer in
11984
- * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
11985
- * renders a form section. This cap answers ONE question with ONE read and
11986
- * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
11987
- * block, no `settings`, no `runtimeState` and no reducer — exactly like
11988
- * `reboot`, the other pure-RPC device-native cap.
11989
- *
11990
- * ## Absent, and the difference between "no page" and "we cannot say"
11991
- *
11992
- * The two are answered at DIFFERENT layers, on purpose:
11993
- *
11994
- * - **"We cannot say"** → the provider never registers the cap for that
11995
- * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
11996
- * fan are reached only through a vendor cloud; there is no address to hand
11997
- * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
11998
- * conditioner DO have a LAN IP, and still have no HTTP management page
11999
- * behind it. None of them register, so `deviceManager.getBindings` never
12000
- * lists the cap and no surface asks.
12001
- * - **"This device has no page, and I know that"** → the provider registers
12002
- * and `getAdminLink` returns `null`. This is the answer for a device whose
12003
- * sibling DOES have a page: a Reolink battery camera reached over UDP by
12004
- * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
12005
- * transport, a Home Assistant broker authenticated by supervisor token
12006
- * (which carries no `baseUrl` at all).
12007
- *
12008
- * Both draw NOTHING. A button that opens a browser error is worse than no
12009
- * button, and D62 is the same rule from the other side: an off switch is
12010
- * reported off, never made to look broken. There is no third state where the
12011
- * UI renders a disabled button "because the device might have a page".
12012
- *
12013
- * ## The URL never carries credentials
12014
- *
12015
- * Not in userinfo, not in a query string. Every provider builds through
12016
- * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
12017
- * scheme and path as separate arguments — there is no parameter a secret could
12018
- * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
12019
- * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
12020
- * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
12021
- * keeps providers from hand-rolling one anyway.
12022
- *
12023
- * This matters here more than anywhere else in the repo, because every provider
12024
- * that knows a device's host knows its PASSWORD too: `{ host, port, username,
12025
- * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
12026
- * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
12027
- * camera's own page will ask for its own login. That is correct, and pre-
12028
- * filling it is the operator's business, not ours.
12029
- *
12030
- * ## It is a LAN fact
12031
- *
12032
- * The URL addresses the device where the NODE can see it. It is not proxied,
12033
- * not made reachable from outside, and not sent anywhere. A surface renders it
12034
- * as a link the operator's own browser follows, on the operator's own network,
12035
- * or renders nothing.
12036
- */
12037
- /**
12038
- * Whose page is it. The distinction is for the OPERATOR, who needs to know
12039
- * before clicking whether he is about to land on a camera's own web server or
12040
- * inside Home Assistant.
12041
- */
12042
- var AdminLinkTargetEnum = z.enum(["device", "integration"]);
12043
- var DeviceAdminLinkSchema = z.object({
12044
- /**
12045
- * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
12046
- * free of userinfo and of any credential-shaped query key.
12047
- */
12048
- url: z.string(),
12049
- /**
12050
- * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
12051
- * The PROVIDER names it, because only the provider knows what the page is;
12052
- * a UI that invented the label from the addon id would call the Home
12053
- * Assistant device page "Provider Homeassistant".
12054
- */
12055
- label: z.string(),
12056
- target: AdminLinkTargetEnum,
12057
- /**
12058
- * Host the URL points at, without scheme, port or path — for the tooltip, so
12059
- * an operator can see WHERE the button goes before he follows it. Redundant
12060
- * with `url` by construction; carried separately so no surface has to parse
12061
- * a URL to show it.
12062
- */
12063
- host: z.string()
12064
- });
12065
- var deviceAdminLinkCapability = {
12066
- name: "device-admin-link",
12067
- scope: "device",
12068
- deviceNative: true,
12069
- mode: "singleton",
12070
- methods: {
12071
- /**
12072
- * The device's management page, or `null` when this device has none.
12073
- *
12074
- * `auth: 'admin'` deliberately. This is administration, not actuation —
12075
- * the same bucket as `reboot` and `camera-credentials`, and explicitly NOT
12076
- * the actuation set `scripts/check-actuation-not-admin.ts` protects (D403).
12077
- * The URL is also a statement about the LAN, which a household member with
12078
- * a `view` grant on a light has no reason to be handed.
12079
- *
12080
- * The surfaces gate on the QUERY, never on a role they guessed: a caller
12081
- * without the right loses the query and draws nothing, which is the same
12082
- * thing a device with no page draws. There is no path on which a button
12083
- * appears and then fails — the D403 failure mode, from the other end.
12084
- */
12085
- getAdminLink: method(z.object({ deviceId: z.number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
12086
- };
12087
- /**
12088
12025
  * `device-export` — collection cap for addons that export camstack
12089
12026
  * devices to external ecosystems (HomeAssistant via MQTT discovery,
12090
12027
  * HomeKit/HAP, Alexa Smart Home, …).
@@ -25431,6 +25368,87 @@ var storageProviderCapability = {
25431
25368
  })
25432
25369
  }
25433
25370
  };
25371
+ /**
25372
+ * The signals a device can emit to WAKE its own stream.
25373
+ *
25374
+ * A camera whose stream is built on demand sleeps until something asks for it,
25375
+ * and "something" cannot be a consumer that is merely attached — a Frigate-style
25376
+ * puller holds a session open for ever, and treating that as demand would keep
25377
+ * a battery camera awake for ever, which is the whole thing the battery is for
25378
+ * (D173). So the wake has to come from the CAMERA: an event it noticed by
25379
+ * itself, with no stream running.
25380
+ *
25381
+ * ## The vocabulary is the PROVIDER'S, not ours
25382
+ *
25383
+ * Like `consumables`, this cap declares no vocabulary of its own. A provider
25384
+ * names each signal with a `code` it chooses and a `label` an operator reads.
25385
+ * Reolink offers motion and camera-native detection; another provider may offer
25386
+ * a tamper, a doorbell press, a PIR, or something no camera in this fleet has
25387
+ * yet. A fixed enum here would mean every new signal is a framework release.
25388
+ *
25389
+ * It is deliberately NOT derived from the caps a device already binds. Whether
25390
+ * a camera CAN push firmware motion is expressed by `motionSources` containing
25391
+ * `'onboard'`, and whether it does AI on-camera by the `native-object-detection`
25392
+ * binding — but both answer "what drives the detection pipeline", which is a
25393
+ * different question from "what may wake a sleeping stream". A camera can do
25394
+ * the first and not be trusted with the second, and the operator picks per
25395
+ * camera. Two questions, two authorities.
25396
+ *
25397
+ * ## Availability is not permission
25398
+ *
25399
+ * `listSignals` says what the device CAN emit. Whether a given signal actually
25400
+ * wakes the stream is the operator's per-camera choice, held by the broker
25401
+ * alongside the cooldown — see the stream-broker cap's wake settings. A
25402
+ * provider declaring a signal is not a provider enabling it.
25403
+ */
25404
+ /** One signal a device can emit. */
25405
+ var StreamSignalSchema = z.object({
25406
+ /** Stable id chosen by the provider, e.g. `'motion'`, `'person'`, `'tamper'`. */
25407
+ code: z.string().min(1),
25408
+ /** What an operator reads in the picker. The provider's own wording. */
25409
+ label: z.string().min(1),
25410
+ /**
25411
+ * Whether the provider recommends this signal ON when a camera is first set
25412
+ * up. A provider knows which of its signals are cheap and reliable; an
25413
+ * operator should not have to discover that by trial. Reolink recommends
25414
+ * both of its own.
25415
+ */
25416
+ recommended: z.boolean()
25417
+ });
25418
+ var StreamSignalsStatusSchema = z.object({
25419
+ signals: z.array(StreamSignalSchema),
25420
+ lastFetchedAt: z.number()
25421
+ });
25422
+ var streamSignalsCapability = {
25423
+ name: "stream-signals",
25424
+ scope: "device",
25425
+ deviceNative: true,
25426
+ mode: "singleton",
25427
+ deviceTypes: Object.values(DeviceType),
25428
+ runtimeState: StreamSignalsStatusSchema,
25429
+ /**
25430
+ * Runtime-state durability: **session** — mirrored in RAM, never written.
25431
+ *
25432
+ * The slice holds what the DEVICE says it can emit. That is a probed fact,
25433
+ * not an operator choice: the provider re-declares it on every registration,
25434
+ * so losing it loses nothing and persisting it would freeze an answer the
25435
+ * camera is entitled to change. Measured the same day on the sibling case —
25436
+ * `native-object-detection.supportedClasses` was persisted, and a firmware
25437
+ * class the camera really detected stayed missing for the life of the row
25438
+ * because the fix could not reach it.
25439
+ *
25440
+ * See `RuntimeStateDurability`. Enforced by
25441
+ * `scripts/check-runtime-state-durability.ts`.
25442
+ */
25443
+ durability: "session",
25444
+ methods: {
25445
+ /**
25446
+ * What this device can emit. Empty is a valid and common answer — most
25447
+ * cameras have nothing to offer here, and an empty list is what makes the
25448
+ * broker's picker show nothing rather than a false choice.
25449
+ */
25450
+ listSignals: method(z.void(), z.array(StreamSignalSchema).readonly()) }
25451
+ };
25434
25452
  /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
25435
25453
  var ProfileSettingsSchemaBridge = z.unknown().nullable();
25436
25454
  var ProfileSettingsBagSchema = z.record(z.string(), z.unknown());
@@ -26133,6 +26151,7 @@ z.enum([
26133
26151
  "sleeping",
26134
26152
  "camera-refused",
26135
26153
  "no-keyframe",
26154
+ "decode-failed",
26136
26155
  "no-catalog-row",
26137
26156
  "unsupported",
26138
26157
  "unknown-device",
@@ -26407,6 +26426,32 @@ var ClipBytesSchema = z.object({
26407
26426
  durationMs: z.number().positive().optional()
26408
26427
  });
26409
26428
  /**
26429
+ * Where a clip's finished bytes can be TAKEN (D613) — the answer to
26430
+ * {@link videoclipsCapability.methods.offerClipBytes}.
26431
+ *
26432
+ * Everything {@link ClipBytesSchema} carries except the bytes themselves, plus
26433
+ * the one-shot ticket that leads to them. The metadata is answered BEFORE the
26434
+ * transfer on purpose: a consumer learns which twin it got, what to call the
26435
+ * file and how long the clip runs without having to read a byte, so a decision
26436
+ * it would make on that metadata (a wrong twin, an implausible duration) costs
26437
+ * no transfer at all.
26438
+ */
26439
+ var ClipBytesOfferSchema = z.object({
26440
+ /**
26441
+ * One shot, seconds-long, loopback, on the PROVIDER's own host. Open it with
26442
+ * `ctx.peerBytes.open(...)`, which refuses a ticket from another node by
26443
+ * name rather than dialling a port that means something else here.
26444
+ */
26445
+ ticket: PeerBytesTicketSchema,
26446
+ contentType: z.string(),
26447
+ /** Suggested filename, extension included. */
26448
+ name: z.string(),
26449
+ /** Which twin was actually served — see {@link ClipBytesSchema.served}. */
26450
+ served: CamProfileSchema,
26451
+ /** See {@link ClipBytesSchema.durationMs}. Absent when nothing measured it. */
26452
+ durationMs: z.number().positive().optional()
26453
+ });
26454
+ /**
26410
26455
  * Where a clip's STREAM can be dialled (D597) — the answer to
26411
26456
  * {@link videoclipsCapability.methods.dialClipStream}.
26412
26457
  *
@@ -26482,6 +26527,44 @@ var ClipStreamDialSchema = z.object({
26482
26527
  /** Why `servedAudio` is `none` although sound was asked for. */
26483
26528
  audioReason: ClipStreamAudioReasonSchema.optional()
26484
26529
  });
26530
+ /**
26531
+ * What a surface may DRAW for this provider's clips — the answer to
26532
+ * {@link videoclipsCapability.methods.getPlaybackOptions} (D612).
26533
+ *
26534
+ * The envelope is a PROVIDER fact, not a clip fact, and that is measured, not
26535
+ * assumed: the broker's `chooseClipPath` reads exactly two inputs — whether
26536
+ * `dialClipStream` and `readClipBytes` are wired — and both are constants of
26537
+ * the broker's own closure over the provider's methods. The `profile` it is
26538
+ * handed is explicitly not read. So every clip of a provider is served the
26539
+ * same way, and a per-clip channel carried a value that could not vary. The
26540
+ * per-clip `clipTransport` server message was removed for exactly that reason.
26541
+ *
26542
+ * Queried per camera, before a clip is picked, so a control is rendered or
26543
+ * DISABLED rather than offered and refused at play time (D62: a disabled
26544
+ * control reads as unavailable, one that undoes the gesture reads as broken).
26545
+ */
26546
+ var ClipPlaybackOptionsSchema = z.object({
26547
+ /**
26548
+ * How this provider's clips reach the player. `stream` is the provider's
26549
+ * forward-only fMP4 (D597); `file` is one bounded by-handle fetch of the
26550
+ * whole clip, `stbl` indexed (D575).
26551
+ */
26552
+ transport: z.enum(["stream", "file"]),
26553
+ /** `forward` = only ahead of the playhead. `free` = anywhere. */
26554
+ seek: z.enum(["forward", "free"]),
26555
+ /** Frame-step BACKWARD is meaningful. Forward always is. */
26556
+ stepBack: z.boolean(),
26557
+ /** Whether the scrub gesture is served, as opposed to refused by name. */
26558
+ scrub: z.boolean(),
26559
+ /**
26560
+ * The rates that can be delivered, ascending, always containing `1`. The
26561
+ * viewer draws its picker from this and from nothing else — a constant it
26562
+ * keeps instead is the second authority that produced the defect: `8` and
26563
+ * `16` were offered, the broker clamped them to `4`, and no line anywhere
26564
+ * said so. `0` is not a member: pause is the absence of a rate.
26565
+ */
26566
+ rates: z.array(z.number().positive()).min(1).readonly()
26567
+ });
26485
26568
  var ClipSourceAvailabilitySchema = z.object({
26486
26569
  state: z.enum([
26487
26570
  "ok",
@@ -26660,14 +26743,18 @@ var videoclipsCapability = {
26660
26743
  *
26661
26744
  * `getClipPlayback` is the right answer for a player: it hands back a URL
26662
26745
  * on a plane the hub serves `access:'authenticated'`, which a browser and a
26663
- * viewer session satisfy. It is the wrong answer for another ADDON. There
26664
- * is no addon→addon byte transport in this framework — `AddonDataPlane`
26665
- * only lets an addon SERVE, on `127.0.0.1` behind a per-listener secret
26666
- * only the hub may present — so a recorder that wants a camera's clip
26667
- * cannot fetch that URL. This method is the one seam that exists for it,
26668
- * and it is deliberately the same shape (and the same bound) as
26669
- * `recordingExport.readExportBytes`, which exists for the mirror-image
26670
- * reason.
26746
+ * viewer session satisfy. It is the wrong answer for another ADDON: the
26747
+ * hub's proxy in front of that plane takes only a user credential, which
26748
+ * an addon does not hold, so a recorder that wants a camera's clip cannot
26749
+ * fetch that URL. This method is the shape that answer forced — the same
26750
+ * one (and the same bound) as `recordingExport.readExportBytes`.
26751
+ *
26752
+ * **It is no longer the only seam.** Until D613 there was no addon→addon
26753
+ * byte transport at all; there is now
26754
+ * ({@link offerClipBytes}, over `ctx.peerBytes`), it holds nothing on
26755
+ * either side, and it is what a clip EXPORT uses. This method remains for
26756
+ * a consumer that genuinely wants the bytes in hand, and as the named
26757
+ * fallback for a provider not yet redeployed.
26671
26758
  *
26672
26759
  * Routing needs no `provider` pin: the id is source-prefixed and
26673
26760
  * self-contained, so `device-collection-dispatch.ts` rule 3 hands the call
@@ -26734,6 +26821,65 @@ var videoclipsCapability = {
26734
26821
  auth: "protected"
26735
26822
  }),
26736
26823
  /**
26824
+ * Where this clip's finished bytes can be TAKEN — the by-handle read a
26825
+ * clip EXPORT pulls, over the addon→addon byte transport (D613).
26826
+ *
26827
+ * This is {@link readClipBytes} with the envelope removed. Same gates,
26828
+ * same vocabulary, same completion rules, same `served` contract — the
26829
+ * only difference is that the bytes travel over a one-shot loopback
26830
+ * socket instead of inside a base64 field, so neither side holds the
26831
+ * payload whole and the 50 MiB refusal on a long `high` twin stops
26832
+ * existing. The bound that remains is
26833
+ * {@link VIDEOCLIPS_MAX_OFFER_BYTES}, and it bounds the PRODUCER's own
26834
+ * copy rather than the transport.
26835
+ *
26836
+ * **The ticket is loopback and same-host.** A provider on an agent mints a
26837
+ * URL that means nothing on the hub, and `ctx.peerBytes.open` refuses it
26838
+ * `cross-node` by name rather than dialling whatever else holds that port
26839
+ * here. A consumer that can be on the other side of a node boundary from
26840
+ * its provider must be able to read that refusal and say so; it must not
26841
+ * treat it as "no bytes".
26842
+ *
26843
+ * **A ticket is a one-shot bearer credential with a seconds-long life.**
26844
+ * Take it immediately, never persist it, never log its `url`. An untaken
26845
+ * ticket costs the provider one map entry until its TTL, and outstanding
26846
+ * tickets are bounded — which is also what makes a per-frame misuse of
26847
+ * this method refuse by name rather than work slowly (D9/D18: this is a
26848
+ * by-handle fetch of finished media, not a frame pipe).
26849
+ *
26850
+ * Optional on the provider for the same reason `readClipBytes` is: a
26851
+ * source with no camera socket behind it has no bytes. A provider that
26852
+ * predates this method answers `NOT_IMPLEMENTED`, and a consumer may fall
26853
+ * back to `readClipBytes` — but it says so in the log, with the deploy
26854
+ * hint, because that fallback re-imposes the 50 MiB refusal and an
26855
+ * operator who sees `too-large-to-transfer` after this shipped is looking
26856
+ * at a stale addon, not at a clip that cannot be exported.
26857
+ */
26858
+ offerClipBytes: optionalMethod(z.object({
26859
+ deviceId: z.number(),
26860
+ clipId: z.string().min(1),
26861
+ /** WHICH provider holds the bytes — see `readClipBytes.provider`. */
26862
+ provider: z.string().min(1),
26863
+ /** Which twin — `low | mid` → the sub file, `high` → the main twin. */
26864
+ profile: CamProfileSchema.optional(),
26865
+ /**
26866
+ * The CALLER's byte bound, so an over-size clip is refused before the
26867
+ * camera is touched rather than after. Capped by
26868
+ * {@link VIDEOCLIPS_MAX_OFFER_BYTES} whatever is passed; absent means
26869
+ * that ceiling.
26870
+ */
26871
+ maxBytes: z.number().int().positive().optional(),
26872
+ /**
26873
+ * The operator's authorisation to wake a sleeping camera for this
26874
+ * read. Absent — the default — means a sleeping standalone battery
26875
+ * camera is REFUSED by name, before any session is opened.
26876
+ */
26877
+ wake: ClipWakeSchema.optional()
26878
+ }), ClipBytesOfferSchema, {
26879
+ kind: "query",
26880
+ auth: "protected"
26881
+ }),
26882
+ /**
26737
26883
  * Where this clip's STREAM can be dialled (D597) — the forward-only
26738
26884
  * fMP4 the provider writes from the first muxed byte, for the broker to
26739
26885
  * play through the same WebRTC session as recorded footage, with the
@@ -26770,6 +26916,36 @@ var videoclipsCapability = {
26770
26916
  }), ClipStreamDialSchema, {
26771
26917
  kind: "query",
26772
26918
  auth: "protected"
26919
+ }),
26920
+ /**
26921
+ * What a surface may DRAW for this provider's clips: the rates it can be
26922
+ * played at, whether scrub is served, how far a position may be moved,
26923
+ * and whether a backward frame-step means anything (D612).
26924
+ *
26925
+ * **The only authority.** The per-clip `clipTransport` server message
26926
+ * that used to carry the same answer was removed: the broker's transport
26927
+ * choice reads nothing that varies per clip, so the clip level had no
26928
+ * information the provider does not already have, and two channels that
26929
+ * can disagree are worse than one (D62).
26930
+ *
26931
+ * Asked per camera and per provider, so it must stay CHEAP — it is a
26932
+ * statement about wiring, answered from a constant, never a call to the
26933
+ * camera. A provider answers with one of {@link CLIP_PLAYBACK_OPTIONS}
26934
+ * and never composes an envelope of its own.
26935
+ *
26936
+ * Optional, and absence is load-bearing: a provider that has not answered
26937
+ * has not restricted anything, and a viewer reads it as the freedom it
26938
+ * always had. See D612 on the rollout order that absence implies.
26939
+ */
26940
+ getPlaybackOptions: optionalMethod(z.object({
26941
+ deviceId: z.number(),
26942
+ /** WHICH provider to ask — the `addonId` a {@link ClipSourceSchema}
26943
+ * row carries. Required for the same reason `listClips` requires it:
26944
+ * a collection cap has no "the bound one" to resolve to (D554). */
26945
+ provider: z.string().min(1)
26946
+ }), ClipPlaybackOptionsSchema, {
26947
+ kind: "query",
26948
+ auth: "protected"
26773
26949
  })
26774
26950
  }
26775
26951
  };
@@ -28515,6 +28691,179 @@ getCredentials: method(z.object({ deviceId: z.number() }), CameraCredentialsSche
28515
28691
  }
28516
28692
  };
28517
28693
  /**
28694
+ * camera-grid-layout — the geometry of a COMPOSITE camera, on the camera's own
28695
+ * page.
28696
+ *
28697
+ * ## Why this is a capability and not an addon settings schema
28698
+ *
28699
+ * It was one, and it did not render. The addon declared the editor as a
28700
+ * `type: 'widget'` field inside its own `deviceSettingsSchema()`; the hub
28701
+ * returned that section correctly and `ConfigFormField` renders `type:'widget'`
28702
+ * perfectly well — and nothing ever asked camera-grid for it. The Cluster →
28703
+ * Pipeline → Device Overrides page interrogates a HAND-WRITTEN list of four
28704
+ * addons (`PIPELINE_CLUSTER_DEVICE_ADDONS`), whose own comment says an addon
28705
+ * not on it "falls off silently".
28706
+ *
28707
+ * Adding a fifth name to that list would have been the wrong fix twice over:
28708
+ * that page is per-camera DETECTION tuning, and a grid's geometry belongs
28709
+ * beside PTZ and motion zones on the camera itself. The device page is
28710
+ * BINDING-driven (D12), so the way in is a capability bound to the device —
28711
+ * and this cap carries its section the way `recording` does, by RETURNING it
28712
+ * from `getDeviceSettingsContribution`.
28713
+ *
28714
+ * Seven other widgets are still declared the other way, through a
28715
+ * `deviceConfig.ui` block the framework derives a section from. That route
28716
+ * gives the addon no say in where its own panel lands and no way to decline
28717
+ * for a device the panel does not suit, which is why this one does not use it.
28718
+ *
28719
+ * ## Why one addon may implement it
28720
+ *
28721
+ * It is a device-scoped NATIVE cap, registered by the grid camera device
28722
+ * itself. Nothing else declares a composite camera, so nothing else has a
28723
+ * layout — and the device-scoped route means the widget asks THE camera, not
28724
+ * "the camera-grid addon", which is what let the old custom-action pair be
28725
+ * reached only by a caller that already knew the addon id.
28726
+ *
28727
+ * ## The tab
28728
+ *
28729
+ * `streaming`, not a top-tab of its own. A grid's geometry IS what its stream
28730
+ * is, so the Streaming tab is where it belongs; a `grid` top-tab would need an
28731
+ * entry in `WELL_KNOWN_TAB_MAP` or the device page renders the raw id as the
28732
+ * label (measured on the robot camera, 2026-09-06 — a tab called "navigation"
28733
+ * next to "PTZ").
28734
+ */
28735
+ /** A rectangle in normalized [0,1] coordinates of whatever contains it. */
28736
+ var GridNormalizedRectSchema = z.object({
28737
+ x: z.number().min(0).max(1),
28738
+ y: z.number().min(0).max(1),
28739
+ width: z.number().gt(0).max(1),
28740
+ height: z.number().gt(0).max(1)
28741
+ });
28742
+ /**
28743
+ * One source camera, the part of its picture taken, and where that part lands.
28744
+ *
28745
+ * Both rectangles are NORMALIZED (D519): a source camera can change resolution
28746
+ * — a profile switch, a firmware update, a substream that comes back different
28747
+ * — and a stored PIXEL rectangle would quietly start cutting the wrong region,
28748
+ * which is the class of bug nobody files.
28749
+ */
28750
+ var GridLayoutCellSchema = z.object({
28751
+ deviceId: z.number().int().positive(),
28752
+ /** The part of the SOURCE taken, normalized against the source. */
28753
+ source: GridNormalizedRectSchema,
28754
+ /** Where it lands, normalized against the CANVAS. */
28755
+ cell: GridNormalizedRectSchema
28756
+ });
28757
+ /**
28758
+ * Which profiles this grid can actually compose, and why not.
28759
+ *
28760
+ * A grid's `high` composes its sources' `high` and its `low` their `low`, so a
28761
+ * profile is on offer only when EVERY source can serve it. The refusal NAMES
28762
+ * the sources, because "this grid has no low" is not a finding — "615 has no
28763
+ * low" is, and it is the one an operator can act on.
28764
+ */
28765
+ var GridProfileOfferSchema = z.object({
28766
+ profile: z.enum([
28767
+ "high",
28768
+ "mid",
28769
+ "low"
28770
+ ]),
28771
+ offered: z.boolean(),
28772
+ /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
28773
+ missingSources: z.array(z.number().int().positive()),
28774
+ /**
28775
+ * The canvas this profile composes onto, `WxH`, or empty when it is not
28776
+ * offered. DERIVED from the cells and the sources' own size at this profile —
28777
+ * it is reported because nothing else in the system would ever say what the
28778
+ * grid came out as, and because it is the number an operator would otherwise
28779
+ * expect to type.
28780
+ */
28781
+ canvas: z.string(),
28782
+ /**
28783
+ * Whether this profile is PUBLISHED, of the ones the grid could serve.
28784
+ *
28785
+ * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
28786
+ * a 4K canvas built from 4K decodes — something to opt into, not something a
28787
+ * viewer's adaptive should be handed by climbing to the top rung it can see.
28788
+ * Default is `mid` + `low`.
28789
+ */
28790
+ published: z.boolean()
28791
+ });
28792
+ var GridLayoutViewSchema = z.object({
28793
+ /** The persisted grid row this camera was declared from. */
28794
+ instanceId: z.string(),
28795
+ deviceId: z.number().int().nonnegative(),
28796
+ name: z.string(),
28797
+ /**
28798
+ * NO canvas size. A grid's resolution is not authored: each profile derives
28799
+ * its own from the cells and its sources' dimensions. The two numbers that
28800
+ * used to be here were a text field that silently decided both how much the
28801
+ * composite cost and how sharp it was — see `profiles[].canvas` for what it
28802
+ * came out as.
28803
+ */
28804
+ fps: z.number().int(),
28805
+ cells: z.array(GridLayoutCellSchema),
28806
+ /** What the catalog will publish, and what it refuses to. Read-only. */
28807
+ profiles: z.array(GridProfileOfferSchema)
28808
+ });
28809
+ var GridLayoutPatchSchema = z.object({
28810
+ deviceId: z.number().int().nonnegative(),
28811
+ name: z.string().min(1).max(160).optional(),
28812
+ fps: z.number().int().min(1).max(60).optional(),
28813
+ /** Which profiles to publish. See `GridProfileOffer.published`. */
28814
+ publishedProfiles: z.array(z.enum([
28815
+ "high",
28816
+ "mid",
28817
+ "low"
28818
+ ])).max(3).optional(),
28819
+ /**
28820
+ * The whole cell list at once. A per-cell patch would need an ordering the
28821
+ * editor does not have, and a half-applied layout is a picture nobody asked
28822
+ * for.
28823
+ */
28824
+ cells: z.array(GridLayoutCellSchema).max(16)
28825
+ });
28826
+ var cameraGridLayoutCapability = {
28827
+ name: "camera-grid-layout",
28828
+ scope: "device",
28829
+ deviceNative: true,
28830
+ mode: "singleton",
28831
+ deviceTypes: [DeviceType.Camera],
28832
+ /**
28833
+ * The section is built by the ADDON and returned from
28834
+ * `getDeviceSettingsContribution`, not derived by the framework from a
28835
+ * `deviceConfig.ui` block.
28836
+ *
28837
+ * Both mechanisms render the same widget. This one hands the addon two
28838
+ * things the framework-derived route cannot give it:
28839
+ *
28840
+ * - it chooses its own section, `tab`, `location` and `order`, the way any
28841
+ * other setting does, instead of receiving them from a cap declaration;
28842
+ * - it can DECLINE per device. A camera that is not a grid gets no section
28843
+ * at all, rather than a widget that renders its own "not a grid" state.
28844
+ *
28845
+ * `recording` is the precedent (`recorder/recording-device-settings.ts`): it
28846
+ * returns `null` for anything that is not a camera, so the Recording tab
28847
+ * never appears there.
28848
+ */
28849
+ exposesDeviceSettings: true,
28850
+ methods: {
28851
+ /**
28852
+ * The grid behind this device.
28853
+ *
28854
+ * `null` means ANSWERED and this camera is not a grid — not "not yet
28855
+ * known". The widget renders its "this is not a grid camera" state only
28856
+ * from this answer, never from an unresolved query (D315).
28857
+ */
28858
+ getLayout: method(z.object({ deviceId: z.number().int().nonnegative() }), GridLayoutViewSchema.nullable(), { auth: "admin" }),
28859
+ /** Write the geometry back. Returns the grid as it now stands, profiles included. */
28860
+ saveLayout: method(GridLayoutPatchSchema, GridLayoutViewSchema, {
28861
+ kind: "mutation",
28862
+ auth: "admin"
28863
+ })
28864
+ }
28865
+ };
28866
+ /**
28518
28867
  * Carbon-monoxide alarm sensor. Drives Home Assistant `binary_sensor`
28519
28868
  * entries with `device_class: carbon_monoxide`. Push-driven.
28520
28869
  */
@@ -29371,390 +29720,6 @@ var dayNightCapability = {
29371
29720
  volatileStateFields: ["lastFetchedAt"]
29372
29721
  };
29373
29722
  /**
29374
- * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
29375
- * writes to the CAMERA's own card, on the camera's own schedule.
29376
- *
29377
- * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
29378
- * footage ledger, our storage locations, our retention. This one has a
29379
- * different authority — the camera's firmware — and per D62 it stores
29380
- * nothing of its own. Every value here is read from the camera and every
29381
- * write goes back to the camera; there is no CamStack-side mirror that
29382
- * could disagree with the device.
29383
- *
29384
- * ## One shape, two firmwares
29385
- *
29386
- * Measured 2026-09-22 against the live fleet:
29387
- *
29388
- * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
29389
- * | --- | --- | --- |
29390
- * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
29391
- * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
29392
- * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
29393
- * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
29394
- * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
29395
- * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
29396
- * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
29397
- * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
29398
- *
29399
- * The two schedule models look different and are the same thing in
29400
- * different coordinates: both answer "for this trigger, during which
29401
- * weekly windows does the camera record". {@link RecordWindow} is that
29402
- * question in one shape — Hikvision's ranges map straight onto it,
29403
- * Reolink's mask expands into hour-aligned windows.
29404
- *
29405
- * ## Union, not intersection
29406
- *
29407
- * **The same fields exist on every camera.** What differs per device is
29408
- * which VALUES that device accepts, and that is what {@link
29409
- * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
29410
- * per field plus the schedule's own limits. A control a camera cannot
29411
- * honour is rendered DISABLED WITH ITS REASON, never missing and never
29412
- * dead: disabled must not look like broken.
29413
- *
29414
- * ## Refusal by name
29415
- *
29416
- * A write a camera cannot honour is refused with a sentence the operator
29417
- * can read — never accepted and dropped. Both providers refuse through
29418
- * {@link describeOnboardRefusal}, so the vocabulary is one function and
29419
- * one test, not two hand-written vendor opinions.
29420
- *
29421
- * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
29422
- * `getOptions` advertises per-camera availability, `getStatus` (auto-
29423
- * injected from `status`) reports the live values, and a single
29424
- * `setSettings` mutation applies a partial change. No hand-written
29425
- * settings-contribution methods.
29426
- */
29427
- /**
29428
- * What makes the camera start recording during a window.
29429
- *
29430
- * The union of both vendors' vocabularies. `continuous` is Hikvision's
29431
- * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
29432
- * object-class triggers are Reolink-only today and the smart-event ones
29433
- * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
29434
- * firmwares measured — a camera that cannot record on a trigger simply
29435
- * does not list it in `options.schedule.triggers`, and a window naming
29436
- * it is REFUSED, not dropped.
29437
- */
29438
- var RecordTriggerSchema = z.enum([
29439
- "continuous",
29440
- "motion",
29441
- "person",
29442
- "vehicle",
29443
- "animal",
29444
- "lineCrossing",
29445
- "intrusion",
29446
- "loitering",
29447
- "alarmInput"
29448
- ]);
29449
- /**
29450
- * One weekly recording window: "on `day`, from `startMinute` to
29451
- * `endMinute`, record on `trigger`".
29452
- *
29453
- * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
29454
- * both firmwares enumerate). Minutes are local camera time since
29455
- * midnight; `endMinute` may be 1440, meaning end of day — that is
29456
- * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
29457
- * collapsing it to 0 would turn a whole-day window into an empty one.
29458
- */
29459
- var RecordWindowSchema = z.object({
29460
- trigger: RecordTriggerSchema,
29461
- day: z.number().int().min(0).max(6),
29462
- startMinute: z.number().int().min(0).max(1439),
29463
- endMinute: z.number().int().min(1).max(1440)
29464
- });
29465
- /** Status of one physical volume, as the camera itself describes it. */
29466
- var OnboardStorageVolumeSchema = z.object({
29467
- /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
29468
- id: z.string(),
29469
- /** The camera's own name for it, when it gives one (`hddName`). */
29470
- label: z.string().optional(),
29471
- status: z.enum([
29472
- "ok",
29473
- "unformatted",
29474
- "error",
29475
- "offline",
29476
- "unknown"
29477
- ]),
29478
- /**
29479
- * Total size in MB, or **null when the camera did not say**.
29480
- *
29481
- * Never 0 for an unreadable value: a measurement that failed is not a
29482
- * measurement (D393), and a card whose size is unknown must not be
29483
- * rendered as a card of size zero.
29484
- */
29485
- capacityMb: z.number().nullable(),
29486
- /**
29487
- * Free space in MB, or null when unknown.
29488
- *
29489
- * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
29490
- * 1439 both report exactly 11776 MB free — the fixed reserve a looping
29491
- * card converges on once it has wrapped. At loop steady state the
29492
- * number is identical whether the camera recorded yesterday or stopped
29493
- * a month ago.
29494
- */
29495
- freeMb: z.number().nullable(),
29496
- /** True when the camera reports the volume writable (`property` RW). */
29497
- writable: z.boolean().optional()
29498
- });
29499
- /**
29500
- * Whether the camera has usable onboard storage.
29501
- *
29502
- * Three states, never two: an unreadable answer is `unknown`, never
29503
- * folded into `absent`. Folding a failed read into "no card" is how a
29504
- * healthy camera gets reported as broken, and the inverse is how a
29505
- * camera with no card reports `ok` and then an empty list (D393).
29506
- */
29507
- var OnboardStorageSchema = z.discriminatedUnion("kind", [
29508
- z.object({
29509
- kind: z.literal("present"),
29510
- volumes: z.array(OnboardStorageVolumeSchema)
29511
- }),
29512
- z.object({
29513
- kind: z.literal("absent"),
29514
- reason: z.string()
29515
- }),
29516
- z.object({
29517
- kind: z.literal("unknown"),
29518
- reason: z.string()
29519
- })
29520
- ]);
29521
- /**
29522
- * One recording track the camera exposes. Read-only.
29523
- *
29524
- * Hikvision lists several — 1436 and 3833 each list 101 AND 103, with
29525
- * DIFFERENT schedules (101 MOTION all day, 103 CMR 00:05–23:57) — so a
29526
- * cap that reported one number for "the schedule" would be lying on
29527
- * every Hikvision in the fleet. Reolink has exactly one per channel.
29528
- *
29529
- * There is deliberately **no `framerate`**. Hikvision declares it in the
29530
- * track `Description` and it is WRONG: 1436 says `2.260000 fps` while
29531
- * the media measures 12.55 fps. `codec` and `resolution` from the same
29532
- * string ARE right — those are the only two worth surfacing.
29533
- */
29534
- var OnboardRecordTrackSchema = z.object({
29535
- id: z.string(),
29536
- enabled: z.boolean(),
29537
- isVideo: z.boolean(),
29538
- /** From the camera's own track description. Null when it does not say. */
29539
- codec: z.string().nullable(),
29540
- resolution: z.string().nullable(),
29541
- /** Per-track overwrite flag, where the firmware keeps it per track. */
29542
- overwriteWhenFull: z.boolean().nullable()
29543
- });
29544
- /**
29545
- * What the camera is doing with its own storage, right now.
29546
- *
29547
- * Every scalar is nullable and **null means the camera did not answer**,
29548
- * never a default. A form that seeds `0` from an unanswered read invites
29549
- * the operator to save that 0 back onto the camera.
29550
- */
29551
- var RecordingOnboardStatusSchema = z.object({
29552
- storage: OnboardStorageSchema,
29553
- tracks: z.array(OnboardRecordTrackSchema),
29554
- /**
29555
- * The track the write path targets — the enabled VIDEO one. Null when
29556
- * no track could be identified, which is itself a refusal reason.
29557
- */
29558
- primaryTrackId: z.string().nullable(),
29559
- /** Master "record to the card at all" switch. */
29560
- enabled: z.boolean().nullable(),
29561
- overwriteWhenFull: z.boolean().nullable(),
29562
- preRecordSec: z.number().nullable(),
29563
- postRecordSec: z.number().nullable(),
29564
- /** Length of one recorded file, in minutes. */
29565
- segmentMinutes: z.number().nullable(),
29566
- /** The primary track's weekly windows, flattened. */
29567
- windows: z.array(RecordWindowSchema),
29568
- /**
29569
- * How many windows the camera described that CamStack could NOT read —
29570
- * an unrecognised trigger, an unparseable clock, a weekday it does not
29571
- * name.
29572
- *
29573
- * A dropped window is work the reader threw away, and a schedule that
29574
- * silently shows fewer rows than the camera holds is how an operator
29575
- * saves back a schedule shorter than the one they were looking at
29576
- * (D391). Non-zero means the window list is INCOMPLETE and a write
29577
- * that replaces it would delete what was not shown — which is why a
29578
- * provider reporting a non-zero count also reports the schedule as not
29579
- * writable.
29580
- */
29581
- unreadableWindows: z.number(),
29582
- /**
29583
- * The camera is scheduled to record and has NO usable storage.
29584
- *
29585
- * A first-class fact because it is the fleet's most common silent
29586
- * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
29587
- * to a card that is not there. Neither the schedule nor the storage
29588
- * read says anything wrong on its own; only the pair does.
29589
- */
29590
- recordingToNowhere: z.boolean(),
29591
- lastFetchedAt: z.number()
29592
- });
29593
- /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
29594
- var RangeSchema = z.object({
29595
- min: z.number(),
29596
- max: z.number(),
29597
- step: z.number()
29598
- });
29599
- /**
29600
- * The values a camera actually takes for a numeric field, when they are a SET
29601
- * rather than a range.
29602
- *
29603
- * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
29604
- * (I91DN) on 2026-09-22 by writing each value and reading it back:
29605
- *
29606
- * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
29607
- * camera's "no limit" — `-1` and `4294967295` both land on it);
29608
- * - post-record: `5, 10, 30, 60, 120, 300, 600`.
29609
- *
29610
- * Neither is expressible as a step: the first has a sentinel two billion away
29611
- * from its neighbours, the second doubles and then jumps. A range that tried
29612
- * would forbid values the camera takes AND permit values it silently replaces
29613
- * with 5 — wrong in both directions at once.
29614
- *
29615
- * `sentinel` names the member that is not a duration, so a surface can render
29616
- * "no limit" instead of `2147483647` seconds.
29617
- */
29618
- var AllowedValuesSchema = z.object({
29619
- values: z.array(z.number()).min(1),
29620
- sentinel: z.object({
29621
- value: z.number(),
29622
- meaning: z.enum(["no-limit", "disabled"])
29623
- }).optional()
29624
- });
29625
- /**
29626
- * Per-field availability on ONE camera.
29627
- *
29628
- * The field exists on every camera — this says whether this one can be
29629
- * read and whether it can be written, and `reason` says why not when
29630
- * either is false. The UI renders the control DISABLED with the reason
29631
- * rather than hiding it, so a limitation is legible instead of looking
29632
- * like a missing feature.
29633
- */
29634
- var OnboardFieldSupportSchema = z.object({
29635
- readable: z.boolean(),
29636
- writable: z.boolean(),
29637
- /** Required whenever `readable` or `writable` is false. */
29638
- reason: z.string().optional()
29639
- });
29640
- /** What this camera's schedule model can express. */
29641
- var OnboardScheduleSupportSchema = z.object({
29642
- support: OnboardFieldSupportSchema,
29643
- /**
29644
- * The smallest time step the camera can express, in minutes.
29645
- *
29646
- * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
29647
- * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
29648
- * window whose edges are not a multiple of this is REFUSED rather than
29649
- * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
29650
- * and nothing says so.
29651
- */
29652
- granularityMinutes: z.number(),
29653
- /** Triggers this camera can record on. A window naming another is refused. */
29654
- triggers: z.array(RecordTriggerSchema),
29655
- /**
29656
- * False when the camera stores ONE trigger per time range, so two
29657
- * windows overlapping on the same day cannot carry different triggers.
29658
- * True on Reolink, whose mask is per-trigger and independent.
29659
- */
29660
- supportsOverlappingTriggers: z.boolean()
29661
- });
29662
- var RecordingOnboardOptionsSchema = z.object({
29663
- enabled: OnboardFieldSupportSchema,
29664
- overwriteWhenFull: OnboardFieldSupportSchema,
29665
- preRecordSec: OnboardFieldSupportSchema,
29666
- preRecordSecRange: RangeSchema.optional(),
29667
- /** Preferred over the range when the camera takes a SET, not a span. */
29668
- preRecordSecAllowed: AllowedValuesSchema.optional(),
29669
- postRecordSec: OnboardFieldSupportSchema,
29670
- postRecordSecRange: RangeSchema.optional(),
29671
- /** Preferred over the range when the camera takes a SET, not a span. */
29672
- postRecordSecAllowed: AllowedValuesSchema.optional(),
29673
- segmentMinutes: OnboardFieldSupportSchema,
29674
- segmentMinutesRange: RangeSchema.optional(),
29675
- /** Preferred over the range when the camera takes a SET, not a span. */
29676
- segmentMinutesAllowed: AllowedValuesSchema.optional(),
29677
- schedule: OnboardScheduleSupportSchema
29678
- });
29679
- /**
29680
- * A partial change. Every field optional.
29681
- *
29682
- * Unlike the other `deviceConfig` caps, a provider here does **NOT**
29683
- * silently ignore a field it cannot support — it refuses, by name,
29684
- * through {@link describeOnboardRefusal}. Silence on a recording setting
29685
- * is the failure D62 exists to prevent: the operator believes the camera
29686
- * is recording the way the form says, and it is not.
29687
- */
29688
- var RecordingOnboardPatchSchema = z.object({
29689
- enabled: z.boolean().optional(),
29690
- overwriteWhenFull: z.boolean().optional(),
29691
- preRecordSec: z.number().optional(),
29692
- postRecordSec: z.number().optional(),
29693
- segmentMinutes: z.number().optional(),
29694
- /** The complete new window set for the primary track — not a delta. */
29695
- windows: z.array(RecordWindowSchema).optional()
29696
- });
29697
- var recordingOnboardCapability = {
29698
- name: "recording-onboard",
29699
- scope: "device",
29700
- deviceNative: true,
29701
- mode: "singleton",
29702
- deviceTypes: [DeviceType.Camera],
29703
- deviceConfig: { ui: {
29704
- kind: "derived-form",
29705
- builderId: "recording-onboard",
29706
- tab: "recording"
29707
- } },
29708
- methods: {
29709
- getOptions: method(z.object({ deviceId: z.number() }), RecordingOnboardOptionsSchema),
29710
- setSettings: method(z.object({
29711
- deviceId: z.number(),
29712
- settings: RecordingOnboardPatchSchema
29713
- }), z.void(), {
29714
- kind: "mutation",
29715
- auth: "admin"
29716
- })
29717
- },
29718
- status: {
29719
- schema: RecordingOnboardStatusSchema,
29720
- kind: "poll"
29721
- },
29722
- runtimeState: RecordingOnboardStatusSchema,
29723
- /**
29724
- * Runtime-state durability: **restored** — operator-set camera-side
29725
- * recording config; mutation-driven, and the storage half is the last
29726
- * thing the camera said about its own card.
29727
- *
29728
- * See `RuntimeStateDurability`. Enforced by
29729
- * `scripts/check-runtime-state-durability.ts`.
29730
- */
29731
- durability: "restored",
29732
- /** Clock fields: written, but excluded from the compare that decides
29733
- * whether persisting is worth a SQLite commit. */
29734
- volatileStateFields: ["lastFetchedAt"]
29735
- };
29736
- /** Minutes in a day. `endMinute === 1440` means "to end of day". */
29737
- var MINUTES_PER_DAY = 1440;
29738
- /**
29739
- * Day-of-week names in the cap's index order (0 = Monday), for messages
29740
- * an operator reads and for Hikvision's `<DayOfWeek>` element.
29741
- */
29742
- var DAY_NAMES = [
29743
- "Monday",
29744
- "Tuesday",
29745
- "Wednesday",
29746
- "Thursday",
29747
- "Friday",
29748
- "Saturday",
29749
- "Sunday"
29750
- ];
29751
- /** `510` → `08:30`. For messages, not for the wire. */
29752
- function formatMinutes(minute) {
29753
- const hour = Math.floor(minute / 60);
29754
- const rest = minute % 60;
29755
- return `${String(hour).padStart(2, "0")}:${String(rest).padStart(2, "0")}`;
29756
- }
29757
- /**
29758
29723
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
29759
29724
  * for every device, regardless of provider — the kernel needs a uniform
29760
29725
  * cap-keyed slice for the basic device flags every consumer expects to
@@ -29973,40 +29938,6 @@ var eventEmitterCapability = {
29973
29938
  */
29974
29939
  durability: "session"
29975
29940
  };
29976
- var EventItemSchema = z.object({
29977
- id: z.string(),
29978
- type: z.string(),
29979
- timestamp: z.number(),
29980
- label: z.string().optional(),
29981
- thumbnailUrl: z.string().optional(),
29982
- clipUrl: z.string().optional(),
29983
- metadata: z.record(z.string(), z.unknown()).optional()
29984
- });
29985
- var eventsCapability = {
29986
- name: "events",
29987
- scope: "device",
29988
- mode: "singleton",
29989
- deviceTypes: [DeviceType.Camera],
29990
- methods: {
29991
- getEvents: method(z.object({
29992
- deviceId: z.number(),
29993
- from: z.number().optional(),
29994
- to: z.number().optional(),
29995
- limit: z.number().optional()
29996
- }), z.array(EventItemSchema)),
29997
- getEventThumbnail: method(z.object({
29998
- deviceId: z.number(),
29999
- eventId: z.string()
30000
- }), z.object({
30001
- base64: z.string(),
30002
- contentType: z.string()
30003
- }).nullable()),
30004
- getEventClipUrl: method(z.object({
30005
- deviceId: z.number(),
30006
- eventId: z.string()
30007
- }), z.string().nullable())
30008
- }
30009
- };
30010
29941
  var IdentitySchema = z.object({
30011
29942
  id: z.string(),
30012
29943
  name: z.string(),
@@ -32540,179 +32471,6 @@ var motionTriggerCapability = {
32540
32471
  durability: "session"
32541
32472
  };
32542
32473
  /**
32543
- * camera-grid-layout — the geometry of a COMPOSITE camera, on the camera's own
32544
- * page.
32545
- *
32546
- * ## Why this is a capability and not an addon settings schema
32547
- *
32548
- * It was one, and it did not render. The addon declared the editor as a
32549
- * `type: 'widget'` field inside its own `deviceSettingsSchema()`; the hub
32550
- * returned that section correctly and `ConfigFormField` renders `type:'widget'`
32551
- * perfectly well — and nothing ever asked camera-grid for it. The Cluster →
32552
- * Pipeline → Device Overrides page interrogates a HAND-WRITTEN list of four
32553
- * addons (`PIPELINE_CLUSTER_DEVICE_ADDONS`), whose own comment says an addon
32554
- * not on it "falls off silently".
32555
- *
32556
- * Adding a fifth name to that list would have been the wrong fix twice over:
32557
- * that page is per-camera DETECTION tuning, and a grid's geometry belongs
32558
- * beside PTZ and motion zones on the camera itself. The device page is
32559
- * BINDING-driven (D12), so the way in is a capability bound to the device —
32560
- * and this cap carries its section the way `recording` does, by RETURNING it
32561
- * from `getDeviceSettingsContribution`.
32562
- *
32563
- * Seven other widgets are still declared the other way, through a
32564
- * `deviceConfig.ui` block the framework derives a section from. That route
32565
- * gives the addon no say in where its own panel lands and no way to decline
32566
- * for a device the panel does not suit, which is why this one does not use it.
32567
- *
32568
- * ## Why one addon may implement it
32569
- *
32570
- * It is a device-scoped NATIVE cap, registered by the grid camera device
32571
- * itself. Nothing else declares a composite camera, so nothing else has a
32572
- * layout — and the device-scoped route means the widget asks THE camera, not
32573
- * "the camera-grid addon", which is what let the old custom-action pair be
32574
- * reached only by a caller that already knew the addon id.
32575
- *
32576
- * ## The tab
32577
- *
32578
- * `streaming`, not a top-tab of its own. A grid's geometry IS what its stream
32579
- * is, so the Streaming tab is where it belongs; a `grid` top-tab would need an
32580
- * entry in `WELL_KNOWN_TAB_MAP` or the device page renders the raw id as the
32581
- * label (measured on the robot camera, 2026-09-06 — a tab called "navigation"
32582
- * next to "PTZ").
32583
- */
32584
- /** A rectangle in normalized [0,1] coordinates of whatever contains it. */
32585
- var GridNormalizedRectSchema = z.object({
32586
- x: z.number().min(0).max(1),
32587
- y: z.number().min(0).max(1),
32588
- width: z.number().gt(0).max(1),
32589
- height: z.number().gt(0).max(1)
32590
- });
32591
- /**
32592
- * One source camera, the part of its picture taken, and where that part lands.
32593
- *
32594
- * Both rectangles are NORMALIZED (D519): a source camera can change resolution
32595
- * — a profile switch, a firmware update, a substream that comes back different
32596
- * — and a stored PIXEL rectangle would quietly start cutting the wrong region,
32597
- * which is the class of bug nobody files.
32598
- */
32599
- var GridLayoutCellSchema = z.object({
32600
- deviceId: z.number().int().positive(),
32601
- /** The part of the SOURCE taken, normalized against the source. */
32602
- source: GridNormalizedRectSchema,
32603
- /** Where it lands, normalized against the CANVAS. */
32604
- cell: GridNormalizedRectSchema
32605
- });
32606
- /**
32607
- * Which profiles this grid can actually compose, and why not.
32608
- *
32609
- * A grid's `high` composes its sources' `high` and its `low` their `low`, so a
32610
- * profile is on offer only when EVERY source can serve it. The refusal NAMES
32611
- * the sources, because "this grid has no low" is not a finding — "615 has no
32612
- * low" is, and it is the one an operator can act on.
32613
- */
32614
- var GridProfileOfferSchema = z.object({
32615
- profile: z.enum([
32616
- "high",
32617
- "mid",
32618
- "low"
32619
- ]),
32620
- offered: z.boolean(),
32621
- /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
32622
- missingSources: z.array(z.number().int().positive()),
32623
- /**
32624
- * The canvas this profile composes onto, `WxH`, or empty when it is not
32625
- * offered. DERIVED from the cells and the sources' own size at this profile —
32626
- * it is reported because nothing else in the system would ever say what the
32627
- * grid came out as, and because it is the number an operator would otherwise
32628
- * expect to type.
32629
- */
32630
- canvas: z.string(),
32631
- /**
32632
- * Whether this profile is PUBLISHED, of the ones the grid could serve.
32633
- *
32634
- * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
32635
- * a 4K canvas built from 4K decodes — something to opt into, not something a
32636
- * viewer's adaptive should be handed by climbing to the top rung it can see.
32637
- * Default is `mid` + `low`.
32638
- */
32639
- published: z.boolean()
32640
- });
32641
- var GridLayoutViewSchema = z.object({
32642
- /** The persisted grid row this camera was declared from. */
32643
- instanceId: z.string(),
32644
- deviceId: z.number().int().nonnegative(),
32645
- name: z.string(),
32646
- /**
32647
- * NO canvas size. A grid's resolution is not authored: each profile derives
32648
- * its own from the cells and its sources' dimensions. The two numbers that
32649
- * used to be here were a text field that silently decided both how much the
32650
- * composite cost and how sharp it was — see `profiles[].canvas` for what it
32651
- * came out as.
32652
- */
32653
- fps: z.number().int(),
32654
- cells: z.array(GridLayoutCellSchema),
32655
- /** What the catalog will publish, and what it refuses to. Read-only. */
32656
- profiles: z.array(GridProfileOfferSchema)
32657
- });
32658
- var GridLayoutPatchSchema = z.object({
32659
- deviceId: z.number().int().nonnegative(),
32660
- name: z.string().min(1).max(160).optional(),
32661
- fps: z.number().int().min(1).max(60).optional(),
32662
- /** Which profiles to publish. See `GridProfileOffer.published`. */
32663
- publishedProfiles: z.array(z.enum([
32664
- "high",
32665
- "mid",
32666
- "low"
32667
- ])).max(3).optional(),
32668
- /**
32669
- * The whole cell list at once. A per-cell patch would need an ordering the
32670
- * editor does not have, and a half-applied layout is a picture nobody asked
32671
- * for.
32672
- */
32673
- cells: z.array(GridLayoutCellSchema).max(16)
32674
- });
32675
- var cameraGridLayoutCapability = {
32676
- name: "camera-grid-layout",
32677
- scope: "device",
32678
- deviceNative: true,
32679
- mode: "singleton",
32680
- deviceTypes: [DeviceType.Camera],
32681
- /**
32682
- * The section is built by the ADDON and returned from
32683
- * `getDeviceSettingsContribution`, not derived by the framework from a
32684
- * `deviceConfig.ui` block.
32685
- *
32686
- * Both mechanisms render the same widget. This one hands the addon two
32687
- * things the framework-derived route cannot give it:
32688
- *
32689
- * - it chooses its own section, `tab`, `location` and `order`, the way any
32690
- * other setting does, instead of receiving them from a cap declaration;
32691
- * - it can DECLINE per device. A camera that is not a grid gets no section
32692
- * at all, rather than a widget that renders its own "not a grid" state.
32693
- *
32694
- * `recording` is the precedent (`recorder/recording-device-settings.ts`): it
32695
- * returns `null` for anything that is not a camera, so the Recording tab
32696
- * never appears there.
32697
- */
32698
- exposesDeviceSettings: true,
32699
- methods: {
32700
- /**
32701
- * The grid behind this device.
32702
- *
32703
- * `null` means ANSWERED and this camera is not a grid — not "not yet
32704
- * known". The widget renders its "this is not a grid camera" state only
32705
- * from this answer, never from an unresolved query (D315).
32706
- */
32707
- getLayout: method(z.object({ deviceId: z.number().int().nonnegative() }), GridLayoutViewSchema.nullable(), { auth: "admin" }),
32708
- /** Write the geometry back. Returns the grid as it now stands, profiles included. */
32709
- saveLayout: method(GridLayoutPatchSchema, GridLayoutViewSchema, {
32710
- kind: "mutation",
32711
- auth: "admin"
32712
- })
32713
- }
32714
- };
32715
- /**
32716
32474
  * Motion-zones share the same MaskShape vocabulary as privacy-mask — the
32717
32475
  * on-camera motion-detection mask is a single `grid` region (a row-major
32718
32476
  * boolean cell lattice the camera's onboard VMD evaluates). Composing it as
@@ -35421,37 +35179,40 @@ var rebootCapability = {
35421
35179
  auth: "admin"
35422
35180
  }) }
35423
35181
  };
35424
- /**
35425
- * `recording` cap — footage availability + HLS playback manifests + per-device
35426
- * recording config. NOTE on events (source of truth, R5/C3): this cap carries
35427
- * NO event surface — `getPlaybackManifest` returns playlist URLs only. Timeline
35428
- * events (motion/object/audio) come from `pipelineAnalytics` (durable SQLite
35429
- * rows) and are the ONLY event surface — the recorder has none. The in-RAM
35430
- * playback markers it used to build were deleted on 2026-08-29 because nothing
35431
- * ever read them. Event<->footage joins are by time, padded with the shared
35432
- * `EVENT_PAD_MS` (`interfaces/recording-config.ts`).
35433
- */
35434
- var RecordingStatusSchema = z.object({
35435
- deviceId: z.number(),
35436
- enabled: z.boolean(),
35437
- /** THE derived storage mode, from the one definition
35438
- * (`deriveRecordingMode`) — never a second enum. A duplicated list is how
35439
- * `on-device-decision` could have reached the recorder and not the status. */
35440
- activeMode: RecordingStorageModeSchema,
35441
- nodeId: z.string(),
35442
- storageBytes: z.number()
35443
- });
35182
+ /** The one addon allowed to serve {@link RECORDING_SOURCE_CAMSTACK}: the
35183
+ * recorder, which is the addon that writes the footage (D560). */
35184
+ var RECORDING_SOURCE_CAMSTACK_ADDON = "recorder";
35444
35185
  var RecordingRangeSchema = z.object({
35445
35186
  profile: z.string(),
35446
35187
  startMs: z.number(),
35447
35188
  endMs: z.number()
35448
35189
  });
35190
+ /**
35191
+ * How a source ANSWERED, on every singular read of this cap.
35192
+ *
35193
+ * `'read'` — it looked. `ranges: []` / `days: []` is then a real claim: this
35194
+ * source has no coverage in the window. `'unreadable'` — nobody could look
35195
+ * (the camera was unreachable, the calendar rung threw, the location is
35196
+ * unmounted, the node is still on the old build), and the emptiness beside it
35197
+ * means NOTHING.
35198
+ *
35199
+ * The batch rows have carried this since the grid existed; the SINGULAR
35200
+ * answers gained it with the collection (D625 §10.4), because they are the
35201
+ * ones the single-camera picker uses and because a half-converted fleet makes
35202
+ * "nobody looked" common for the length of a deploy. Without it the timeline
35203
+ * has no vocabulary for it, and `(data ?? [])` in a viewer turns a rollout into
35204
+ * a fleet of cameras that appear to have lost their recordings (D315, D393).
35205
+ */
35206
+ var RecordingReadSchema = z.enum(["read", "unreadable"]);
35449
35207
  var RecordingAvailabilitySchema = z.object({
35450
35208
  deviceId: z.number(),
35209
+ /** See {@link RecordingReadSchema}. An `'unreadable'` answer carries an empty
35210
+ * `ranges` that means nothing — never draw it as "no footage". */
35211
+ read: RecordingReadSchema,
35451
35212
  ranges: z.array(RecordingRangeSchema),
35452
35213
  /**
35453
- * Every profile this camera has footage in — not only the one `ranges`
35454
- * describes (D433).
35214
+ * Every profile this camera has footage in AT THIS SOURCE — not only the one
35215
+ * `ranges` describes (D433).
35455
35216
  *
35456
35217
  * `ranges` answers for ONE profile by design: the timeline is a single bar,
35457
35218
  * and enumerating all of them triples the directory reads for a bar that
@@ -35468,15 +35229,344 @@ var RecordingAvailabilitySchema = z.object({
35468
35229
  });
35469
35230
  var RecordingDaysSchema = z.object({
35470
35231
  deviceId: z.number(),
35232
+ /** See {@link RecordingReadSchema}. `days: []` on an `'unreadable'` answer is
35233
+ * "nobody could look", and the date-picker must not spell it the same as
35234
+ * "no footage this month". */
35235
+ read: RecordingReadSchema,
35471
35236
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
35472
35237
  days: z.array(z.number())
35473
35238
  });
35239
+ var RecordingManifestSchema = z.object({
35240
+ deviceId: z.number(),
35241
+ /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
35242
+ localMasterPath: z.string().nullable(),
35243
+ /** HTTP(S) URL to the master playlist on the recording node's playback server
35244
+ * (the PRIMARY candidate); null when no recording / server. Carries the
35245
+ * scoped playback token in its path. */
35246
+ playbackUrl: z.string().nullable(),
35247
+ /**
35248
+ * Candidate master-playlist URLs the client tries in order (LAN first, then
35249
+ * remote — Tailscale/Cloudflare if the operator configured extra hosts), each
35250
+ * carrying the same scoped token. `playbackUrl` is the first entry. Empty when
35251
+ * there is no recording / server.
35252
+ */
35253
+ playbackEndpoints: z.array(z.string())
35254
+ });
35255
+ var RecordingSourceAvailabilitySchema = z.object({
35256
+ state: z.enum([
35257
+ "ok",
35258
+ "sleeping",
35259
+ "unreachable",
35260
+ "no-storage",
35261
+ "index-empty"
35262
+ ]),
35263
+ /** Free text, shown verbatim. Names the camera's own refusal when there is one. */
35264
+ reason: z.string().optional(),
35265
+ /** When this source's coverage was last CONFIRMED. A cached answer is never
35266
+ * drawn as current: the surface shows the age whenever it is older than the
35267
+ * refresh interval. The clip catalog's `catalogAsOf`, under the name the
35268
+ * timeline uses for it. */
35269
+ coverageAsOf: z.number().optional()
35270
+ });
35271
+ /**
35272
+ * One SOURCE of recorded coverage for a camera — a row of the picker.
35273
+ *
35274
+ * A provider lists the sources IT serves for that device, and answers for each
35275
+ * of them whether it can answer at all. A provider with nothing to offer on a
35276
+ * camera returns `[]` — it is not that camera's business. The five availability
35277
+ * states are `ClipSourceAvailability`'s verbatim: they mean exactly the same
35278
+ * things about a coverage index as about a clip catalog, and `sleeping` in
35279
+ * particular is what stops a battery camera being woken to paint a bar.
35280
+ */
35281
+ var RecordingSourceSchema = z.object({
35282
+ /** The source id. {@link RECORDING_SOURCE_CAMSTACK} for ours (RESERVED), a
35283
+ * vendor namespace (`native:reolink:onboard`, …) for a camera's own store. */
35284
+ source: z.string(),
35285
+ /** Operator-facing name of the source ("CamStack recordings", "SD card"). */
35286
+ label: z.string(),
35287
+ /**
35288
+ * The addon that SERVES this row, and the value a later call passes as
35289
+ * `provider`.
35290
+ *
35291
+ * Optional for version skew only. The collection dispatcher stamps it from
35292
+ * the registry, so a row that travelled through the fan-out carries the
35293
+ * authoritative id whatever the provider filled in (D557 §4).
35294
+ */
35295
+ addonId: z.string().optional(),
35296
+ availability: RecordingSourceAvailabilitySchema
35297
+ });
35298
+ /**
35299
+ * What a surface may DRAW for this (camera, source) — D612's rule applied to a
35300
+ * timeline: **the source declares what it can do, and the surface draws what
35301
+ * was declared. It never assumes, and never offers a gesture it will then
35302
+ * refuse.** D612 exists because `8` and `16` were offered as clip rates, the
35303
+ * broker clamped them to `4`, and no line anywhere said so.
35304
+ *
35305
+ * Asked once per (camera, source) before anything is drawn — never replaced by
35306
+ * a constant the surface keeps, which is the second authority D612 ends.
35307
+ */
35308
+ var RecordingSourceOptionsSchema = z.object({
35309
+ /** How this source's media reaches the player.
35310
+ * `archive` = our own indexed segment tree; `stream` = the provider's
35311
+ * forward-only fMP4 (D597); `realtime` = a replay bound to wall clock. */
35312
+ transport: z.enum([
35313
+ "archive",
35314
+ "stream",
35315
+ "realtime"
35316
+ ]),
35317
+ /** What the BAR means. `continuous` = gaps are holes in a recording;
35318
+ * `sparse` = gaps are the absence of one, and must be drawn as such.
35319
+ *
35320
+ * Not an onboard-only concession: measured 2026-09-24, OUR bar covers 98.8 %
35321
+ * of 592's day and 1.2 % of 1436's. It is a fact about a (source, camera)
35322
+ * pair, and ours answers it per camera from `deriveRecordingMode`. */
35323
+ coverage: z.enum(["continuous", "sparse"]),
35324
+ /** Where the playhead may be put.
35325
+ * `free` — anywhere, to the frame.
35326
+ * `forward` — only ahead of the current position.
35327
+ * `segment` — a position SNAPS to the head of the covering segment; a finer
35328
+ * ask is accepted by the camera and SILENTLY IGNORED. Measured
35329
+ * on 1436 (Hikvision V5.7.1, 2026-09-23): a window-narrowed
35330
+ * `ContentMgmt/search` returns a row and a `playbackURI`, the
35331
+ * replay opens 200 and delivers media — and the burned-in OSD of
35332
+ * the first frame reads the SEGMENT HEAD every time. Calling
35333
+ * that `forward` would tell the surface it may move the playhead
35334
+ * ahead within a loaded segment, which it may not. */
35335
+ seek: z.enum([
35336
+ "free",
35337
+ "forward",
35338
+ "segment"
35339
+ ]),
35340
+ /** Frame-step BACKWARD is meaningful. */
35341
+ stepBack: z.boolean(),
35342
+ /** Whether the drag-scrub gesture is served, as opposed to refused by name. */
35343
+ scrub: z.boolean(),
35344
+ /** Deliverable rates, ascending, always containing `1`. The surface draws its
35345
+ * picker from this and from NOTHING else (D612, D620, D621). `0` is not a
35346
+ * member: pause is the absence of a rate. */
35347
+ rates: z.array(z.number().positive()).min(1).readonly(),
35348
+ /** TRUE when a read of this source HOLDS the camera's only playback session.
35349
+ * A surface with this set makes at most ONE read at a time and draws no
35350
+ * scrub-thumbnail strip, no hover preview, no prefetch and no background
35351
+ * refresh. The precedent is exact and expensive: filling one screen of
35352
+ * Hikvision thumbnails at 1.01× realtime consumed fifteen minutes of that
35353
+ * camera's only playback session (1.2.126, reported within minutes), and a
35354
+ * timeline is a screenful of reads by construction. */
35355
+ exclusive: z.boolean()
35356
+ });
35357
+ /**
35358
+ * How to PLAY the instant that was asked for, from the chosen source.
35359
+ *
35360
+ * No new media transport is built for onboard sources: the `clip` arm is a
35361
+ * DELEGATION to the `videoclips` transport that vendor already has (D597 /
35362
+ * D616 / D617). The onboard half of this collection is a PROJECTION of
35363
+ * `videoclips` for coverage and a delegation to it for bytes.
35364
+ */
35365
+ var RecordingPlaybackSchema = z.discriminatedUnion("kind", [
35366
+ z.object({
35367
+ kind: z.literal("hls"),
35368
+ manifest: RecordingManifestSchema
35369
+ }),
35370
+ z.object({
35371
+ kind: z.literal("clip"),
35372
+ /** The `videoclips` source namespace this clip id belongs to. */
35373
+ source: z.string(),
35374
+ clipId: z.string(),
35375
+ /** Where this clip actually STARTS. On a `seek: 'segment'` source the
35376
+ * playhead lands here, not at the requested instant — the surface must be
35377
+ * TOLD, not left to discover it from a burned-in OSD. */
35378
+ startsAtMs: z.number()
35379
+ }),
35380
+ z.object({
35381
+ kind: z.literal("none"),
35382
+ reason: z.string()
35383
+ })
35384
+ ]);
35385
+ var recordingCapability = {
35386
+ name: "recording",
35387
+ scope: "device",
35388
+ /** Several sources per camera, listed beside each other. The mount stays
35389
+ * `device-scoped` — see `resolveCapMount`'s ordering and D554: per-device
35390
+ * wins over the global collection fan-out. */
35391
+ mode: "collection",
35392
+ kind: "wrapper",
35393
+ defaultActive: true,
35394
+ /** Recorded coverage is a property of a camera — the cap is meaningless on a
35395
+ * sensor, a button or an event emitter, and the `defaultActive` auto-bind
35396
+ * reads this to decide which devices it may claim. */
35397
+ deviceTypes: [DeviceType.Camera],
35398
+ methods: {
35399
+ /**
35400
+ * The sources this camera has, WITH the reason any of them cannot answer.
35401
+ *
35402
+ * Asked separately from `getAvailability` because an empty bar is
35403
+ * ambiguous and this is the only place the ambiguity is resolved: every
35404
+ * bound provider contributes its own rows, and a provider that could not be
35405
+ * reached at all still produces one row saying so. A surface that draws "no
35406
+ * recordings" without reading this is drawing a guess.
35407
+ *
35408
+ * The ONLY method here without a `provider` — it is the call that tells the
35409
+ * caller what to put there.
35410
+ */
35411
+ listSources: method(z.object({ deviceId: z.number() }), z.array(RecordingSourceSchema).readonly(), {
35412
+ kind: "query",
35413
+ auth: "protected"
35414
+ }),
35415
+ /**
35416
+ * Recorded coverage of `[fromMs, toMs)` at ONE source — the timeline bar.
35417
+ *
35418
+ * `protected`, not `admin`: a per-camera read is exactly what a camera
35419
+ * viewer is FOR, and the device-scoped mount routes through
35420
+ * `getProviderForDevice`, so a camera outside the caller's scope is refused
35421
+ * before a provider is reached.
35422
+ */
35423
+ getAvailability: method(z.object({
35424
+ deviceId: z.number(),
35425
+ /**
35426
+ * WHICH provider to ask — the `addonId` a {@link RecordingSourceSchema}
35427
+ * row carries, never a source id and never a list. **REQUIRED**, in the
35428
+ * schema, where the generated types make it unomittable rather than
35429
+ * merely discouraged (D554 amended).
35430
+ *
35431
+ * It was learned the expensive way on `videoclips.listClips`: measured
35432
+ * on the live hub 2026-09-20, device 592 bound to `recorder` AND
35433
+ * `provider-reolink`, a bare call with `limit: 3` answered SIX rows,
35434
+ * three from each source, merged — `device-collection-dispatch.ts`
35435
+ * leaves an unpinned fan-out un-narrowed, so absence buys the union the
35436
+ * method exists not to be. An un-narrowed `getAvailability` would do
35437
+ * that to a TIMELINE: our ranges and the card's clips unioned into one
35438
+ * bar, which is "two sources are never drawn together" broken in the
35439
+ * one place it matters most.
35440
+ *
35441
+ * A provider the device is not bound to is refused BY NAME (D552's
35442
+ * `rejectUnresolvedAddonPin`), never answered by another one.
35443
+ */
35444
+ provider: z.string().min(1),
35445
+ fromMs: z.number(),
35446
+ toMs: z.number(),
35447
+ /**
35448
+ * Answer for THIS profile instead of the source's preferred one (D433).
35449
+ * Absent keeps the timeline's behaviour — one bar, one profile, one set
35450
+ * of reads. `profilesWithFootage` on the answer says what may be asked
35451
+ * for.
35452
+ */
35453
+ profile: z.string().optional()
35454
+ }), RecordingAvailabilitySchema, {
35455
+ kind: "query",
35456
+ auth: "protected"
35457
+ }),
35458
+ /** Which calendar days in [fromMs,toMs) this source has ≥1 recording in,
35459
+ * bucketed by the client's local day (`tzOffsetMinutes` = minutes to add
35460
+ * to UTC). Drives the theater date-picker's day dots. `provider` is
35461
+ * REQUIRED for the reason `getAvailability` states. */
35462
+ getDaysWithRecordings: method(z.object({
35463
+ deviceId: z.number(),
35464
+ provider: z.string().min(1),
35465
+ fromMs: z.number(),
35466
+ toMs: z.number(),
35467
+ tzOffsetMinutes: z.number()
35468
+ }), RecordingDaysSchema, {
35469
+ kind: "query",
35470
+ auth: "protected"
35471
+ }),
35472
+ /**
35473
+ * How to PLAY `[fromMs, toMs)` at this source.
35474
+ *
35475
+ * It was `getPlaybackManifest`, and the rename is not cosmetic: a
35476
+ * "manifest" is an HLS master playlist, which is a property of OUR recorder
35477
+ * and of nothing else. Keeping the name would make every onboard
35478
+ * implementation a lie in its signature. The old shape survives verbatim
35479
+ * inside the union's `hls` arm, so the recorder's implementation is
35480
+ * unchanged behind it.
35481
+ */
35482
+ getPlayback: method(z.object({
35483
+ deviceId: z.number(),
35484
+ provider: z.string().min(1),
35485
+ fromMs: z.number(),
35486
+ toMs: z.number(),
35487
+ profile: CamProfileSchema.optional()
35488
+ }), RecordingPlaybackSchema, {
35489
+ kind: "query",
35490
+ auth: "protected"
35491
+ }),
35492
+ /**
35493
+ * What this (camera, source) can actually DO — asked before anything is
35494
+ * drawn. See {@link RecordingSourceOptionsSchema}; a constant the surface
35495
+ * keeps instead is the second authority D612 exists to end.
35496
+ */
35497
+ getPlaybackOptions: method(z.object({
35498
+ deviceId: z.number(),
35499
+ provider: z.string().min(1)
35500
+ }), RecordingSourceOptionsSchema, {
35501
+ kind: "query",
35502
+ auth: "protected"
35503
+ })
35504
+ }
35505
+ };
35506
+ /**
35507
+ * `recording-archive` — OUR archive, and the intent that fills it.
35508
+ *
35509
+ * The system-singleton half of the 2026-09-24 cut (D625). `recording` used to
35510
+ * be one 33-method system singleton holding two unrelated subjects: three
35511
+ * per-camera READS about coverage and playback, and everything else — storage
35512
+ * locations, retention, relocation, rebalance, the ops log, the placement
35513
+ * table and the byte-plane primitives our scrub and export are built on.
35514
+ *
35515
+ * The reads became a device-scoped COLLECTION, so a camera's own card can be a
35516
+ * source beside ours (`recording.cap.ts`). Everything that is about OUR store,
35517
+ * or unimplementable by a camera, stayed here.
35518
+ *
35519
+ * ## On the name
35520
+ *
35521
+ * `recording-storage` was the obvious choice and is wrong: this cap also holds
35522
+ * `getDeviceConfig`/`setDeviceConfig`, which are recording INTENT — bands,
35523
+ * retention, the D62 switch authority — and a name that says "storage" invites
35524
+ * the next reader to move them out again. An archive is a thing we keep, and
35525
+ * what we keep it under is a policy; the name covers both halves honestly and
35526
+ * sits in the existing family (`recording-onboard`, `recording-export`,
35527
+ * `recording-signal`).
35528
+ *
35529
+ * ## What must NOT happen to it
35530
+ *
35531
+ * It stays a SINGLETON. It is registered by `recorder`, which is
35532
+ * `placement: 'any-node'` and runs on every recording node; the hub dispatches
35533
+ * to one of them. Putting the ledger, the placement table or the relocation
35534
+ * jobs behind a fan-out is the one genuinely dangerous move in this cut.
35535
+ *
35536
+ * `getDeviceConfig` / `setDeviceConfig` in particular are the D62 recording
35537
+ * authority (`CameraSwitch.authority`). If a write reached a different provider
35538
+ * than the read — which a collection fan-out permits — two authorities would
35539
+ * decide when one camera records, and the symptom (recording silently off, or
35540
+ * a `bands` array clobbered by a partial write) is durable and silent. Keeping
35541
+ * them here means the worst case during a rollout is a 412: the switch refuses
35542
+ * to flip and SAYS so. **Do not move them into the collection, at any point,
35543
+ * for any reason.**
35544
+ *
35545
+ * ## The two batch reads
35546
+ *
35547
+ * `getAvailabilityBatch` / `getDaysWithRecordingsBatch` take `deviceIds:
35548
+ * number[]` with no single `deviceId`, and a device-scoped mount routes
35549
+ * through `getProviderForDevice(deviceId)` — there is nothing for it to route
35550
+ * on. They stay here, and on this cap the batch is explicitly OURS: a grid has
35551
+ * no per-camera picker, and a caller that wants another source's coverage asks
35552
+ * `recording.getAvailability` per device with that source's `provider`.
35553
+ */
35554
+ var RecordingStatusSchema = z.object({
35555
+ deviceId: z.number(),
35556
+ enabled: z.boolean(),
35557
+ /** THE derived storage mode, from the one definition
35558
+ * (`deriveRecordingMode`) — never a second enum. A duplicated list is how
35559
+ * `on-device-decision` could have reached the recorder and not the status. */
35560
+ activeMode: RecordingStorageModeSchema,
35561
+ nodeId: z.string(),
35562
+ storageBytes: z.number()
35563
+ });
35474
35564
  /**
35475
35565
  * One camera's row in a `getAvailabilityBatch` answer.
35476
35566
  *
35477
- * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
35478
- * batch collapses the transport, not the work — plus the one thing the singular
35479
- * method never had to say:
35567
+ * `ranges` is EXACTLY what `recording.getAvailability` returns for that camera
35568
+ * at OUR source — the batch collapses the transport, not the work — plus the
35569
+ * `read` mark the singular answer now carries too (D625):
35480
35570
  *
35481
35571
  * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
35482
35572
  * no footage in the window", which is a real claim.
@@ -35508,22 +35598,6 @@ var RecordingDaysForDeviceSchema = z.object({
35508
35598
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
35509
35599
  days: z.array(z.number()).readonly()
35510
35600
  });
35511
- var RecordingManifestSchema = z.object({
35512
- deviceId: z.number(),
35513
- /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
35514
- localMasterPath: z.string().nullable(),
35515
- /** HTTP(S) URL to the master playlist on the recording node's playback server
35516
- * (the PRIMARY candidate); null when no recording / server. Carries the
35517
- * scoped playback token in its path. */
35518
- playbackUrl: z.string().nullable(),
35519
- /**
35520
- * Candidate master-playlist URLs the client tries in order (LAN first, then
35521
- * remote — Tailscale/Cloudflare if the operator configured extra hosts), each
35522
- * carrying the same scoped token. `playbackUrl` is the first entry. Empty when
35523
- * there is no recording / server.
35524
- */
35525
- playbackEndpoints: z.array(z.string())
35526
- });
35527
35601
  /**
35528
35602
  * Recording storage usage for one camera — what the ARCHIVE holds for it,
35529
35603
  * across every profile and every resolvable location on this node.
@@ -35781,33 +35855,22 @@ var ReadWindowBytesResultSchema = z.discriminatedUnion("kind", [z.object({
35781
35855
  * not a retry hint (retrying the same window would refuse again). */
35782
35856
  segmentEndMs: z.number()
35783
35857
  })]);
35784
- var recordingCapability = {
35785
- name: "recording",
35858
+ var recordingArchiveCapability = {
35859
+ name: "recording-archive",
35786
35860
  scope: "system",
35787
35861
  mode: "singleton",
35862
+ /** Moves here with the config pair — the derived Recording settings
35863
+ * section is a view over `getDeviceConfig`/`setDeviceConfig` (D14), and
35864
+ * those did not move (D625). */
35788
35865
  exposesDeviceSettings: true,
35789
35866
  status: {
35790
35867
  schema: RecordingStatusSchema,
35791
35868
  kind: "command-driven"
35792
35869
  },
35793
35870
  methods: {
35794
- getAvailability: method(z.object({
35795
- deviceId: z.number(),
35796
- fromMs: z.number(),
35797
- toMs: z.number(),
35798
- /**
35799
- * Answer for THIS profile instead of the preferred one (D433). Absent
35800
- * keeps the timeline's behaviour — one bar, one profile, one set of
35801
- * reads. `profilesWithFootage` on the answer says what may be asked
35802
- * for.
35803
- */
35804
- profile: z.string().optional()
35805
- }), RecordingAvailabilitySchema, {
35806
- kind: "query",
35807
- auth: "protected"
35808
- }),
35809
35871
  /**
35810
- * `getAvailability` for a SET of cameras, in one round trip.
35872
+ * `recording.getAvailability` for a SET of cameras, in one round trip, at
35873
+ * OUR source.
35811
35874
  *
35812
35875
  * A multi-camera timeline re-asks availability for every camera in the grid
35813
35876
  * on every day change; fanned out that is one request per camera for N
@@ -35815,6 +35878,13 @@ var recordingCapability = {
35815
35878
  * `availabilityProfileFor` + `rangesIn`, run concurrently inside the
35816
35879
  * recorder) — only the transport collapses.
35817
35880
  *
35881
+ * It lives on the ARCHIVE, not on the `recording` collection, because it
35882
+ * takes `deviceIds` with no single `deviceId` and a device-scoped mount has
35883
+ * nothing to route on (D625 §10.2). The consequence is stated rather than
35884
+ * hidden: the batch answers for OURS. A caller that needs another source's
35885
+ * coverage asks `recording.getAvailability` per device, naming that
35886
+ * source's provider.
35887
+ *
35818
35888
  * `protected` for the same reason the singular method is: every id in
35819
35889
  * `deviceIds` is a device reference, so the F1 #3 gate refuses any camera
35820
35890
  * outside the caller's scope — one id out of scope refuses the CALL, it
@@ -35832,20 +35902,10 @@ var recordingCapability = {
35832
35902
  kind: "query",
35833
35903
  auth: "protected"
35834
35904
  }),
35835
- /** Which calendar days in [fromMs,toMs) have ≥1 recorded segment, bucketed by
35836
- * the client's local day (`tzOffsetMinutes` = minutes to add to UTC). Drives
35837
- * the theater date-picker's day dots. */
35838
- getDaysWithRecordings: method(z.object({
35839
- deviceId: z.number(),
35840
- fromMs: z.number(),
35841
- toMs: z.number(),
35842
- tzOffsetMinutes: z.number()
35843
- }), RecordingDaysSchema, {
35844
- kind: "query",
35845
- auth: "protected"
35846
- }),
35847
35905
  /**
35848
- * `getDaysWithRecordings` for a SET of cameras, in one round trip.
35906
+ * `recording.getDaysWithRecordings` for a SET of cameras, in one round
35907
+ * trip, at OUR source. Same placement argument as
35908
+ * {@link getAvailabilityBatch}.
35849
35909
  *
35850
35910
  * The cheapest question in the product, asked once per camera per month
35851
35911
  * change. One directory read per day per camera at the owner, unchanged;
@@ -35864,14 +35924,6 @@ var recordingCapability = {
35864
35924
  kind: "query",
35865
35925
  auth: "protected"
35866
35926
  }),
35867
- getPlaybackManifest: method(z.object({
35868
- deviceId: z.number(),
35869
- fromMs: z.number(),
35870
- toMs: z.number()
35871
- }), RecordingManifestSchema, {
35872
- kind: "query",
35873
- auth: "protected"
35874
- }),
35875
35927
  getStorageUsage: method(z.object({}), RecordingStorageUsageSchema, {
35876
35928
  kind: "query",
35877
35929
  auth: "admin"
@@ -35887,6 +35939,10 @@ var recordingCapability = {
35887
35939
  * value (D315, D393, D590). It carries schedules and retention, no secret,
35888
35940
  * and the per-device gate in `scope-access.ts` still applies; `setDeviceConfig`
35889
35941
  * stays `admin`.
35942
+ *
35943
+ * It is a SINGLETON method and stays one (D625 §16.2): it is the D62
35944
+ * recording authority, and an authority that several providers could answer
35945
+ * is the "two knobs over one decision" D62 forbids.
35890
35946
  */
35891
35947
  getDeviceConfig: method(z.object({ deviceId: z.number() }), RecordingConfigSchema, {
35892
35948
  kind: "query",
@@ -35894,7 +35950,13 @@ var recordingCapability = {
35894
35950
  }),
35895
35951
  /** Locate footage at a wall-clock instant: the covering segment's window,
35896
35952
  * or a gap with the forward nearest covered edge. Used by a feeder running
35897
- * in another addon process to seek recorded footage over tRPC. */
35953
+ * in another addon process to seek recorded footage over tRPC.
35954
+ *
35955
+ * BYTE PLANE: defined by `mfra` byte ranges over OUR own MP4 segment tree.
35956
+ * There is no vendor-neutral statement of it, its only callers are feeders
35957
+ * in other addon processes (the stream broker, the replay-clip source),
35958
+ * and an onboard source that ever needs bytes delegates to the
35959
+ * `videoclips` transport its vendor already has (D625 §9.3, §10.5). */
35898
35960
  locateSegment: method(z.object({
35899
35961
  deviceId: z.number(),
35900
35962
  profile: z.string(),
@@ -36543,6 +36605,390 @@ var recordingExportCapability = {
36543
36605
  }
36544
36606
  };
36545
36607
  /**
36608
+ * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
36609
+ * writes to the CAMERA's own card, on the camera's own schedule.
36610
+ *
36611
+ * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
36612
+ * footage ledger, our storage locations, our retention. This one has a
36613
+ * different authority — the camera's firmware — and per D62 it stores
36614
+ * nothing of its own. Every value here is read from the camera and every
36615
+ * write goes back to the camera; there is no CamStack-side mirror that
36616
+ * could disagree with the device.
36617
+ *
36618
+ * ## One shape, two firmwares
36619
+ *
36620
+ * Measured 2026-09-22 against the live fleet:
36621
+ *
36622
+ * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
36623
+ * | --- | --- | --- |
36624
+ * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
36625
+ * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
36626
+ * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
36627
+ * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
36628
+ * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
36629
+ * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
36630
+ * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
36631
+ * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
36632
+ *
36633
+ * The two schedule models look different and are the same thing in
36634
+ * different coordinates: both answer "for this trigger, during which
36635
+ * weekly windows does the camera record". {@link RecordWindow} is that
36636
+ * question in one shape — Hikvision's ranges map straight onto it,
36637
+ * Reolink's mask expands into hour-aligned windows.
36638
+ *
36639
+ * ## Union, not intersection
36640
+ *
36641
+ * **The same fields exist on every camera.** What differs per device is
36642
+ * which VALUES that device accepts, and that is what {@link
36643
+ * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
36644
+ * per field plus the schedule's own limits. A control a camera cannot
36645
+ * honour is rendered DISABLED WITH ITS REASON, never missing and never
36646
+ * dead: disabled must not look like broken.
36647
+ *
36648
+ * ## Refusal by name
36649
+ *
36650
+ * A write a camera cannot honour is refused with a sentence the operator
36651
+ * can read — never accepted and dropped. Both providers refuse through
36652
+ * {@link describeOnboardRefusal}, so the vocabulary is one function and
36653
+ * one test, not two hand-written vendor opinions.
36654
+ *
36655
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
36656
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
36657
+ * injected from `status`) reports the live values, and a single
36658
+ * `setSettings` mutation applies a partial change. No hand-written
36659
+ * settings-contribution methods.
36660
+ */
36661
+ /**
36662
+ * What makes the camera start recording during a window.
36663
+ *
36664
+ * The union of both vendors' vocabularies. `continuous` is Hikvision's
36665
+ * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
36666
+ * object-class triggers are Reolink-only today and the smart-event ones
36667
+ * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
36668
+ * firmwares measured — a camera that cannot record on a trigger simply
36669
+ * does not list it in `options.schedule.triggers`, and a window naming
36670
+ * it is REFUSED, not dropped.
36671
+ */
36672
+ var RecordTriggerSchema = z.enum([
36673
+ "continuous",
36674
+ "motion",
36675
+ "person",
36676
+ "vehicle",
36677
+ "animal",
36678
+ "lineCrossing",
36679
+ "intrusion",
36680
+ "loitering",
36681
+ "alarmInput"
36682
+ ]);
36683
+ /**
36684
+ * One weekly recording window: "on `day`, from `startMinute` to
36685
+ * `endMinute`, record on `trigger`".
36686
+ *
36687
+ * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
36688
+ * both firmwares enumerate). Minutes are local camera time since
36689
+ * midnight; `endMinute` may be 1440, meaning end of day — that is
36690
+ * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
36691
+ * collapsing it to 0 would turn a whole-day window into an empty one.
36692
+ */
36693
+ var RecordWindowSchema = z.object({
36694
+ trigger: RecordTriggerSchema,
36695
+ day: z.number().int().min(0).max(6),
36696
+ startMinute: z.number().int().min(0).max(1439),
36697
+ endMinute: z.number().int().min(1).max(1440)
36698
+ });
36699
+ /** Status of one physical volume, as the camera itself describes it. */
36700
+ var OnboardStorageVolumeSchema = z.object({
36701
+ /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
36702
+ id: z.string(),
36703
+ /** The camera's own name for it, when it gives one (`hddName`). */
36704
+ label: z.string().optional(),
36705
+ status: z.enum([
36706
+ "ok",
36707
+ "unformatted",
36708
+ "error",
36709
+ "offline",
36710
+ "unknown"
36711
+ ]),
36712
+ /**
36713
+ * Total size in MB, or **null when the camera did not say**.
36714
+ *
36715
+ * Never 0 for an unreadable value: a measurement that failed is not a
36716
+ * measurement (D393), and a card whose size is unknown must not be
36717
+ * rendered as a card of size zero.
36718
+ */
36719
+ capacityMb: z.number().nullable(),
36720
+ /**
36721
+ * Free space in MB, or null when unknown.
36722
+ *
36723
+ * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
36724
+ * 1439 both report exactly 11776 MB free — the fixed reserve a looping
36725
+ * card converges on once it has wrapped. At loop steady state the
36726
+ * number is identical whether the camera recorded yesterday or stopped
36727
+ * a month ago.
36728
+ */
36729
+ freeMb: z.number().nullable(),
36730
+ /** True when the camera reports the volume writable (`property` RW). */
36731
+ writable: z.boolean().optional()
36732
+ });
36733
+ /**
36734
+ * Whether the camera has usable onboard storage.
36735
+ *
36736
+ * Three states, never two: an unreadable answer is `unknown`, never
36737
+ * folded into `absent`. Folding a failed read into "no card" is how a
36738
+ * healthy camera gets reported as broken, and the inverse is how a
36739
+ * camera with no card reports `ok` and then an empty list (D393).
36740
+ */
36741
+ var OnboardStorageSchema = z.discriminatedUnion("kind", [
36742
+ z.object({
36743
+ kind: z.literal("present"),
36744
+ volumes: z.array(OnboardStorageVolumeSchema)
36745
+ }),
36746
+ z.object({
36747
+ kind: z.literal("absent"),
36748
+ reason: z.string()
36749
+ }),
36750
+ z.object({
36751
+ kind: z.literal("unknown"),
36752
+ reason: z.string()
36753
+ })
36754
+ ]);
36755
+ /**
36756
+ * One recording track the camera exposes. Read-only.
36757
+ *
36758
+ * Hikvision lists several — 1436 and 3833 each list 101 AND 103, with
36759
+ * DIFFERENT schedules (101 MOTION all day, 103 CMR 00:05–23:57) — so a
36760
+ * cap that reported one number for "the schedule" would be lying on
36761
+ * every Hikvision in the fleet. Reolink has exactly one per channel.
36762
+ *
36763
+ * There is deliberately **no `framerate`**. Hikvision declares it in the
36764
+ * track `Description` and it is WRONG: 1436 says `2.260000 fps` while
36765
+ * the media measures 12.55 fps. `codec` and `resolution` from the same
36766
+ * string ARE right — those are the only two worth surfacing.
36767
+ */
36768
+ var OnboardRecordTrackSchema = z.object({
36769
+ id: z.string(),
36770
+ enabled: z.boolean(),
36771
+ isVideo: z.boolean(),
36772
+ /** From the camera's own track description. Null when it does not say. */
36773
+ codec: z.string().nullable(),
36774
+ resolution: z.string().nullable(),
36775
+ /** Per-track overwrite flag, where the firmware keeps it per track. */
36776
+ overwriteWhenFull: z.boolean().nullable()
36777
+ });
36778
+ /**
36779
+ * What the camera is doing with its own storage, right now.
36780
+ *
36781
+ * Every scalar is nullable and **null means the camera did not answer**,
36782
+ * never a default. A form that seeds `0` from an unanswered read invites
36783
+ * the operator to save that 0 back onto the camera.
36784
+ */
36785
+ var RecordingOnboardStatusSchema = z.object({
36786
+ storage: OnboardStorageSchema,
36787
+ tracks: z.array(OnboardRecordTrackSchema),
36788
+ /**
36789
+ * The track the write path targets — the enabled VIDEO one. Null when
36790
+ * no track could be identified, which is itself a refusal reason.
36791
+ */
36792
+ primaryTrackId: z.string().nullable(),
36793
+ /** Master "record to the card at all" switch. */
36794
+ enabled: z.boolean().nullable(),
36795
+ overwriteWhenFull: z.boolean().nullable(),
36796
+ preRecordSec: z.number().nullable(),
36797
+ postRecordSec: z.number().nullable(),
36798
+ /** Length of one recorded file, in minutes. */
36799
+ segmentMinutes: z.number().nullable(),
36800
+ /** The primary track's weekly windows, flattened. */
36801
+ windows: z.array(RecordWindowSchema),
36802
+ /**
36803
+ * How many windows the camera described that CamStack could NOT read —
36804
+ * an unrecognised trigger, an unparseable clock, a weekday it does not
36805
+ * name.
36806
+ *
36807
+ * A dropped window is work the reader threw away, and a schedule that
36808
+ * silently shows fewer rows than the camera holds is how an operator
36809
+ * saves back a schedule shorter than the one they were looking at
36810
+ * (D391). Non-zero means the window list is INCOMPLETE and a write
36811
+ * that replaces it would delete what was not shown — which is why a
36812
+ * provider reporting a non-zero count also reports the schedule as not
36813
+ * writable.
36814
+ */
36815
+ unreadableWindows: z.number(),
36816
+ /**
36817
+ * The camera is scheduled to record and has NO usable storage.
36818
+ *
36819
+ * A first-class fact because it is the fleet's most common silent
36820
+ * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
36821
+ * to a card that is not there. Neither the schedule nor the storage
36822
+ * read says anything wrong on its own; only the pair does.
36823
+ */
36824
+ recordingToNowhere: z.boolean(),
36825
+ lastFetchedAt: z.number()
36826
+ });
36827
+ /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
36828
+ var RangeSchema = z.object({
36829
+ min: z.number(),
36830
+ max: z.number(),
36831
+ step: z.number()
36832
+ });
36833
+ /**
36834
+ * The values a camera actually takes for a numeric field, when they are a SET
36835
+ * rather than a range.
36836
+ *
36837
+ * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
36838
+ * (I91DN) on 2026-09-22 by writing each value and reading it back:
36839
+ *
36840
+ * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
36841
+ * camera's "no limit" — `-1` and `4294967295` both land on it);
36842
+ * - post-record: `5, 10, 30, 60, 120, 300, 600`.
36843
+ *
36844
+ * Neither is expressible as a step: the first has a sentinel two billion away
36845
+ * from its neighbours, the second doubles and then jumps. A range that tried
36846
+ * would forbid values the camera takes AND permit values it silently replaces
36847
+ * with 5 — wrong in both directions at once.
36848
+ *
36849
+ * `sentinel` names the member that is not a duration, so a surface can render
36850
+ * "no limit" instead of `2147483647` seconds.
36851
+ */
36852
+ var AllowedValuesSchema = z.object({
36853
+ values: z.array(z.number()).min(1),
36854
+ sentinel: z.object({
36855
+ value: z.number(),
36856
+ meaning: z.enum(["no-limit", "disabled"])
36857
+ }).optional()
36858
+ });
36859
+ /**
36860
+ * Per-field availability on ONE camera.
36861
+ *
36862
+ * The field exists on every camera — this says whether this one can be
36863
+ * read and whether it can be written, and `reason` says why not when
36864
+ * either is false. The UI renders the control DISABLED with the reason
36865
+ * rather than hiding it, so a limitation is legible instead of looking
36866
+ * like a missing feature.
36867
+ */
36868
+ var OnboardFieldSupportSchema = z.object({
36869
+ readable: z.boolean(),
36870
+ writable: z.boolean(),
36871
+ /** Required whenever `readable` or `writable` is false. */
36872
+ reason: z.string().optional()
36873
+ });
36874
+ /** What this camera's schedule model can express. */
36875
+ var OnboardScheduleSupportSchema = z.object({
36876
+ support: OnboardFieldSupportSchema,
36877
+ /**
36878
+ * The smallest time step the camera can express, in minutes.
36879
+ *
36880
+ * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
36881
+ * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
36882
+ * window whose edges are not a multiple of this is REFUSED rather than
36883
+ * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
36884
+ * and nothing says so.
36885
+ */
36886
+ granularityMinutes: z.number(),
36887
+ /** Triggers this camera can record on. A window naming another is refused. */
36888
+ triggers: z.array(RecordTriggerSchema),
36889
+ /**
36890
+ * False when the camera stores ONE trigger per time range, so two
36891
+ * windows overlapping on the same day cannot carry different triggers.
36892
+ * True on Reolink, whose mask is per-trigger and independent.
36893
+ */
36894
+ supportsOverlappingTriggers: z.boolean()
36895
+ });
36896
+ var RecordingOnboardOptionsSchema = z.object({
36897
+ enabled: OnboardFieldSupportSchema,
36898
+ overwriteWhenFull: OnboardFieldSupportSchema,
36899
+ preRecordSec: OnboardFieldSupportSchema,
36900
+ preRecordSecRange: RangeSchema.optional(),
36901
+ /** Preferred over the range when the camera takes a SET, not a span. */
36902
+ preRecordSecAllowed: AllowedValuesSchema.optional(),
36903
+ postRecordSec: OnboardFieldSupportSchema,
36904
+ postRecordSecRange: RangeSchema.optional(),
36905
+ /** Preferred over the range when the camera takes a SET, not a span. */
36906
+ postRecordSecAllowed: AllowedValuesSchema.optional(),
36907
+ segmentMinutes: OnboardFieldSupportSchema,
36908
+ segmentMinutesRange: RangeSchema.optional(),
36909
+ /** Preferred over the range when the camera takes a SET, not a span. */
36910
+ segmentMinutesAllowed: AllowedValuesSchema.optional(),
36911
+ schedule: OnboardScheduleSupportSchema
36912
+ });
36913
+ /**
36914
+ * A partial change. Every field optional.
36915
+ *
36916
+ * Unlike the other `deviceConfig` caps, a provider here does **NOT**
36917
+ * silently ignore a field it cannot support — it refuses, by name,
36918
+ * through {@link describeOnboardRefusal}. Silence on a recording setting
36919
+ * is the failure D62 exists to prevent: the operator believes the camera
36920
+ * is recording the way the form says, and it is not.
36921
+ */
36922
+ var RecordingOnboardPatchSchema = z.object({
36923
+ enabled: z.boolean().optional(),
36924
+ overwriteWhenFull: z.boolean().optional(),
36925
+ preRecordSec: z.number().optional(),
36926
+ postRecordSec: z.number().optional(),
36927
+ segmentMinutes: z.number().optional(),
36928
+ /** The complete new window set for the primary track — not a delta. */
36929
+ windows: z.array(RecordWindowSchema).optional()
36930
+ });
36931
+ var recordingOnboardCapability = {
36932
+ name: "recording-onboard",
36933
+ scope: "device",
36934
+ deviceNative: true,
36935
+ mode: "singleton",
36936
+ deviceTypes: [DeviceType.Camera],
36937
+ deviceConfig: { ui: {
36938
+ kind: "derived-form",
36939
+ builderId: "recording-onboard",
36940
+ tab: "recording"
36941
+ } },
36942
+ methods: {
36943
+ getOptions: method(z.object({ deviceId: z.number() }), RecordingOnboardOptionsSchema),
36944
+ setSettings: method(z.object({
36945
+ deviceId: z.number(),
36946
+ settings: RecordingOnboardPatchSchema
36947
+ }), z.void(), {
36948
+ kind: "mutation",
36949
+ auth: "admin"
36950
+ })
36951
+ },
36952
+ status: {
36953
+ schema: RecordingOnboardStatusSchema,
36954
+ kind: "poll"
36955
+ },
36956
+ runtimeState: RecordingOnboardStatusSchema,
36957
+ /**
36958
+ * Runtime-state durability: **restored** — operator-set camera-side
36959
+ * recording config; mutation-driven, and the storage half is the last
36960
+ * thing the camera said about its own card.
36961
+ *
36962
+ * See `RuntimeStateDurability`. Enforced by
36963
+ * `scripts/check-runtime-state-durability.ts`.
36964
+ */
36965
+ durability: "restored",
36966
+ /** Clock fields: written, but excluded from the compare that decides
36967
+ * whether persisting is worth a SQLite commit. */
36968
+ volatileStateFields: ["lastFetchedAt"]
36969
+ };
36970
+ /** Minutes in a day. `endMinute === 1440` means "to end of day". */
36971
+ var MINUTES_PER_DAY = 1440;
36972
+ /**
36973
+ * Day-of-week names in the cap's index order (0 = Monday), for messages
36974
+ * an operator reads and for Hikvision's `<DayOfWeek>` element.
36975
+ */
36976
+ var DAY_NAMES = [
36977
+ "Monday",
36978
+ "Tuesday",
36979
+ "Wednesday",
36980
+ "Thursday",
36981
+ "Friday",
36982
+ "Saturday",
36983
+ "Sunday"
36984
+ ];
36985
+ /** `510` → `08:30`. For messages, not for the wire. */
36986
+ function formatMinutes(minute) {
36987
+ const hour = Math.floor(minute / 60);
36988
+ const rest = minute % 60;
36989
+ return `${String(hour).padStart(2, "0")}:${String(rest).padStart(2, "0")}`;
36990
+ }
36991
+ /**
36546
36992
  * A camera's own "record me NOW" LEVEL — a signal the device raises while
36547
36993
  * something it knows about is happening (a robot vacuum cleaning, a machine
36548
36994
  * running, a gate open) and lowers when it stops.
@@ -42316,7 +42762,7 @@ var CAP_NAMES_WITH_STATUS = [
42316
42762
  "privacy-mask",
42317
42763
  "ptz",
42318
42764
  "ptz-autotrack",
42319
- "recording",
42765
+ "recording-archive",
42320
42766
  "recording-onboard",
42321
42767
  "recording-signal",
42322
42768
  "scene-monitor",
@@ -42401,7 +42847,6 @@ var ALL_CAPABILITY_DEFINITIONS = [
42401
42847
  embeddingEncoderCapability,
42402
42848
  enumSensorCapability,
42403
42849
  eventEmitterCapability,
42404
- eventsCapability,
42405
42850
  faceGalleryCapability,
42406
42851
  failureContributionCapability,
42407
42852
  fanControlCapability,
@@ -42462,6 +42907,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
42462
42907
  ptzAutotrackCapability,
42463
42908
  rebootCapability,
42464
42909
  recordingCapability,
42910
+ recordingArchiveCapability,
42465
42911
  recordingExportCapability,
42466
42912
  recordingOnboardCapability,
42467
42913
  recordingSignalCapability,
@@ -44696,24 +45142,6 @@ var METHOD_ACCESS_MAP = Object.freeze({
44696
45142
  addonId: null,
44697
45143
  access: "view"
44698
45144
  },
44699
- "events.getEventClipUrl": {
44700
- capName: "events",
44701
- capScope: "device",
44702
- addonId: null,
44703
- access: "view"
44704
- },
44705
- "events.getEvents": {
44706
- capName: "events",
44707
- capScope: "device",
44708
- addonId: null,
44709
- access: "view"
44710
- },
44711
- "events.getEventThumbnail": {
44712
- capName: "events",
44713
- capScope: "device",
44714
- addonId: null,
44715
- access: "view"
44716
- },
44717
45145
  "faceGallery.assignFace": {
44718
45146
  capName: "face-gallery",
44719
45147
  capScope: "system",
@@ -47588,224 +48016,236 @@ var METHOD_ACCESS_MAP = Object.freeze({
47588
48016
  addonId: null,
47589
48017
  access: "create"
47590
48018
  },
47591
- "recording.applyDeviceSettingsPatch": {
48019
+ "recording.getAvailability": {
47592
48020
  capName: "recording",
47593
- capScope: "system",
48021
+ capScope: "device",
47594
48022
  addonId: null,
47595
- access: "create"
48023
+ access: "view"
47596
48024
  },
47597
- "recording.cancelRelocateJob": {
48025
+ "recording.getDaysWithRecordings": {
47598
48026
  capName: "recording",
47599
- capScope: "system",
48027
+ capScope: "device",
47600
48028
  addonId: null,
47601
- access: "create"
48029
+ access: "view"
47602
48030
  },
47603
- "recording.cancelStorageMigrationMove": {
48031
+ "recording.getPlayback": {
47604
48032
  capName: "recording",
47605
- capScope: "system",
48033
+ capScope: "device",
47606
48034
  addonId: null,
47607
- access: "create"
48035
+ access: "view"
47608
48036
  },
47609
- "recording.deleteFootprint": {
48037
+ "recording.getPlaybackOptions": {
47610
48038
  capName: "recording",
47611
- capScope: "system",
48039
+ capScope: "device",
47612
48040
  addonId: null,
47613
- access: "delete"
48041
+ access: "view"
47614
48042
  },
47615
- "recording.getAvailability": {
48043
+ "recording.listSources": {
47616
48044
  capName: "recording",
47617
- capScope: "system",
48045
+ capScope: "device",
47618
48046
  addonId: null,
47619
48047
  access: "view"
47620
48048
  },
47621
- "recording.getAvailabilityBatch": {
47622
- capName: "recording",
48049
+ "recordingArchive.applyDeviceSettingsPatch": {
48050
+ capName: "recording-archive",
47623
48051
  capScope: "system",
47624
48052
  addonId: null,
47625
- access: "view"
48053
+ access: "create"
47626
48054
  },
47627
- "recording.getDaysWithRecordings": {
47628
- capName: "recording",
48055
+ "recordingArchive.cancelRelocateJob": {
48056
+ capName: "recording-archive",
47629
48057
  capScope: "system",
47630
48058
  addonId: null,
47631
- access: "view"
48059
+ access: "create"
47632
48060
  },
47633
- "recording.getDaysWithRecordingsBatch": {
47634
- capName: "recording",
48061
+ "recordingArchive.cancelStorageMigrationMove": {
48062
+ capName: "recording-archive",
48063
+ capScope: "system",
48064
+ addonId: null,
48065
+ access: "create"
48066
+ },
48067
+ "recordingArchive.deleteFootprint": {
48068
+ capName: "recording-archive",
48069
+ capScope: "system",
48070
+ addonId: null,
48071
+ access: "delete"
48072
+ },
48073
+ "recordingArchive.getAvailabilityBatch": {
48074
+ capName: "recording-archive",
47635
48075
  capScope: "system",
47636
48076
  addonId: null,
47637
48077
  access: "view"
47638
48078
  },
47639
- "recording.getDeviceConfig": {
47640
- capName: "recording",
48079
+ "recordingArchive.getDaysWithRecordingsBatch": {
48080
+ capName: "recording-archive",
47641
48081
  capScope: "system",
47642
48082
  addonId: null,
47643
48083
  access: "view"
47644
48084
  },
47645
- "recording.getDeviceLiveContribution": {
47646
- capName: "recording",
48085
+ "recordingArchive.getDeviceConfig": {
48086
+ capName: "recording-archive",
47647
48087
  capScope: "system",
47648
48088
  addonId: null,
47649
48089
  access: "view"
47650
48090
  },
47651
- "recording.getDeviceSettingsContribution": {
47652
- capName: "recording",
48091
+ "recordingArchive.getDeviceLiveContribution": {
48092
+ capName: "recording-archive",
47653
48093
  capScope: "system",
47654
48094
  addonId: null,
47655
48095
  access: "view"
47656
48096
  },
47657
- "recording.getPlacement": {
47658
- capName: "recording",
48097
+ "recordingArchive.getDeviceSettingsContribution": {
48098
+ capName: "recording-archive",
47659
48099
  capScope: "system",
47660
48100
  addonId: null,
47661
48101
  access: "view"
47662
48102
  },
47663
- "recording.getPlaybackManifest": {
47664
- capName: "recording",
48103
+ "recordingArchive.getPlacement": {
48104
+ capName: "recording-archive",
47665
48105
  capScope: "system",
47666
48106
  addonId: null,
47667
48107
  access: "view"
47668
48108
  },
47669
- "recording.getRelocateResidue": {
47670
- capName: "recording",
48109
+ "recordingArchive.getRelocateResidue": {
48110
+ capName: "recording-archive",
47671
48111
  capScope: "system",
47672
48112
  addonId: null,
47673
48113
  access: "view"
47674
48114
  },
47675
- "recording.getStatus": {
47676
- capName: "recording",
48115
+ "recordingArchive.getStatus": {
48116
+ capName: "recording-archive",
47677
48117
  capScope: "system",
47678
48118
  addonId: null,
47679
48119
  access: "view"
47680
48120
  },
47681
- "recording.getStorageMigrationMoveStatus": {
47682
- capName: "recording",
48121
+ "recordingArchive.getStorageMigrationMoveStatus": {
48122
+ capName: "recording-archive",
47683
48123
  capScope: "system",
47684
48124
  addonId: null,
47685
48125
  access: "view"
47686
48126
  },
47687
- "recording.getStorageUsage": {
47688
- capName: "recording",
48127
+ "recordingArchive.getStorageUsage": {
48128
+ capName: "recording-archive",
47689
48129
  capScope: "system",
47690
48130
  addonId: null,
47691
48131
  access: "view"
47692
48132
  },
47693
- "recording.listOpsLog": {
47694
- capName: "recording",
48133
+ "recordingArchive.listOpsLog": {
48134
+ capName: "recording-archive",
47695
48135
  capScope: "system",
47696
48136
  addonId: null,
47697
48137
  access: "view"
47698
48138
  },
47699
- "recording.listRelocateJobs": {
47700
- capName: "recording",
48139
+ "recordingArchive.listRelocateJobs": {
48140
+ capName: "recording-archive",
47701
48141
  capScope: "system",
47702
48142
  addonId: null,
47703
48143
  access: "view"
47704
48144
  },
47705
- "recording.locateSegment": {
47706
- capName: "recording",
48145
+ "recordingArchive.locateSegment": {
48146
+ capName: "recording-archive",
47707
48147
  capScope: "system",
47708
48148
  addonId: null,
47709
48149
  access: "view"
47710
48150
  },
47711
- "recording.pauseForStorageMigration": {
47712
- capName: "recording",
48151
+ "recordingArchive.pauseForStorageMigration": {
48152
+ capName: "recording-archive",
47713
48153
  capScope: "system",
47714
48154
  addonId: null,
47715
48155
  access: "create"
47716
48156
  },
47717
- "recording.planStorageRebalance": {
47718
- capName: "recording",
48157
+ "recordingArchive.planStorageRebalance": {
48158
+ capName: "recording-archive",
47719
48159
  capScope: "system",
47720
48160
  addonId: null,
47721
48161
  access: "view"
47722
48162
  },
47723
- "recording.pruneFootage": {
47724
- capName: "recording",
48163
+ "recordingArchive.pruneFootage": {
48164
+ capName: "recording-archive",
47725
48165
  capScope: "system",
47726
48166
  addonId: null,
47727
48167
  access: "create"
47728
48168
  },
47729
- "recording.readGopBytes": {
47730
- capName: "recording",
48169
+ "recordingArchive.readGopBytes": {
48170
+ capName: "recording-archive",
47731
48171
  capScope: "system",
47732
48172
  addonId: null,
47733
48173
  access: "view"
47734
48174
  },
47735
- "recording.readSegmentBytes": {
47736
- capName: "recording",
48175
+ "recordingArchive.readSegmentBytes": {
48176
+ capName: "recording-archive",
47737
48177
  capScope: "system",
47738
48178
  addonId: null,
47739
48179
  access: "view"
47740
48180
  },
47741
- "recording.readWindowBytes": {
47742
- capName: "recording",
48181
+ "recordingArchive.readWindowBytes": {
48182
+ capName: "recording-archive",
47743
48183
  capScope: "system",
47744
48184
  addonId: null,
47745
48185
  access: "view"
47746
48186
  },
47747
- "recording.reconcileLedgerAgainstDisk": {
47748
- capName: "recording",
48187
+ "recordingArchive.reconcileLedgerAgainstDisk": {
48188
+ capName: "recording-archive",
47749
48189
  capScope: "system",
47750
48190
  addonId: null,
47751
48191
  access: "create"
47752
48192
  },
47753
- "recording.refreshStorageLocationsForMigration": {
47754
- capName: "recording",
48193
+ "recordingArchive.refreshStorageLocationsForMigration": {
48194
+ capName: "recording-archive",
47755
48195
  capScope: "system",
47756
48196
  addonId: null,
47757
48197
  access: "create"
47758
48198
  },
47759
- "recording.relocateFootage": {
47760
- capName: "recording",
48199
+ "recordingArchive.relocateFootage": {
48200
+ capName: "recording-archive",
47761
48201
  capScope: "system",
47762
48202
  addonId: null,
47763
48203
  access: "create"
47764
48204
  },
47765
- "recording.renderClip": {
47766
- capName: "recording",
48205
+ "recordingArchive.renderClip": {
48206
+ capName: "recording-archive",
47767
48207
  capScope: "system",
47768
48208
  addonId: null,
47769
48209
  access: "create"
47770
48210
  },
47771
- "recording.renderGif": {
47772
- capName: "recording",
48211
+ "recordingArchive.renderGif": {
48212
+ capName: "recording-archive",
47773
48213
  capScope: "system",
47774
48214
  addonId: null,
47775
48215
  access: "create"
47776
48216
  },
47777
- "recording.rescanStorage": {
47778
- capName: "recording",
48217
+ "recordingArchive.rescanStorage": {
48218
+ capName: "recording-archive",
47779
48219
  capScope: "system",
47780
48220
  addonId: null,
47781
48221
  access: "create"
47782
48222
  },
47783
- "recording.resumeForStorageMigration": {
47784
- capName: "recording",
48223
+ "recordingArchive.resumeForStorageMigration": {
48224
+ capName: "recording-archive",
47785
48225
  capScope: "system",
47786
48226
  addonId: null,
47787
48227
  access: "create"
47788
48228
  },
47789
- "recording.setDeviceConfig": {
47790
- capName: "recording",
48229
+ "recordingArchive.setDeviceConfig": {
48230
+ capName: "recording-archive",
47791
48231
  capScope: "system",
47792
48232
  addonId: null,
47793
48233
  access: "create"
47794
48234
  },
47795
- "recording.setDevicePlacement": {
47796
- capName: "recording",
48235
+ "recordingArchive.setDevicePlacement": {
48236
+ capName: "recording-archive",
47797
48237
  capScope: "system",
47798
48238
  addonId: null,
47799
48239
  access: "create"
47800
48240
  },
47801
- "recording.startStorageMigrationMove": {
47802
- capName: "recording",
48241
+ "recordingArchive.startStorageMigrationMove": {
48242
+ capName: "recording-archive",
47803
48243
  capScope: "system",
47804
48244
  addonId: null,
47805
48245
  access: "create"
47806
48246
  },
47807
- "recording.startStorageRebalance": {
47808
- capName: "recording",
48247
+ "recordingArchive.startStorageRebalance": {
48248
+ capName: "recording-archive",
47809
48249
  capScope: "system",
47810
48250
  addonId: null,
47811
48251
  access: "create"
@@ -49334,6 +49774,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
49334
49774
  addonId: null,
49335
49775
  access: "view"
49336
49776
  },
49777
+ "videoclips.getPlaybackOptions": {
49778
+ capName: "videoclips",
49779
+ capScope: "device",
49780
+ addonId: null,
49781
+ access: "view"
49782
+ },
49337
49783
  "videoclips.listClips": {
49338
49784
  capName: "videoclips",
49339
49785
  capScope: "device",
@@ -49346,6 +49792,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
49346
49792
  addonId: null,
49347
49793
  access: "view"
49348
49794
  },
49795
+ "videoclips.offerClipBytes": {
49796
+ capName: "videoclips",
49797
+ capScope: "device",
49798
+ addonId: null,
49799
+ access: "view"
49800
+ },
49349
49801
  "videoclips.readClipBytes": {
49350
49802
  capName: "videoclips",
49351
49803
  capScope: "device",
@@ -50098,21 +50550,6 @@ Object.freeze({
50098
50550
  form: "single",
50099
50551
  optional: false
50100
50552
  }],
50101
- "events.getEventClipUrl": [{
50102
- name: "deviceId",
50103
- form: "single",
50104
- optional: false
50105
- }],
50106
- "events.getEvents": [{
50107
- name: "deviceId",
50108
- form: "single",
50109
- optional: false
50110
- }],
50111
- "events.getEventThumbnail": [{
50112
- name: "deviceId",
50113
- form: "single",
50114
- optional: false
50115
- }],
50116
50553
  "faceGallery.getFaceByTrack": [{
50117
50554
  name: "deviceId",
50118
50555
  form: "single",
@@ -51031,107 +51468,117 @@ Object.freeze({
51031
51468
  form: "single",
51032
51469
  optional: false
51033
51470
  }],
51034
- "recording.deleteFootprint": [{
51471
+ "recording.getAvailability": [{
51035
51472
  name: "deviceId",
51036
51473
  form: "single",
51037
51474
  optional: false
51038
51475
  }],
51039
- "recording.getAvailability": [{
51476
+ "recording.getDaysWithRecordings": [{
51040
51477
  name: "deviceId",
51041
51478
  form: "single",
51042
51479
  optional: false
51043
51480
  }],
51044
- "recording.getAvailabilityBatch": [{
51045
- name: "deviceIds",
51046
- form: "array",
51481
+ "recording.getPlayback": [{
51482
+ name: "deviceId",
51483
+ form: "single",
51047
51484
  optional: false
51048
51485
  }],
51049
- "recording.getDaysWithRecordings": [{
51486
+ "recording.getPlaybackOptions": [{
51050
51487
  name: "deviceId",
51051
51488
  form: "single",
51052
51489
  optional: false
51053
51490
  }],
51054
- "recording.getDaysWithRecordingsBatch": [{
51055
- name: "deviceIds",
51056
- form: "array",
51491
+ "recording.listSources": [{
51492
+ name: "deviceId",
51493
+ form: "single",
51057
51494
  optional: false
51058
51495
  }],
51059
- "recording.getDeviceConfig": [{
51496
+ "recordingArchive.deleteFootprint": [{
51060
51497
  name: "deviceId",
51061
51498
  form: "single",
51062
51499
  optional: false
51063
51500
  }],
51064
- "recording.getPlaybackManifest": [{
51501
+ "recordingArchive.getAvailabilityBatch": [{
51502
+ name: "deviceIds",
51503
+ form: "array",
51504
+ optional: false
51505
+ }],
51506
+ "recordingArchive.getDaysWithRecordingsBatch": [{
51507
+ name: "deviceIds",
51508
+ form: "array",
51509
+ optional: false
51510
+ }],
51511
+ "recordingArchive.getDeviceConfig": [{
51065
51512
  name: "deviceId",
51066
51513
  form: "single",
51067
51514
  optional: false
51068
51515
  }],
51069
- "recording.listOpsLog": [{
51516
+ "recordingArchive.listOpsLog": [{
51070
51517
  name: "deviceId",
51071
51518
  form: "single",
51072
51519
  optional: true
51073
51520
  }],
51074
- "recording.locateSegment": [{
51521
+ "recordingArchive.locateSegment": [{
51075
51522
  name: "deviceId",
51076
51523
  form: "single",
51077
51524
  optional: false
51078
51525
  }],
51079
- "recording.pruneFootage": [{
51526
+ "recordingArchive.pruneFootage": [{
51080
51527
  name: "deviceId",
51081
51528
  form: "single",
51082
51529
  optional: false
51083
51530
  }],
51084
- "recording.readGopBytes": [{
51531
+ "recordingArchive.readGopBytes": [{
51085
51532
  name: "deviceId",
51086
51533
  form: "single",
51087
51534
  optional: false
51088
51535
  }],
51089
- "recording.readSegmentBytes": [{
51536
+ "recordingArchive.readSegmentBytes": [{
51090
51537
  name: "deviceId",
51091
51538
  form: "single",
51092
51539
  optional: false
51093
51540
  }],
51094
- "recording.readWindowBytes": [{
51541
+ "recordingArchive.readWindowBytes": [{
51095
51542
  name: "deviceId",
51096
51543
  form: "single",
51097
51544
  optional: false
51098
51545
  }],
51099
- "recording.reconcileLedgerAgainstDisk": [{
51546
+ "recordingArchive.reconcileLedgerAgainstDisk": [{
51100
51547
  name: "deviceId",
51101
51548
  form: "single",
51102
51549
  optional: true
51103
51550
  }],
51104
- "recording.relocateFootage": [{
51551
+ "recordingArchive.relocateFootage": [{
51105
51552
  name: "deviceId",
51106
51553
  form: "single",
51107
51554
  optional: true
51108
51555
  }],
51109
- "recording.renderClip": [{
51556
+ "recordingArchive.renderClip": [{
51110
51557
  name: "deviceId",
51111
51558
  form: "single",
51112
51559
  optional: false
51113
51560
  }],
51114
- "recording.renderGif": [{
51561
+ "recordingArchive.renderGif": [{
51115
51562
  name: "deviceId",
51116
51563
  form: "single",
51117
51564
  optional: false
51118
51565
  }],
51119
- "recording.rescanStorage": [{
51566
+ "recordingArchive.rescanStorage": [{
51120
51567
  name: "deviceId",
51121
51568
  form: "single",
51122
51569
  optional: false
51123
51570
  }],
51124
- "recording.setDeviceConfig": [{
51571
+ "recordingArchive.setDeviceConfig": [{
51125
51572
  name: "deviceId",
51126
51573
  form: "single",
51127
51574
  optional: false
51128
51575
  }],
51129
- "recording.setDevicePlacement": [{
51576
+ "recordingArchive.setDevicePlacement": [{
51130
51577
  name: "deviceId",
51131
51578
  form: "single",
51132
51579
  optional: false
51133
51580
  }],
51134
- "recording.startStorageMigrationMove": [{
51581
+ "recordingArchive.startStorageMigrationMove": [{
51135
51582
  name: "deviceId",
51136
51583
  form: "single",
51137
51584
  optional: true
@@ -51392,6 +51839,11 @@ Object.freeze({
51392
51839
  form: "single",
51393
51840
  optional: false
51394
51841
  }],
51842
+ "videoclips.getPlaybackOptions": [{
51843
+ name: "deviceId",
51844
+ form: "single",
51845
+ optional: false
51846
+ }],
51395
51847
  "videoclips.listClips": [{
51396
51848
  name: "deviceId",
51397
51849
  form: "single",
@@ -51402,6 +51854,11 @@ Object.freeze({
51402
51854
  form: "single",
51403
51855
  optional: false
51404
51856
  }],
51857
+ "videoclips.offerClipBytes": [{
51858
+ name: "deviceId",
51859
+ form: "single",
51860
+ optional: false
51861
+ }],
51405
51862
  "videoclips.readClipBytes": [{
51406
51863
  name: "deviceId",
51407
51864
  form: "single",
@@ -52533,4 +52990,4 @@ function enumerateInferenceDevices(hw) {
52533
52990
  return out;
52534
52991
  }
52535
52992
  //#endregion
52536
- export { deriveBatteryPresence as $, storageCapability as $t, ScopedTokenSchema as A, isEvent as An, mayWriteToLocation as At, authProviderCapability as B, ptzAutotrackCapability as Bt, REDACTED_SECRET as C, asString as Cn, lifecycleJobSchema as Ct, SOURCE_DEVICE_TYPES as D, expandCapMethods as Dn, logLevelAtMost as Dt, SOURCE_CAPS as E, emitReadiness as En, logDestinationCapability as Et, UserRecordSchema as F, resolveCapMount as Fn, objectInputDeclaresAddonId as Ft, cameraGridLayoutCapability as G, resolveMethodAuth as Gt, bareAddonId as H, resolveBucketMs as Ht, addonPagesCapability as I, scopeKey as In, objectInputDeclaresRequiredDeviceId as It, coreBlockIdFromAddonId as J, scopesAllowDeviceCap as Jt, collectSecretConfigKeys as K, runtimeStatePolicyFor as Kt, addonSettingsCapability as L, sleep as Ln, parseStreamParamsFormPatch as Lt, StorageLocationModeSchema as M, parseJsonObject as Mn, modeMayWrite as Mt, StorageLocationTypeSchema as N, parseJsonUnknown as Nn, motionCapability as Nt, STORAGE_ACCESS_FALLBACK as O, hydrateSchema as On, looseSchema as Ot, StorageMigrationJobSchema as P, readinessKey as Pn, normalizeUnit as Pt, decodeVectorBase64 as Q, snapshotCapability as Qt, addonWidgetsCapability as R, EventCategory as Rn, platformProbeCapability as Rt, MOTION_CLOSE_AFTER_MS as S, asNumber as Sn, legacyModeOf as St, RecordingStatusSchema as T, emitDownForOwnedCaps as Tn, locationIconKey as Tt, batteryCapability as U, resolveContainerPrimaryChild as Ut, backupCapability as V, reducePoints as Vt, buildStreamParamsConfigSchema as W, resolveLocationMode as Wt, dataStoreProviderCapability as X, settingsStoreCapability as Xt, coreBlocksCapability as Y, scoreRuntimes as Yt, declarationOwnerNodeId as Z, siteZoneKey as Zt, LOG_LEVEL_RANK as _, DeviceType as _n, isObjectInput as _t, BatteryStatusSchema as a, userManagementCapability as an, enumerateItemArrayFields as at, METHOD_ACCESS_MAP as b, WELL_KNOWN_TAB_MAP as bn, isVoidInput as bt, CORE_BLOCKS_ADDON_ID as c, withLocationMode as cn, evictionPolicyOfLocation as ct, DAY_NAMES as d, BaseAddon as dn, foldSnapshotByFunction as dt, storageMigrationCapability as en, deviceManagerCapability as et, DEFAULT_FIRST_SIGHTING_FRESHNESS_MS as f, DATAPLANE_SECRET_HEADER as fn, formatMinutes as ft, FIRST_LEVEL_MACRO_CLASSES as g, DeviceRole as gn, isLocationEnabled as gt, DeviceStatusSchema as h, DeviceFeature as hn, isCollectionArrayMethod as ht, BaseDevice as i, streamQualityLabel as in, enumerateInferenceDevices as it, StorageCleanupJobSchema as j, nodePin as jn, metricsProviderCapability as jt, STREAM_PROFILE_META as k, isDeviceConfigCap as kn, mayReadLocation as kt, CORE_BLOCK_ADDON_PREFIX as l, errMsg as ln, extractNestedAddonId as lt, DeclaredDevices as m, DEVICE_STATUS_METHOD as mn, isArrayOutputSchema as mt, AlertSchema as n, storageProviderCapability as nn, deviceStatusCapability as nt, CAMERA_SWITCH_ORDER as o, vectorDimFromBase64 as on, enumerateSchemaFields as ot, DERIVED_CAMERA_SILENCED_CAP_NAMES as p, DEVICE_SETTINGS_CONTRIBUTION_METHODS as pn, gbToBytes as pt, coreBlockAddonId as q, scopesAllowAddon as qt, ApiKeyRecordSchema as r, streamCatalogCapability as rn, doorbellCapability as rt, CAP_NAMES_WITH_STATUS as s, vectorStoreCapability as sn, evaluateSensorEdge as st, ALL_CAPABILITY_DEFINITIONS as t, storageOccupancyCapability as tn, deviceStateCapability as tt, CoreBlockSchema as u, buildFfmpegArgs as un, filesystemBrowseCapability as ut, LegacyStorageLocationDefaultSchema as v, ReadinessRegistry as vn, isSameAddonId as vt, RUNTIME_DEFAULTS as w, createEvent as wn, localNetworkCapability as wt, MINUTES_PER_DAY as x, asJsonObject as xn, kebabToCamel as xt, LocationIconIdSchema as y, ReadinessTimeoutError as yn, isSourceCap as yt, alertsCapability as z, procedureAuthKey as zt };
52993
+ export { decodeVectorBase64 as $, snapshotCapability as $t, STREAM_PROFILE_META as A, isDeviceConfigCap as An, mayReadLocation as At, alertsCapability as B, procedureAuthKey as Bt, RECORDING_SOURCE_CAMSTACK_ADDON as C, asNumber as Cn, legacyModeOf as Ct, SOURCE_CAPS as D, emitReadiness as Dn, logDestinationCapability as Dt, RecordingStatusSchema as E, emitDownForOwnedCaps as En, locationIconKey as Et, StorageMigrationJobSchema as F, readinessKey as Fn, normalizeUnit as Ft, buildStreamParamsConfigSchema as G, resolveLocationMode as Gt, backupCapability as H, reducePoints as Ht, UserRecordSchema as I, resolveCapMount as In, objectInputDeclaresAddonId as It, coreBlockAddonId as J, scopesAllowAddon as Jt, cameraGridLayoutCapability as K, resolveMethodAuth as Kt, addonPagesCapability as L, scopeKey as Ln, objectInputDeclaresRequiredDeviceId as Lt, StorageCleanupJobSchema as M, nodePin as Mn, metricsProviderCapability as Mt, StorageLocationModeSchema as N, parseJsonObject as Nn, modeMayWrite as Nt, SOURCE_DEVICE_TYPES as O, expandCapMethods as On, logLevelAtMost as Ot, StorageLocationTypeSchema as P, parseJsonUnknown as Pn, motionCapability as Pt, declarationOwnerNodeId as Q, siteZoneKey as Qt, addonSettingsCapability as R, sleep as Rn, parseStreamParamsFormPatch as Rt, MOTION_CLOSE_AFTER_MS as S, asJsonObject as Sn, kebabToCamel as St, RUNTIME_DEFAULTS as T, createEvent as Tn, localNetworkCapability as Tt, bareAddonId as U, resolveBucketMs as Ut, authProviderCapability as V, ptzAutotrackCapability as Vt, batteryCapability as W, resolveContainerPrimaryChild as Wt, coreBlocksCapability as X, scoreRuntimes as Xt, coreBlockIdFromAddonId as Y, scopesAllowDeviceCap as Yt, dataStoreProviderCapability as Z, settingsStoreCapability as Zt, LOG_LEVEL_RANK as _, DeviceRole as _n, isLocationEnabled as _t, BatteryStatusSchema as a, streamQualityLabel as an, enumerateInferenceDevices as at, METHOD_ACCESS_MAP as b, ReadinessTimeoutError as bn, isSourceCap as bt, CORE_BLOCKS_ADDON_ID as c, vectorStoreCapability as cn, evaluateSensorEdge as ct, DAY_NAMES as d, buildFfmpegArgs as dn, filesystemBrowseCapability as dt, storageCapability as en, deriveBatteryPresence as et, DEFAULT_FIRST_SIGHTING_FRESHNESS_MS as f, BaseAddon as fn, foldSnapshotByFunction as ft, FIRST_LEVEL_MACRO_CLASSES as g, DeviceFeature as gn, isCollectionArrayMethod as gt, DeviceStatusSchema as h, DEVICE_STATUS_METHOD as hn, isArrayOutputSchema as ht, BaseDevice as i, streamCatalogCapability as in, doorbellCapability as it, ScopedTokenSchema as j, isEvent as jn, mayWriteToLocation as jt, STORAGE_ACCESS_FALLBACK as k, hydrateSchema as kn, looseSchema as kt, CORE_BLOCK_ADDON_PREFIX as l, withLocationMode as ln, evictionPolicyOfLocation as lt, DeclaredDevices as m, DEVICE_SETTINGS_CONTRIBUTION_METHODS as mn, gbToBytes as mt, AlertSchema as n, storageOccupancyCapability as nn, deviceStateCapability as nt, CAMERA_SWITCH_ORDER as o, userManagementCapability as on, enumerateItemArrayFields as ot, DERIVED_CAMERA_SILENCED_CAP_NAMES as p, DATAPLANE_SECRET_HEADER as pn, formatMinutes as pt, collectSecretConfigKeys as q, runtimeStatePolicyFor as qt, ApiKeyRecordSchema as r, storageProviderCapability as rn, deviceStatusCapability as rt, CAP_NAMES_WITH_STATUS as s, vectorDimFromBase64 as sn, enumerateSchemaFields as st, ALL_CAPABILITY_DEFINITIONS as t, storageMigrationCapability as tn, deviceManagerCapability as tt, CoreBlockSchema as u, errMsg as un, extractNestedAddonId as ut, LegacyStorageLocationDefaultSchema as v, DeviceType as vn, isObjectInput as vt, REDACTED_SECRET as w, asString as wn, lifecycleJobSchema as wt, MINUTES_PER_DAY as x, WELL_KNOWN_TAB_MAP as xn, isVoidInput as xt, LocationIconIdSchema as y, ReadinessRegistry as yn, isSameAddonId as yt, addonWidgetsCapability as z, EventCategory as zn, platformProbeCapability as zt };