@camstack/system 1.2.290 → 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 (61) 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-BkjMEa2F.mjs → dist-DiMNxNDh.mjs} +1286 -1049
  53. package/dist/{dist-KRK9zLWt.js → dist-XnunBp0n.js} +1287 -1062
  54. package/dist/index.js +2 -2
  55. package/dist/index.mjs +2 -2
  56. package/dist/kernel/moleculer/addon-peer-bytes-facility.d.ts +2 -1
  57. package/dist/{manifest-system-deps-Dm7QhyvL.js → manifest-system-deps-DTgkfBJy.js} +4 -5
  58. package/dist/{manifest-system-deps-D_5UXqDb.mjs → manifest-system-deps-ghRqcy_-.mjs} +4 -5
  59. package/dist/{retired-settings-keys-BSLMlTcz.js → retired-settings-keys-DQn9s_AI.js} +1 -1
  60. package/dist/{retired-settings-keys-DNK-p6aa.mjs → retired-settings-keys-DnCcgrCG.mjs} +1 -1
  61. 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-COWaSCAi.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 = zod.z.object({
1856
+ /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
1857
+ url: zod.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: zod.z.string().min(1),
1864
+ expiresAtMs: zod.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: zod.z.number().int().nonnegative().nullable()
1871
+ });
1854
1872
  /** Shared Zod schemas used across streaming capabilities. */
1855
1873
  var CamProfileSchema = zod.z.enum([
1856
1874
  "high",
@@ -4050,46 +4068,6 @@ zod.z.object({
4050
4068
  })
4051
4069
  });
4052
4070
  /**
4053
- * How long a minted ticket stays takeable.
4054
- *
4055
- * The consumer's dial is the next thing that happens after the cap call that
4056
- * minted it returns — measured well under 100 ms on the same host for the clip
4057
- * dial this is generalised from. Thirty seconds covers a busy hub and is short
4058
- * enough that a ticket which leaked into a log is worth nothing by the time
4059
- * anybody reads it.
4060
- */
4061
- var PEER_BYTES_TICKET_TTL_MS = 3e4;
4062
- /**
4063
- * How long the producer waits on a consumer that has stopped reading before it
4064
- * cuts the body.
4065
- *
4066
- * D447: a raw HTTP stream reads no chunk the socket has not taken. The producer
4067
- * honours `write()`'s return and awaits `drain`; a peer that never drains is
4068
- * a peer that is gone, and holding the body for it is the unbounded buffer this
4069
- * whole facility exists to avoid.
4070
- */
4071
- var PEER_BYTES_STALL_MS = 15e3;
4072
- /** Bytes handed to the socket per `write()` when the producer holds a buffer. */
4073
- var PEER_BYTES_CHUNK_BYTES = 256 * 1024;
4074
- /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
4075
- var PeerBytesTicketSchema = zod.z.object({
4076
- /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
4077
- url: zod.z.string().min(1),
4078
- /**
4079
- * The HOST node this URL means something on — the hub or a named agent,
4080
- * never a runner. {@link AddonPeerBytes.open} compares it to its own and
4081
- * refuses `cross-node` by name when they differ, without dialling.
4082
- */
4083
- hostNodeId: zod.z.string().min(1),
4084
- expiresAtMs: zod.z.number().int().nonnegative(),
4085
- /**
4086
- * What the producer DECLARED the body to be, when it knows — `null` when it
4087
- * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
4088
- * must be able to tell "the producer did not say" from "the body is empty".
4089
- */
4090
- declaredBytes: zod.z.number().int().nonnegative().nullable()
4091
- });
4092
- /**
4093
4071
  * Adoption job — the background form of `device-adoption.adopt`.
4094
4072
  *
4095
4073
  * ## Why this exists
@@ -4200,7 +4178,7 @@ var AdoptionJobSchema = zod.z.object({
4200
4178
  * component's original options — detection to the detection-pipeline wrapper
4201
4179
  * binding, audio analysis to its own, recording to `RecordingConfig.enabled`
4202
4180
  * (which was always first-class; the switch was a veneer over
4203
- * `recording.setDeviceConfig`), notifications to a notification-center
4181
+ * `recordingArchive.setDeviceConfig`), notifications to a notification-center
4204
4182
  * per-device setting, the two camera planes to their own components.
4205
4183
  *
4206
4184
  * What survives is {@link composeSwitchedOff}: `CameraStatus.switchedOff`, the
@@ -4226,7 +4204,7 @@ var AdoptionJobSchema = zod.z.object({
4226
4204
  * | `stream-broker` | `deviceManager.setDisabled` | `StreamBrokerManager.reconcileAllCatalogs` releases the brokers; `ensureBroker` refuses re-creation |
4227
4205
  * | `object-detection` | `deviceManager.setWrapperActive('detection-pipeline')` | `PipelineSettingsStore.resolvePipelineForDevice` returns `{ steps: [], audio: null }` |
4228
4206
  * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
4229
- * | `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 |
4230
4208
  * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
4231
4209
  * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
4232
4210
  * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
@@ -9058,87 +9036,6 @@ var cameraPipelineConfigCapability = {
9058
9036
  exposesDeviceSettings: true,
9059
9037
  methods: {}
9060
9038
  };
9061
- /**
9062
- * The signals a device can emit to WAKE its own stream.
9063
- *
9064
- * A camera whose stream is built on demand sleeps until something asks for it,
9065
- * and "something" cannot be a consumer that is merely attached — a Frigate-style
9066
- * puller holds a session open for ever, and treating that as demand would keep
9067
- * a battery camera awake for ever, which is the whole thing the battery is for
9068
- * (D173). So the wake has to come from the CAMERA: an event it noticed by
9069
- * itself, with no stream running.
9070
- *
9071
- * ## The vocabulary is the PROVIDER'S, not ours
9072
- *
9073
- * Like `consumables`, this cap declares no vocabulary of its own. A provider
9074
- * names each signal with a `code` it chooses and a `label` an operator reads.
9075
- * Reolink offers motion and camera-native detection; another provider may offer
9076
- * a tamper, a doorbell press, a PIR, or something no camera in this fleet has
9077
- * yet. A fixed enum here would mean every new signal is a framework release.
9078
- *
9079
- * It is deliberately NOT derived from the caps a device already binds. Whether
9080
- * a camera CAN push firmware motion is expressed by `motionSources` containing
9081
- * `'onboard'`, and whether it does AI on-camera by the `native-object-detection`
9082
- * binding — but both answer "what drives the detection pipeline", which is a
9083
- * different question from "what may wake a sleeping stream". A camera can do
9084
- * the first and not be trusted with the second, and the operator picks per
9085
- * camera. Two questions, two authorities.
9086
- *
9087
- * ## Availability is not permission
9088
- *
9089
- * `listSignals` says what the device CAN emit. Whether a given signal actually
9090
- * wakes the stream is the operator's per-camera choice, held by the broker
9091
- * alongside the cooldown — see the stream-broker cap's wake settings. A
9092
- * provider declaring a signal is not a provider enabling it.
9093
- */
9094
- /** One signal a device can emit. */
9095
- var StreamSignalSchema = zod.z.object({
9096
- /** Stable id chosen by the provider, e.g. `'motion'`, `'person'`, `'tamper'`. */
9097
- code: zod.z.string().min(1),
9098
- /** What an operator reads in the picker. The provider's own wording. */
9099
- label: zod.z.string().min(1),
9100
- /**
9101
- * Whether the provider recommends this signal ON when a camera is first set
9102
- * up. A provider knows which of its signals are cheap and reliable; an
9103
- * operator should not have to discover that by trial. Reolink recommends
9104
- * both of its own.
9105
- */
9106
- recommended: zod.z.boolean()
9107
- });
9108
- var StreamSignalsStatusSchema = zod.z.object({
9109
- signals: zod.z.array(StreamSignalSchema),
9110
- lastFetchedAt: zod.z.number()
9111
- });
9112
- var streamSignalsCapability = {
9113
- name: "stream-signals",
9114
- scope: "device",
9115
- deviceNative: true,
9116
- mode: "singleton",
9117
- deviceTypes: Object.values(DeviceType),
9118
- runtimeState: StreamSignalsStatusSchema,
9119
- /**
9120
- * Runtime-state durability: **session** — mirrored in RAM, never written.
9121
- *
9122
- * The slice holds what the DEVICE says it can emit. That is a probed fact,
9123
- * not an operator choice: the provider re-declares it on every registration,
9124
- * so losing it loses nothing and persisting it would freeze an answer the
9125
- * camera is entitled to change. Measured the same day on the sibling case —
9126
- * `native-object-detection.supportedClasses` was persisted, and a firmware
9127
- * class the camera really detected stayed missing for the life of the row
9128
- * because the fix could not reach it.
9129
- *
9130
- * See `RuntimeStateDurability`. Enforced by
9131
- * `scripts/check-runtime-state-durability.ts`.
9132
- */
9133
- durability: "session",
9134
- methods: {
9135
- /**
9136
- * What this device can emit. Empty is a valid and common answer — most
9137
- * cameras have nothing to offer here, and an empty list is what makes the
9138
- * broker's picker show nothing rather than a false choice.
9139
- */
9140
- listSignals: method(zod.z.void(), zod.z.array(StreamSignalSchema).readonly()) }
9141
- };
9142
9039
  /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
9143
9040
  var StreamFormatSchema = zod.z.enum([
9144
9041
  "webrtc",
@@ -11517,6 +11414,118 @@ var detectionPipelineCapability = {
11517
11414
  methods: {}
11518
11415
  };
11519
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 = zod.z.enum(["device", "integration"]);
11484
+ var DeviceAdminLinkSchema = zod.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: zod.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: zod.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: zod.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(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
11527
+ };
11528
+ /**
11520
11529
  * Identity envelope for a device's upstream-system metadata.
11521
11530
  *
11522
11531
  * Two jobs:
@@ -12013,118 +12022,6 @@ var deviceAdoptionCapability = {
12013
12022
  }
12014
12023
  };
12015
12024
  /**
12016
- * device-admin-link — "this device has a management page of its own, and here
12017
- * is its address".
12018
- *
12019
- * ## Why this is not a `deviceConfig` cap
12020
- *
12021
- * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
12022
- * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
12023
- * patch back through a setter; it costs a `builderId` reducer in
12024
- * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
12025
- * renders a form section. This cap answers ONE question with ONE read and
12026
- * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
12027
- * block, no `settings`, no `runtimeState` and no reducer — exactly like
12028
- * `reboot`, the other pure-RPC device-native cap.
12029
- *
12030
- * ## Absent, and the difference between "no page" and "we cannot say"
12031
- *
12032
- * The two are answered at DIFFERENT layers, on purpose:
12033
- *
12034
- * - **"We cannot say"** → the provider never registers the cap for that
12035
- * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
12036
- * fan are reached only through a vendor cloud; there is no address to hand
12037
- * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
12038
- * conditioner DO have a LAN IP, and still have no HTTP management page
12039
- * behind it. None of them register, so `deviceManager.getBindings` never
12040
- * lists the cap and no surface asks.
12041
- * - **"This device has no page, and I know that"** → the provider registers
12042
- * and `getAdminLink` returns `null`. This is the answer for a device whose
12043
- * sibling DOES have a page: a Reolink battery camera reached over UDP by
12044
- * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
12045
- * transport, a Home Assistant broker authenticated by supervisor token
12046
- * (which carries no `baseUrl` at all).
12047
- *
12048
- * Both draw NOTHING. A button that opens a browser error is worse than no
12049
- * button, and D62 is the same rule from the other side: an off switch is
12050
- * reported off, never made to look broken. There is no third state where the
12051
- * UI renders a disabled button "because the device might have a page".
12052
- *
12053
- * ## The URL never carries credentials
12054
- *
12055
- * Not in userinfo, not in a query string. Every provider builds through
12056
- * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
12057
- * scheme and path as separate arguments — there is no parameter a secret could
12058
- * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
12059
- * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
12060
- * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
12061
- * keeps providers from hand-rolling one anyway.
12062
- *
12063
- * This matters here more than anywhere else in the repo, because every provider
12064
- * that knows a device's host knows its PASSWORD too: `{ host, port, username,
12065
- * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
12066
- * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
12067
- * camera's own page will ask for its own login. That is correct, and pre-
12068
- * filling it is the operator's business, not ours.
12069
- *
12070
- * ## It is a LAN fact
12071
- *
12072
- * The URL addresses the device where the NODE can see it. It is not proxied,
12073
- * not made reachable from outside, and not sent anywhere. A surface renders it
12074
- * as a link the operator's own browser follows, on the operator's own network,
12075
- * or renders nothing.
12076
- */
12077
- /**
12078
- * Whose page is it. The distinction is for the OPERATOR, who needs to know
12079
- * before clicking whether he is about to land on a camera's own web server or
12080
- * inside Home Assistant.
12081
- */
12082
- var AdminLinkTargetEnum = zod.z.enum(["device", "integration"]);
12083
- var DeviceAdminLinkSchema = zod.z.object({
12084
- /**
12085
- * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
12086
- * free of userinfo and of any credential-shaped query key.
12087
- */
12088
- url: zod.z.string(),
12089
- /**
12090
- * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
12091
- * The PROVIDER names it, because only the provider knows what the page is;
12092
- * a UI that invented the label from the addon id would call the Home
12093
- * Assistant device page "Provider Homeassistant".
12094
- */
12095
- label: zod.z.string(),
12096
- target: AdminLinkTargetEnum,
12097
- /**
12098
- * Host the URL points at, without scheme, port or path — for the tooltip, so
12099
- * an operator can see WHERE the button goes before he follows it. Redundant
12100
- * with `url` by construction; carried separately so no surface has to parse
12101
- * a URL to show it.
12102
- */
12103
- host: zod.z.string()
12104
- });
12105
- var deviceAdminLinkCapability = {
12106
- name: "device-admin-link",
12107
- scope: "device",
12108
- deviceNative: true,
12109
- mode: "singleton",
12110
- methods: {
12111
- /**
12112
- * The device's management page, or `null` when this device has none.
12113
- *
12114
- * `auth: 'admin'` deliberately. This is administration, not actuation —
12115
- * the same bucket as `reboot` and `camera-credentials`, and explicitly NOT
12116
- * the actuation set `scripts/check-actuation-not-admin.ts` protects (D403).
12117
- * The URL is also a statement about the LAN, which a household member with
12118
- * a `view` grant on a light has no reason to be handed.
12119
- *
12120
- * The surfaces gate on the QUERY, never on a role they guessed: a caller
12121
- * without the right loses the query and draws nothing, which is the same
12122
- * thing a device with no page draws. There is no path on which a button
12123
- * appears and then fails — the D403 failure mode, from the other end.
12124
- */
12125
- getAdminLink: method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
12126
- };
12127
- /**
12128
12025
  * `device-export` — collection cap for addons that export camstack
12129
12026
  * devices to external ecosystems (HomeAssistant via MQTT discovery,
12130
12027
  * HomeKit/HAP, Alexa Smart Home, …).
@@ -25471,6 +25368,87 @@ var storageProviderCapability = {
25471
25368
  })
25472
25369
  }
25473
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 = zod.z.object({
25406
+ /** Stable id chosen by the provider, e.g. `'motion'`, `'person'`, `'tamper'`. */
25407
+ code: zod.z.string().min(1),
25408
+ /** What an operator reads in the picker. The provider's own wording. */
25409
+ label: zod.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: zod.z.boolean()
25417
+ });
25418
+ var StreamSignalsStatusSchema = zod.z.object({
25419
+ signals: zod.z.array(StreamSignalSchema),
25420
+ lastFetchedAt: zod.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(zod.z.void(), zod.z.array(StreamSignalSchema).readonly()) }
25451
+ };
25474
25452
  /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
25475
25453
  var ProfileSettingsSchemaBridge = zod.z.unknown().nullable();
25476
25454
  var ProfileSettingsBagSchema = zod.z.record(zod.z.string(), zod.z.unknown());
@@ -28713,6 +28691,179 @@ getCredentials: method(zod.z.object({ deviceId: zod.z.number() }), CameraCredent
28713
28691
  }
28714
28692
  };
28715
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 = zod.z.object({
28737
+ x: zod.z.number().min(0).max(1),
28738
+ y: zod.z.number().min(0).max(1),
28739
+ width: zod.z.number().gt(0).max(1),
28740
+ height: zod.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 = zod.z.object({
28751
+ deviceId: zod.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 = zod.z.object({
28766
+ profile: zod.z.enum([
28767
+ "high",
28768
+ "mid",
28769
+ "low"
28770
+ ]),
28771
+ offered: zod.z.boolean(),
28772
+ /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
28773
+ missingSources: zod.z.array(zod.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: zod.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: zod.z.boolean()
28791
+ });
28792
+ var GridLayoutViewSchema = zod.z.object({
28793
+ /** The persisted grid row this camera was declared from. */
28794
+ instanceId: zod.z.string(),
28795
+ deviceId: zod.z.number().int().nonnegative(),
28796
+ name: zod.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: zod.z.number().int(),
28805
+ cells: zod.z.array(GridLayoutCellSchema),
28806
+ /** What the catalog will publish, and what it refuses to. Read-only. */
28807
+ profiles: zod.z.array(GridProfileOfferSchema)
28808
+ });
28809
+ var GridLayoutPatchSchema = zod.z.object({
28810
+ deviceId: zod.z.number().int().nonnegative(),
28811
+ name: zod.z.string().min(1).max(160).optional(),
28812
+ fps: zod.z.number().int().min(1).max(60).optional(),
28813
+ /** Which profiles to publish. See `GridProfileOffer.published`. */
28814
+ publishedProfiles: zod.z.array(zod.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: zod.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(zod.z.object({ deviceId: zod.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
+ /**
28716
28867
  * Carbon-monoxide alarm sensor. Drives Home Assistant `binary_sensor`
28717
28868
  * entries with `device_class: carbon_monoxide`. Push-driven.
28718
28869
  */
@@ -29569,390 +29720,6 @@ var dayNightCapability = {
29569
29720
  volatileStateFields: ["lastFetchedAt"]
29570
29721
  };
29571
29722
  /**
29572
- * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
29573
- * writes to the CAMERA's own card, on the camera's own schedule.
29574
- *
29575
- * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
29576
- * footage ledger, our storage locations, our retention. This one has a
29577
- * different authority — the camera's firmware — and per D62 it stores
29578
- * nothing of its own. Every value here is read from the camera and every
29579
- * write goes back to the camera; there is no CamStack-side mirror that
29580
- * could disagree with the device.
29581
- *
29582
- * ## One shape, two firmwares
29583
- *
29584
- * Measured 2026-09-22 against the live fleet:
29585
- *
29586
- * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
29587
- * | --- | --- | --- |
29588
- * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
29589
- * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
29590
- * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
29591
- * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
29592
- * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
29593
- * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
29594
- * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
29595
- * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
29596
- *
29597
- * The two schedule models look different and are the same thing in
29598
- * different coordinates: both answer "for this trigger, during which
29599
- * weekly windows does the camera record". {@link RecordWindow} is that
29600
- * question in one shape — Hikvision's ranges map straight onto it,
29601
- * Reolink's mask expands into hour-aligned windows.
29602
- *
29603
- * ## Union, not intersection
29604
- *
29605
- * **The same fields exist on every camera.** What differs per device is
29606
- * which VALUES that device accepts, and that is what {@link
29607
- * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
29608
- * per field plus the schedule's own limits. A control a camera cannot
29609
- * honour is rendered DISABLED WITH ITS REASON, never missing and never
29610
- * dead: disabled must not look like broken.
29611
- *
29612
- * ## Refusal by name
29613
- *
29614
- * A write a camera cannot honour is refused with a sentence the operator
29615
- * can read — never accepted and dropped. Both providers refuse through
29616
- * {@link describeOnboardRefusal}, so the vocabulary is one function and
29617
- * one test, not two hand-written vendor opinions.
29618
- *
29619
- * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
29620
- * `getOptions` advertises per-camera availability, `getStatus` (auto-
29621
- * injected from `status`) reports the live values, and a single
29622
- * `setSettings` mutation applies a partial change. No hand-written
29623
- * settings-contribution methods.
29624
- */
29625
- /**
29626
- * What makes the camera start recording during a window.
29627
- *
29628
- * The union of both vendors' vocabularies. `continuous` is Hikvision's
29629
- * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
29630
- * object-class triggers are Reolink-only today and the smart-event ones
29631
- * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
29632
- * firmwares measured — a camera that cannot record on a trigger simply
29633
- * does not list it in `options.schedule.triggers`, and a window naming
29634
- * it is REFUSED, not dropped.
29635
- */
29636
- var RecordTriggerSchema = zod.z.enum([
29637
- "continuous",
29638
- "motion",
29639
- "person",
29640
- "vehicle",
29641
- "animal",
29642
- "lineCrossing",
29643
- "intrusion",
29644
- "loitering",
29645
- "alarmInput"
29646
- ]);
29647
- /**
29648
- * One weekly recording window: "on `day`, from `startMinute` to
29649
- * `endMinute`, record on `trigger`".
29650
- *
29651
- * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
29652
- * both firmwares enumerate). Minutes are local camera time since
29653
- * midnight; `endMinute` may be 1440, meaning end of day — that is
29654
- * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
29655
- * collapsing it to 0 would turn a whole-day window into an empty one.
29656
- */
29657
- var RecordWindowSchema = zod.z.object({
29658
- trigger: RecordTriggerSchema,
29659
- day: zod.z.number().int().min(0).max(6),
29660
- startMinute: zod.z.number().int().min(0).max(1439),
29661
- endMinute: zod.z.number().int().min(1).max(1440)
29662
- });
29663
- /** Status of one physical volume, as the camera itself describes it. */
29664
- var OnboardStorageVolumeSchema = zod.z.object({
29665
- /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
29666
- id: zod.z.string(),
29667
- /** The camera's own name for it, when it gives one (`hddName`). */
29668
- label: zod.z.string().optional(),
29669
- status: zod.z.enum([
29670
- "ok",
29671
- "unformatted",
29672
- "error",
29673
- "offline",
29674
- "unknown"
29675
- ]),
29676
- /**
29677
- * Total size in MB, or **null when the camera did not say**.
29678
- *
29679
- * Never 0 for an unreadable value: a measurement that failed is not a
29680
- * measurement (D393), and a card whose size is unknown must not be
29681
- * rendered as a card of size zero.
29682
- */
29683
- capacityMb: zod.z.number().nullable(),
29684
- /**
29685
- * Free space in MB, or null when unknown.
29686
- *
29687
- * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
29688
- * 1439 both report exactly 11776 MB free — the fixed reserve a looping
29689
- * card converges on once it has wrapped. At loop steady state the
29690
- * number is identical whether the camera recorded yesterday or stopped
29691
- * a month ago.
29692
- */
29693
- freeMb: zod.z.number().nullable(),
29694
- /** True when the camera reports the volume writable (`property` RW). */
29695
- writable: zod.z.boolean().optional()
29696
- });
29697
- /**
29698
- * Whether the camera has usable onboard storage.
29699
- *
29700
- * Three states, never two: an unreadable answer is `unknown`, never
29701
- * folded into `absent`. Folding a failed read into "no card" is how a
29702
- * healthy camera gets reported as broken, and the inverse is how a
29703
- * camera with no card reports `ok` and then an empty list (D393).
29704
- */
29705
- var OnboardStorageSchema = zod.z.discriminatedUnion("kind", [
29706
- zod.z.object({
29707
- kind: zod.z.literal("present"),
29708
- volumes: zod.z.array(OnboardStorageVolumeSchema)
29709
- }),
29710
- zod.z.object({
29711
- kind: zod.z.literal("absent"),
29712
- reason: zod.z.string()
29713
- }),
29714
- zod.z.object({
29715
- kind: zod.z.literal("unknown"),
29716
- reason: zod.z.string()
29717
- })
29718
- ]);
29719
- /**
29720
- * One recording track the camera exposes. Read-only.
29721
- *
29722
- * Hikvision lists several — 1436 and 3833 each list 101 AND 103, with
29723
- * DIFFERENT schedules (101 MOTION all day, 103 CMR 00:05–23:57) — so a
29724
- * cap that reported one number for "the schedule" would be lying on
29725
- * every Hikvision in the fleet. Reolink has exactly one per channel.
29726
- *
29727
- * There is deliberately **no `framerate`**. Hikvision declares it in the
29728
- * track `Description` and it is WRONG: 1436 says `2.260000 fps` while
29729
- * the media measures 12.55 fps. `codec` and `resolution` from the same
29730
- * string ARE right — those are the only two worth surfacing.
29731
- */
29732
- var OnboardRecordTrackSchema = zod.z.object({
29733
- id: zod.z.string(),
29734
- enabled: zod.z.boolean(),
29735
- isVideo: zod.z.boolean(),
29736
- /** From the camera's own track description. Null when it does not say. */
29737
- codec: zod.z.string().nullable(),
29738
- resolution: zod.z.string().nullable(),
29739
- /** Per-track overwrite flag, where the firmware keeps it per track. */
29740
- overwriteWhenFull: zod.z.boolean().nullable()
29741
- });
29742
- /**
29743
- * What the camera is doing with its own storage, right now.
29744
- *
29745
- * Every scalar is nullable and **null means the camera did not answer**,
29746
- * never a default. A form that seeds `0` from an unanswered read invites
29747
- * the operator to save that 0 back onto the camera.
29748
- */
29749
- var RecordingOnboardStatusSchema = zod.z.object({
29750
- storage: OnboardStorageSchema,
29751
- tracks: zod.z.array(OnboardRecordTrackSchema),
29752
- /**
29753
- * The track the write path targets — the enabled VIDEO one. Null when
29754
- * no track could be identified, which is itself a refusal reason.
29755
- */
29756
- primaryTrackId: zod.z.string().nullable(),
29757
- /** Master "record to the card at all" switch. */
29758
- enabled: zod.z.boolean().nullable(),
29759
- overwriteWhenFull: zod.z.boolean().nullable(),
29760
- preRecordSec: zod.z.number().nullable(),
29761
- postRecordSec: zod.z.number().nullable(),
29762
- /** Length of one recorded file, in minutes. */
29763
- segmentMinutes: zod.z.number().nullable(),
29764
- /** The primary track's weekly windows, flattened. */
29765
- windows: zod.z.array(RecordWindowSchema),
29766
- /**
29767
- * How many windows the camera described that CamStack could NOT read —
29768
- * an unrecognised trigger, an unparseable clock, a weekday it does not
29769
- * name.
29770
- *
29771
- * A dropped window is work the reader threw away, and a schedule that
29772
- * silently shows fewer rows than the camera holds is how an operator
29773
- * saves back a schedule shorter than the one they were looking at
29774
- * (D391). Non-zero means the window list is INCOMPLETE and a write
29775
- * that replaces it would delete what was not shown — which is why a
29776
- * provider reporting a non-zero count also reports the schedule as not
29777
- * writable.
29778
- */
29779
- unreadableWindows: zod.z.number(),
29780
- /**
29781
- * The camera is scheduled to record and has NO usable storage.
29782
- *
29783
- * A first-class fact because it is the fleet's most common silent
29784
- * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
29785
- * to a card that is not there. Neither the schedule nor the storage
29786
- * read says anything wrong on its own; only the pair does.
29787
- */
29788
- recordingToNowhere: zod.z.boolean(),
29789
- lastFetchedAt: zod.z.number()
29790
- });
29791
- /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
29792
- var RangeSchema = zod.z.object({
29793
- min: zod.z.number(),
29794
- max: zod.z.number(),
29795
- step: zod.z.number()
29796
- });
29797
- /**
29798
- * The values a camera actually takes for a numeric field, when they are a SET
29799
- * rather than a range.
29800
- *
29801
- * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
29802
- * (I91DN) on 2026-09-22 by writing each value and reading it back:
29803
- *
29804
- * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
29805
- * camera's "no limit" — `-1` and `4294967295` both land on it);
29806
- * - post-record: `5, 10, 30, 60, 120, 300, 600`.
29807
- *
29808
- * Neither is expressible as a step: the first has a sentinel two billion away
29809
- * from its neighbours, the second doubles and then jumps. A range that tried
29810
- * would forbid values the camera takes AND permit values it silently replaces
29811
- * with 5 — wrong in both directions at once.
29812
- *
29813
- * `sentinel` names the member that is not a duration, so a surface can render
29814
- * "no limit" instead of `2147483647` seconds.
29815
- */
29816
- var AllowedValuesSchema = zod.z.object({
29817
- values: zod.z.array(zod.z.number()).min(1),
29818
- sentinel: zod.z.object({
29819
- value: zod.z.number(),
29820
- meaning: zod.z.enum(["no-limit", "disabled"])
29821
- }).optional()
29822
- });
29823
- /**
29824
- * Per-field availability on ONE camera.
29825
- *
29826
- * The field exists on every camera — this says whether this one can be
29827
- * read and whether it can be written, and `reason` says why not when
29828
- * either is false. The UI renders the control DISABLED with the reason
29829
- * rather than hiding it, so a limitation is legible instead of looking
29830
- * like a missing feature.
29831
- */
29832
- var OnboardFieldSupportSchema = zod.z.object({
29833
- readable: zod.z.boolean(),
29834
- writable: zod.z.boolean(),
29835
- /** Required whenever `readable` or `writable` is false. */
29836
- reason: zod.z.string().optional()
29837
- });
29838
- /** What this camera's schedule model can express. */
29839
- var OnboardScheduleSupportSchema = zod.z.object({
29840
- support: OnboardFieldSupportSchema,
29841
- /**
29842
- * The smallest time step the camera can express, in minutes.
29843
- *
29844
- * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
29845
- * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
29846
- * window whose edges are not a multiple of this is REFUSED rather than
29847
- * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
29848
- * and nothing says so.
29849
- */
29850
- granularityMinutes: zod.z.number(),
29851
- /** Triggers this camera can record on. A window naming another is refused. */
29852
- triggers: zod.z.array(RecordTriggerSchema),
29853
- /**
29854
- * False when the camera stores ONE trigger per time range, so two
29855
- * windows overlapping on the same day cannot carry different triggers.
29856
- * True on Reolink, whose mask is per-trigger and independent.
29857
- */
29858
- supportsOverlappingTriggers: zod.z.boolean()
29859
- });
29860
- var RecordingOnboardOptionsSchema = zod.z.object({
29861
- enabled: OnboardFieldSupportSchema,
29862
- overwriteWhenFull: OnboardFieldSupportSchema,
29863
- preRecordSec: OnboardFieldSupportSchema,
29864
- preRecordSecRange: RangeSchema.optional(),
29865
- /** Preferred over the range when the camera takes a SET, not a span. */
29866
- preRecordSecAllowed: AllowedValuesSchema.optional(),
29867
- postRecordSec: OnboardFieldSupportSchema,
29868
- postRecordSecRange: RangeSchema.optional(),
29869
- /** Preferred over the range when the camera takes a SET, not a span. */
29870
- postRecordSecAllowed: AllowedValuesSchema.optional(),
29871
- segmentMinutes: OnboardFieldSupportSchema,
29872
- segmentMinutesRange: RangeSchema.optional(),
29873
- /** Preferred over the range when the camera takes a SET, not a span. */
29874
- segmentMinutesAllowed: AllowedValuesSchema.optional(),
29875
- schedule: OnboardScheduleSupportSchema
29876
- });
29877
- /**
29878
- * A partial change. Every field optional.
29879
- *
29880
- * Unlike the other `deviceConfig` caps, a provider here does **NOT**
29881
- * silently ignore a field it cannot support — it refuses, by name,
29882
- * through {@link describeOnboardRefusal}. Silence on a recording setting
29883
- * is the failure D62 exists to prevent: the operator believes the camera
29884
- * is recording the way the form says, and it is not.
29885
- */
29886
- var RecordingOnboardPatchSchema = zod.z.object({
29887
- enabled: zod.z.boolean().optional(),
29888
- overwriteWhenFull: zod.z.boolean().optional(),
29889
- preRecordSec: zod.z.number().optional(),
29890
- postRecordSec: zod.z.number().optional(),
29891
- segmentMinutes: zod.z.number().optional(),
29892
- /** The complete new window set for the primary track — not a delta. */
29893
- windows: zod.z.array(RecordWindowSchema).optional()
29894
- });
29895
- var recordingOnboardCapability = {
29896
- name: "recording-onboard",
29897
- scope: "device",
29898
- deviceNative: true,
29899
- mode: "singleton",
29900
- deviceTypes: [DeviceType.Camera],
29901
- deviceConfig: { ui: {
29902
- kind: "derived-form",
29903
- builderId: "recording-onboard",
29904
- tab: "recording"
29905
- } },
29906
- methods: {
29907
- getOptions: method(zod.z.object({ deviceId: zod.z.number() }), RecordingOnboardOptionsSchema),
29908
- setSettings: method(zod.z.object({
29909
- deviceId: zod.z.number(),
29910
- settings: RecordingOnboardPatchSchema
29911
- }), zod.z.void(), {
29912
- kind: "mutation",
29913
- auth: "admin"
29914
- })
29915
- },
29916
- status: {
29917
- schema: RecordingOnboardStatusSchema,
29918
- kind: "poll"
29919
- },
29920
- runtimeState: RecordingOnboardStatusSchema,
29921
- /**
29922
- * Runtime-state durability: **restored** — operator-set camera-side
29923
- * recording config; mutation-driven, and the storage half is the last
29924
- * thing the camera said about its own card.
29925
- *
29926
- * See `RuntimeStateDurability`. Enforced by
29927
- * `scripts/check-runtime-state-durability.ts`.
29928
- */
29929
- durability: "restored",
29930
- /** Clock fields: written, but excluded from the compare that decides
29931
- * whether persisting is worth a SQLite commit. */
29932
- volatileStateFields: ["lastFetchedAt"]
29933
- };
29934
- /** Minutes in a day. `endMinute === 1440` means "to end of day". */
29935
- var MINUTES_PER_DAY = 1440;
29936
- /**
29937
- * Day-of-week names in the cap's index order (0 = Monday), for messages
29938
- * an operator reads and for Hikvision's `<DayOfWeek>` element.
29939
- */
29940
- var DAY_NAMES = [
29941
- "Monday",
29942
- "Tuesday",
29943
- "Wednesday",
29944
- "Thursday",
29945
- "Friday",
29946
- "Saturday",
29947
- "Sunday"
29948
- ];
29949
- /** `510` → `08:30`. For messages, not for the wire. */
29950
- function formatMinutes(minute) {
29951
- const hour = Math.floor(minute / 60);
29952
- const rest = minute % 60;
29953
- return `${String(hour).padStart(2, "0")}:${String(rest).padStart(2, "0")}`;
29954
- }
29955
- /**
29956
29723
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
29957
29724
  * for every device, regardless of provider — the kernel needs a uniform
29958
29725
  * cap-keyed slice for the basic device flags every consumer expects to
@@ -30171,40 +29938,6 @@ var eventEmitterCapability = {
30171
29938
  */
30172
29939
  durability: "session"
30173
29940
  };
30174
- var EventItemSchema = zod.z.object({
30175
- id: zod.z.string(),
30176
- type: zod.z.string(),
30177
- timestamp: zod.z.number(),
30178
- label: zod.z.string().optional(),
30179
- thumbnailUrl: zod.z.string().optional(),
30180
- clipUrl: zod.z.string().optional(),
30181
- metadata: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
30182
- });
30183
- var eventsCapability = {
30184
- name: "events",
30185
- scope: "device",
30186
- mode: "singleton",
30187
- deviceTypes: [DeviceType.Camera],
30188
- methods: {
30189
- getEvents: method(zod.z.object({
30190
- deviceId: zod.z.number(),
30191
- from: zod.z.number().optional(),
30192
- to: zod.z.number().optional(),
30193
- limit: zod.z.number().optional()
30194
- }), zod.z.array(EventItemSchema)),
30195
- getEventThumbnail: method(zod.z.object({
30196
- deviceId: zod.z.number(),
30197
- eventId: zod.z.string()
30198
- }), zod.z.object({
30199
- base64: zod.z.string(),
30200
- contentType: zod.z.string()
30201
- }).nullable()),
30202
- getEventClipUrl: method(zod.z.object({
30203
- deviceId: zod.z.number(),
30204
- eventId: zod.z.string()
30205
- }), zod.z.string().nullable())
30206
- }
30207
- };
30208
29941
  var IdentitySchema = zod.z.object({
30209
29942
  id: zod.z.string(),
30210
29943
  name: zod.z.string(),
@@ -32738,179 +32471,6 @@ var motionTriggerCapability = {
32738
32471
  durability: "session"
32739
32472
  };
32740
32473
  /**
32741
- * camera-grid-layout — the geometry of a COMPOSITE camera, on the camera's own
32742
- * page.
32743
- *
32744
- * ## Why this is a capability and not an addon settings schema
32745
- *
32746
- * It was one, and it did not render. The addon declared the editor as a
32747
- * `type: 'widget'` field inside its own `deviceSettingsSchema()`; the hub
32748
- * returned that section correctly and `ConfigFormField` renders `type:'widget'`
32749
- * perfectly well — and nothing ever asked camera-grid for it. The Cluster →
32750
- * Pipeline → Device Overrides page interrogates a HAND-WRITTEN list of four
32751
- * addons (`PIPELINE_CLUSTER_DEVICE_ADDONS`), whose own comment says an addon
32752
- * not on it "falls off silently".
32753
- *
32754
- * Adding a fifth name to that list would have been the wrong fix twice over:
32755
- * that page is per-camera DETECTION tuning, and a grid's geometry belongs
32756
- * beside PTZ and motion zones on the camera itself. The device page is
32757
- * BINDING-driven (D12), so the way in is a capability bound to the device —
32758
- * and this cap carries its section the way `recording` does, by RETURNING it
32759
- * from `getDeviceSettingsContribution`.
32760
- *
32761
- * Seven other widgets are still declared the other way, through a
32762
- * `deviceConfig.ui` block the framework derives a section from. That route
32763
- * gives the addon no say in where its own panel lands and no way to decline
32764
- * for a device the panel does not suit, which is why this one does not use it.
32765
- *
32766
- * ## Why one addon may implement it
32767
- *
32768
- * It is a device-scoped NATIVE cap, registered by the grid camera device
32769
- * itself. Nothing else declares a composite camera, so nothing else has a
32770
- * layout — and the device-scoped route means the widget asks THE camera, not
32771
- * "the camera-grid addon", which is what let the old custom-action pair be
32772
- * reached only by a caller that already knew the addon id.
32773
- *
32774
- * ## The tab
32775
- *
32776
- * `streaming`, not a top-tab of its own. A grid's geometry IS what its stream
32777
- * is, so the Streaming tab is where it belongs; a `grid` top-tab would need an
32778
- * entry in `WELL_KNOWN_TAB_MAP` or the device page renders the raw id as the
32779
- * label (measured on the robot camera, 2026-09-06 — a tab called "navigation"
32780
- * next to "PTZ").
32781
- */
32782
- /** A rectangle in normalized [0,1] coordinates of whatever contains it. */
32783
- var GridNormalizedRectSchema = zod.z.object({
32784
- x: zod.z.number().min(0).max(1),
32785
- y: zod.z.number().min(0).max(1),
32786
- width: zod.z.number().gt(0).max(1),
32787
- height: zod.z.number().gt(0).max(1)
32788
- });
32789
- /**
32790
- * One source camera, the part of its picture taken, and where that part lands.
32791
- *
32792
- * Both rectangles are NORMALIZED (D519): a source camera can change resolution
32793
- * — a profile switch, a firmware update, a substream that comes back different
32794
- * — and a stored PIXEL rectangle would quietly start cutting the wrong region,
32795
- * which is the class of bug nobody files.
32796
- */
32797
- var GridLayoutCellSchema = zod.z.object({
32798
- deviceId: zod.z.number().int().positive(),
32799
- /** The part of the SOURCE taken, normalized against the source. */
32800
- source: GridNormalizedRectSchema,
32801
- /** Where it lands, normalized against the CANVAS. */
32802
- cell: GridNormalizedRectSchema
32803
- });
32804
- /**
32805
- * Which profiles this grid can actually compose, and why not.
32806
- *
32807
- * A grid's `high` composes its sources' `high` and its `low` their `low`, so a
32808
- * profile is on offer only when EVERY source can serve it. The refusal NAMES
32809
- * the sources, because "this grid has no low" is not a finding — "615 has no
32810
- * low" is, and it is the one an operator can act on.
32811
- */
32812
- var GridProfileOfferSchema = zod.z.object({
32813
- profile: zod.z.enum([
32814
- "high",
32815
- "mid",
32816
- "low"
32817
- ]),
32818
- offered: zod.z.boolean(),
32819
- /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
32820
- missingSources: zod.z.array(zod.z.number().int().positive()),
32821
- /**
32822
- * The canvas this profile composes onto, `WxH`, or empty when it is not
32823
- * offered. DERIVED from the cells and the sources' own size at this profile —
32824
- * it is reported because nothing else in the system would ever say what the
32825
- * grid came out as, and because it is the number an operator would otherwise
32826
- * expect to type.
32827
- */
32828
- canvas: zod.z.string(),
32829
- /**
32830
- * Whether this profile is PUBLISHED, of the ones the grid could serve.
32831
- *
32832
- * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
32833
- * a 4K canvas built from 4K decodes — something to opt into, not something a
32834
- * viewer's adaptive should be handed by climbing to the top rung it can see.
32835
- * Default is `mid` + `low`.
32836
- */
32837
- published: zod.z.boolean()
32838
- });
32839
- var GridLayoutViewSchema = zod.z.object({
32840
- /** The persisted grid row this camera was declared from. */
32841
- instanceId: zod.z.string(),
32842
- deviceId: zod.z.number().int().nonnegative(),
32843
- name: zod.z.string(),
32844
- /**
32845
- * NO canvas size. A grid's resolution is not authored: each profile derives
32846
- * its own from the cells and its sources' dimensions. The two numbers that
32847
- * used to be here were a text field that silently decided both how much the
32848
- * composite cost and how sharp it was — see `profiles[].canvas` for what it
32849
- * came out as.
32850
- */
32851
- fps: zod.z.number().int(),
32852
- cells: zod.z.array(GridLayoutCellSchema),
32853
- /** What the catalog will publish, and what it refuses to. Read-only. */
32854
- profiles: zod.z.array(GridProfileOfferSchema)
32855
- });
32856
- var GridLayoutPatchSchema = zod.z.object({
32857
- deviceId: zod.z.number().int().nonnegative(),
32858
- name: zod.z.string().min(1).max(160).optional(),
32859
- fps: zod.z.number().int().min(1).max(60).optional(),
32860
- /** Which profiles to publish. See `GridProfileOffer.published`. */
32861
- publishedProfiles: zod.z.array(zod.z.enum([
32862
- "high",
32863
- "mid",
32864
- "low"
32865
- ])).max(3).optional(),
32866
- /**
32867
- * The whole cell list at once. A per-cell patch would need an ordering the
32868
- * editor does not have, and a half-applied layout is a picture nobody asked
32869
- * for.
32870
- */
32871
- cells: zod.z.array(GridLayoutCellSchema).max(16)
32872
- });
32873
- var cameraGridLayoutCapability = {
32874
- name: "camera-grid-layout",
32875
- scope: "device",
32876
- deviceNative: true,
32877
- mode: "singleton",
32878
- deviceTypes: [DeviceType.Camera],
32879
- /**
32880
- * The section is built by the ADDON and returned from
32881
- * `getDeviceSettingsContribution`, not derived by the framework from a
32882
- * `deviceConfig.ui` block.
32883
- *
32884
- * Both mechanisms render the same widget. This one hands the addon two
32885
- * things the framework-derived route cannot give it:
32886
- *
32887
- * - it chooses its own section, `tab`, `location` and `order`, the way any
32888
- * other setting does, instead of receiving them from a cap declaration;
32889
- * - it can DECLINE per device. A camera that is not a grid gets no section
32890
- * at all, rather than a widget that renders its own "not a grid" state.
32891
- *
32892
- * `recording` is the precedent (`recorder/recording-device-settings.ts`): it
32893
- * returns `null` for anything that is not a camera, so the Recording tab
32894
- * never appears there.
32895
- */
32896
- exposesDeviceSettings: true,
32897
- methods: {
32898
- /**
32899
- * The grid behind this device.
32900
- *
32901
- * `null` means ANSWERED and this camera is not a grid — not "not yet
32902
- * known". The widget renders its "this is not a grid camera" state only
32903
- * from this answer, never from an unresolved query (D315).
32904
- */
32905
- getLayout: method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), GridLayoutViewSchema.nullable(), { auth: "admin" }),
32906
- /** Write the geometry back. Returns the grid as it now stands, profiles included. */
32907
- saveLayout: method(GridLayoutPatchSchema, GridLayoutViewSchema, {
32908
- kind: "mutation",
32909
- auth: "admin"
32910
- })
32911
- }
32912
- };
32913
- /**
32914
32474
  * Motion-zones share the same MaskShape vocabulary as privacy-mask — the
32915
32475
  * on-camera motion-detection mask is a single `grid` region (a row-major
32916
32476
  * boolean cell lattice the camera's onboard VMD evaluates). Composing it as
@@ -35619,37 +35179,40 @@ var rebootCapability = {
35619
35179
  auth: "admin"
35620
35180
  }) }
35621
35181
  };
35622
- /**
35623
- * `recording` cap — footage availability + HLS playback manifests + per-device
35624
- * recording config. NOTE on events (source of truth, R5/C3): this cap carries
35625
- * NO event surface — `getPlaybackManifest` returns playlist URLs only. Timeline
35626
- * events (motion/object/audio) come from `pipelineAnalytics` (durable SQLite
35627
- * rows) and are the ONLY event surface — the recorder has none. The in-RAM
35628
- * playback markers it used to build were deleted on 2026-08-29 because nothing
35629
- * ever read them. Event<->footage joins are by time, padded with the shared
35630
- * `EVENT_PAD_MS` (`interfaces/recording-config.ts`).
35631
- */
35632
- var RecordingStatusSchema = zod.z.object({
35633
- deviceId: zod.z.number(),
35634
- enabled: zod.z.boolean(),
35635
- /** THE derived storage mode, from the one definition
35636
- * (`deriveRecordingMode`) — never a second enum. A duplicated list is how
35637
- * `on-device-decision` could have reached the recorder and not the status. */
35638
- activeMode: RecordingStorageModeSchema,
35639
- nodeId: zod.z.string(),
35640
- storageBytes: zod.z.number()
35641
- });
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";
35642
35185
  var RecordingRangeSchema = zod.z.object({
35643
35186
  profile: zod.z.string(),
35644
35187
  startMs: zod.z.number(),
35645
35188
  endMs: zod.z.number()
35646
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 = zod.z.enum(["read", "unreadable"]);
35647
35207
  var RecordingAvailabilitySchema = zod.z.object({
35648
35208
  deviceId: zod.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,
35649
35212
  ranges: zod.z.array(RecordingRangeSchema),
35650
35213
  /**
35651
- * Every profile this camera has footage in — not only the one `ranges`
35652
- * describes (D433).
35214
+ * Every profile this camera has footage in AT THIS SOURCE — not only the one
35215
+ * `ranges` describes (D433).
35653
35216
  *
35654
35217
  * `ranges` answers for ONE profile by design: the timeline is a single bar,
35655
35218
  * and enumerating all of them triples the directory reads for a bar that
@@ -35666,15 +35229,344 @@ var RecordingAvailabilitySchema = zod.z.object({
35666
35229
  });
35667
35230
  var RecordingDaysSchema = zod.z.object({
35668
35231
  deviceId: zod.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,
35669
35236
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
35670
35237
  days: zod.z.array(zod.z.number())
35671
35238
  });
35239
+ var RecordingManifestSchema = zod.z.object({
35240
+ deviceId: zod.z.number(),
35241
+ /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
35242
+ localMasterPath: zod.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: zod.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: zod.z.array(zod.z.string())
35254
+ });
35255
+ var RecordingSourceAvailabilitySchema = zod.z.object({
35256
+ state: zod.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: zod.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: zod.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 = zod.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: zod.z.string(),
35285
+ /** Operator-facing name of the source ("CamStack recordings", "SD card"). */
35286
+ label: zod.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: zod.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 = zod.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: zod.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: zod.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: zod.z.enum([
35336
+ "free",
35337
+ "forward",
35338
+ "segment"
35339
+ ]),
35340
+ /** Frame-step BACKWARD is meaningful. */
35341
+ stepBack: zod.z.boolean(),
35342
+ /** Whether the drag-scrub gesture is served, as opposed to refused by name. */
35343
+ scrub: zod.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: zod.z.array(zod.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: zod.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 = zod.z.discriminatedUnion("kind", [
35366
+ zod.z.object({
35367
+ kind: zod.z.literal("hls"),
35368
+ manifest: RecordingManifestSchema
35369
+ }),
35370
+ zod.z.object({
35371
+ kind: zod.z.literal("clip"),
35372
+ /** The `videoclips` source namespace this clip id belongs to. */
35373
+ source: zod.z.string(),
35374
+ clipId: zod.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: zod.z.number()
35379
+ }),
35380
+ zod.z.object({
35381
+ kind: zod.z.literal("none"),
35382
+ reason: zod.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(zod.z.object({ deviceId: zod.z.number() }), zod.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(zod.z.object({
35424
+ deviceId: zod.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: zod.z.string().min(1),
35445
+ fromMs: zod.z.number(),
35446
+ toMs: zod.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: zod.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(zod.z.object({
35463
+ deviceId: zod.z.number(),
35464
+ provider: zod.z.string().min(1),
35465
+ fromMs: zod.z.number(),
35466
+ toMs: zod.z.number(),
35467
+ tzOffsetMinutes: zod.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(zod.z.object({
35483
+ deviceId: zod.z.number(),
35484
+ provider: zod.z.string().min(1),
35485
+ fromMs: zod.z.number(),
35486
+ toMs: zod.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(zod.z.object({
35498
+ deviceId: zod.z.number(),
35499
+ provider: zod.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 = zod.z.object({
35555
+ deviceId: zod.z.number(),
35556
+ enabled: zod.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: zod.z.string(),
35562
+ storageBytes: zod.z.number()
35563
+ });
35672
35564
  /**
35673
35565
  * One camera's row in a `getAvailabilityBatch` answer.
35674
35566
  *
35675
- * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
35676
- * batch collapses the transport, not the work — plus the one thing the singular
35677
- * 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):
35678
35570
  *
35679
35571
  * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
35680
35572
  * no footage in the window", which is a real claim.
@@ -35706,22 +35598,6 @@ var RecordingDaysForDeviceSchema = zod.z.object({
35706
35598
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
35707
35599
  days: zod.z.array(zod.z.number()).readonly()
35708
35600
  });
35709
- var RecordingManifestSchema = zod.z.object({
35710
- deviceId: zod.z.number(),
35711
- /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
35712
- localMasterPath: zod.z.string().nullable(),
35713
- /** HTTP(S) URL to the master playlist on the recording node's playback server
35714
- * (the PRIMARY candidate); null when no recording / server. Carries the
35715
- * scoped playback token in its path. */
35716
- playbackUrl: zod.z.string().nullable(),
35717
- /**
35718
- * Candidate master-playlist URLs the client tries in order (LAN first, then
35719
- * remote — Tailscale/Cloudflare if the operator configured extra hosts), each
35720
- * carrying the same scoped token. `playbackUrl` is the first entry. Empty when
35721
- * there is no recording / server.
35722
- */
35723
- playbackEndpoints: zod.z.array(zod.z.string())
35724
- });
35725
35601
  /**
35726
35602
  * Recording storage usage for one camera — what the ARCHIVE holds for it,
35727
35603
  * across every profile and every resolvable location on this node.
@@ -35979,33 +35855,22 @@ var ReadWindowBytesResultSchema = zod.z.discriminatedUnion("kind", [zod.z.object
35979
35855
  * not a retry hint (retrying the same window would refuse again). */
35980
35856
  segmentEndMs: zod.z.number()
35981
35857
  })]);
35982
- var recordingCapability = {
35983
- name: "recording",
35858
+ var recordingArchiveCapability = {
35859
+ name: "recording-archive",
35984
35860
  scope: "system",
35985
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). */
35986
35865
  exposesDeviceSettings: true,
35987
35866
  status: {
35988
35867
  schema: RecordingStatusSchema,
35989
35868
  kind: "command-driven"
35990
35869
  },
35991
35870
  methods: {
35992
- getAvailability: method(zod.z.object({
35993
- deviceId: zod.z.number(),
35994
- fromMs: zod.z.number(),
35995
- toMs: zod.z.number(),
35996
- /**
35997
- * Answer for THIS profile instead of the preferred one (D433). Absent
35998
- * keeps the timeline's behaviour — one bar, one profile, one set of
35999
- * reads. `profilesWithFootage` on the answer says what may be asked
36000
- * for.
36001
- */
36002
- profile: zod.z.string().optional()
36003
- }), RecordingAvailabilitySchema, {
36004
- kind: "query",
36005
- auth: "protected"
36006
- }),
36007
35871
  /**
36008
- * `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.
36009
35874
  *
36010
35875
  * A multi-camera timeline re-asks availability for every camera in the grid
36011
35876
  * on every day change; fanned out that is one request per camera for N
@@ -36013,6 +35878,13 @@ var recordingCapability = {
36013
35878
  * `availabilityProfileFor` + `rangesIn`, run concurrently inside the
36014
35879
  * recorder) — only the transport collapses.
36015
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
+ *
36016
35888
  * `protected` for the same reason the singular method is: every id in
36017
35889
  * `deviceIds` is a device reference, so the F1 #3 gate refuses any camera
36018
35890
  * outside the caller's scope — one id out of scope refuses the CALL, it
@@ -36030,20 +35902,10 @@ var recordingCapability = {
36030
35902
  kind: "query",
36031
35903
  auth: "protected"
36032
35904
  }),
36033
- /** Which calendar days in [fromMs,toMs) have ≥1 recorded segment, bucketed by
36034
- * the client's local day (`tzOffsetMinutes` = minutes to add to UTC). Drives
36035
- * the theater date-picker's day dots. */
36036
- getDaysWithRecordings: method(zod.z.object({
36037
- deviceId: zod.z.number(),
36038
- fromMs: zod.z.number(),
36039
- toMs: zod.z.number(),
36040
- tzOffsetMinutes: zod.z.number()
36041
- }), RecordingDaysSchema, {
36042
- kind: "query",
36043
- auth: "protected"
36044
- }),
36045
35905
  /**
36046
- * `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}.
36047
35909
  *
36048
35910
  * The cheapest question in the product, asked once per camera per month
36049
35911
  * change. One directory read per day per camera at the owner, unchanged;
@@ -36062,14 +35924,6 @@ var recordingCapability = {
36062
35924
  kind: "query",
36063
35925
  auth: "protected"
36064
35926
  }),
36065
- getPlaybackManifest: method(zod.z.object({
36066
- deviceId: zod.z.number(),
36067
- fromMs: zod.z.number(),
36068
- toMs: zod.z.number()
36069
- }), RecordingManifestSchema, {
36070
- kind: "query",
36071
- auth: "protected"
36072
- }),
36073
35927
  getStorageUsage: method(zod.z.object({}), RecordingStorageUsageSchema, {
36074
35928
  kind: "query",
36075
35929
  auth: "admin"
@@ -36085,6 +35939,10 @@ var recordingCapability = {
36085
35939
  * value (D315, D393, D590). It carries schedules and retention, no secret,
36086
35940
  * and the per-device gate in `scope-access.ts` still applies; `setDeviceConfig`
36087
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.
36088
35946
  */
36089
35947
  getDeviceConfig: method(zod.z.object({ deviceId: zod.z.number() }), RecordingConfigSchema, {
36090
35948
  kind: "query",
@@ -36092,7 +35950,13 @@ var recordingCapability = {
36092
35950
  }),
36093
35951
  /** Locate footage at a wall-clock instant: the covering segment's window,
36094
35952
  * or a gap with the forward nearest covered edge. Used by a feeder running
36095
- * 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). */
36096
35960
  locateSegment: method(zod.z.object({
36097
35961
  deviceId: zod.z.number(),
36098
35962
  profile: zod.z.string(),
@@ -36741,6 +36605,390 @@ var recordingExportCapability = {
36741
36605
  }
36742
36606
  };
36743
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 = zod.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 = zod.z.object({
36694
+ trigger: RecordTriggerSchema,
36695
+ day: zod.z.number().int().min(0).max(6),
36696
+ startMinute: zod.z.number().int().min(0).max(1439),
36697
+ endMinute: zod.z.number().int().min(1).max(1440)
36698
+ });
36699
+ /** Status of one physical volume, as the camera itself describes it. */
36700
+ var OnboardStorageVolumeSchema = zod.z.object({
36701
+ /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
36702
+ id: zod.z.string(),
36703
+ /** The camera's own name for it, when it gives one (`hddName`). */
36704
+ label: zod.z.string().optional(),
36705
+ status: zod.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: zod.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: zod.z.number().nullable(),
36730
+ /** True when the camera reports the volume writable (`property` RW). */
36731
+ writable: zod.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 = zod.z.discriminatedUnion("kind", [
36742
+ zod.z.object({
36743
+ kind: zod.z.literal("present"),
36744
+ volumes: zod.z.array(OnboardStorageVolumeSchema)
36745
+ }),
36746
+ zod.z.object({
36747
+ kind: zod.z.literal("absent"),
36748
+ reason: zod.z.string()
36749
+ }),
36750
+ zod.z.object({
36751
+ kind: zod.z.literal("unknown"),
36752
+ reason: zod.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 = zod.z.object({
36769
+ id: zod.z.string(),
36770
+ enabled: zod.z.boolean(),
36771
+ isVideo: zod.z.boolean(),
36772
+ /** From the camera's own track description. Null when it does not say. */
36773
+ codec: zod.z.string().nullable(),
36774
+ resolution: zod.z.string().nullable(),
36775
+ /** Per-track overwrite flag, where the firmware keeps it per track. */
36776
+ overwriteWhenFull: zod.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 = zod.z.object({
36786
+ storage: OnboardStorageSchema,
36787
+ tracks: zod.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: zod.z.string().nullable(),
36793
+ /** Master "record to the card at all" switch. */
36794
+ enabled: zod.z.boolean().nullable(),
36795
+ overwriteWhenFull: zod.z.boolean().nullable(),
36796
+ preRecordSec: zod.z.number().nullable(),
36797
+ postRecordSec: zod.z.number().nullable(),
36798
+ /** Length of one recorded file, in minutes. */
36799
+ segmentMinutes: zod.z.number().nullable(),
36800
+ /** The primary track's weekly windows, flattened. */
36801
+ windows: zod.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: zod.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: zod.z.boolean(),
36825
+ lastFetchedAt: zod.z.number()
36826
+ });
36827
+ /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
36828
+ var RangeSchema = zod.z.object({
36829
+ min: zod.z.number(),
36830
+ max: zod.z.number(),
36831
+ step: zod.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 = zod.z.object({
36853
+ values: zod.z.array(zod.z.number()).min(1),
36854
+ sentinel: zod.z.object({
36855
+ value: zod.z.number(),
36856
+ meaning: zod.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 = zod.z.object({
36869
+ readable: zod.z.boolean(),
36870
+ writable: zod.z.boolean(),
36871
+ /** Required whenever `readable` or `writable` is false. */
36872
+ reason: zod.z.string().optional()
36873
+ });
36874
+ /** What this camera's schedule model can express. */
36875
+ var OnboardScheduleSupportSchema = zod.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: zod.z.number(),
36887
+ /** Triggers this camera can record on. A window naming another is refused. */
36888
+ triggers: zod.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: zod.z.boolean()
36895
+ });
36896
+ var RecordingOnboardOptionsSchema = zod.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 = zod.z.object({
36923
+ enabled: zod.z.boolean().optional(),
36924
+ overwriteWhenFull: zod.z.boolean().optional(),
36925
+ preRecordSec: zod.z.number().optional(),
36926
+ postRecordSec: zod.z.number().optional(),
36927
+ segmentMinutes: zod.z.number().optional(),
36928
+ /** The complete new window set for the primary track — not a delta. */
36929
+ windows: zod.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(zod.z.object({ deviceId: zod.z.number() }), RecordingOnboardOptionsSchema),
36944
+ setSettings: method(zod.z.object({
36945
+ deviceId: zod.z.number(),
36946
+ settings: RecordingOnboardPatchSchema
36947
+ }), zod.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
+ /**
36744
36992
  * A camera's own "record me NOW" LEVEL — a signal the device raises while
36745
36993
  * something it knows about is happening (a robot vacuum cleaning, a machine
36746
36994
  * running, a gate open) and lowers when it stops.
@@ -42514,7 +42762,7 @@ var CAP_NAMES_WITH_STATUS = [
42514
42762
  "privacy-mask",
42515
42763
  "ptz",
42516
42764
  "ptz-autotrack",
42517
- "recording",
42765
+ "recording-archive",
42518
42766
  "recording-onboard",
42519
42767
  "recording-signal",
42520
42768
  "scene-monitor",
@@ -42599,7 +42847,6 @@ var ALL_CAPABILITY_DEFINITIONS = [
42599
42847
  embeddingEncoderCapability,
42600
42848
  enumSensorCapability,
42601
42849
  eventEmitterCapability,
42602
- eventsCapability,
42603
42850
  faceGalleryCapability,
42604
42851
  failureContributionCapability,
42605
42852
  fanControlCapability,
@@ -42660,6 +42907,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
42660
42907
  ptzAutotrackCapability,
42661
42908
  rebootCapability,
42662
42909
  recordingCapability,
42910
+ recordingArchiveCapability,
42663
42911
  recordingExportCapability,
42664
42912
  recordingOnboardCapability,
42665
42913
  recordingSignalCapability,
@@ -44894,24 +45142,6 @@ var METHOD_ACCESS_MAP = Object.freeze({
44894
45142
  addonId: null,
44895
45143
  access: "view"
44896
45144
  },
44897
- "events.getEventClipUrl": {
44898
- capName: "events",
44899
- capScope: "device",
44900
- addonId: null,
44901
- access: "view"
44902
- },
44903
- "events.getEvents": {
44904
- capName: "events",
44905
- capScope: "device",
44906
- addonId: null,
44907
- access: "view"
44908
- },
44909
- "events.getEventThumbnail": {
44910
- capName: "events",
44911
- capScope: "device",
44912
- addonId: null,
44913
- access: "view"
44914
- },
44915
45145
  "faceGallery.assignFace": {
44916
45146
  capName: "face-gallery",
44917
45147
  capScope: "system",
@@ -47786,224 +48016,236 @@ var METHOD_ACCESS_MAP = Object.freeze({
47786
48016
  addonId: null,
47787
48017
  access: "create"
47788
48018
  },
47789
- "recording.applyDeviceSettingsPatch": {
48019
+ "recording.getAvailability": {
47790
48020
  capName: "recording",
47791
- capScope: "system",
48021
+ capScope: "device",
47792
48022
  addonId: null,
47793
- access: "create"
48023
+ access: "view"
47794
48024
  },
47795
- "recording.cancelRelocateJob": {
48025
+ "recording.getDaysWithRecordings": {
47796
48026
  capName: "recording",
47797
- capScope: "system",
48027
+ capScope: "device",
47798
48028
  addonId: null,
47799
- access: "create"
48029
+ access: "view"
47800
48030
  },
47801
- "recording.cancelStorageMigrationMove": {
48031
+ "recording.getPlayback": {
47802
48032
  capName: "recording",
47803
- capScope: "system",
48033
+ capScope: "device",
47804
48034
  addonId: null,
47805
- access: "create"
48035
+ access: "view"
47806
48036
  },
47807
- "recording.deleteFootprint": {
48037
+ "recording.getPlaybackOptions": {
47808
48038
  capName: "recording",
47809
- capScope: "system",
48039
+ capScope: "device",
47810
48040
  addonId: null,
47811
- access: "delete"
48041
+ access: "view"
47812
48042
  },
47813
- "recording.getAvailability": {
48043
+ "recording.listSources": {
47814
48044
  capName: "recording",
47815
- capScope: "system",
48045
+ capScope: "device",
47816
48046
  addonId: null,
47817
48047
  access: "view"
47818
48048
  },
47819
- "recording.getAvailabilityBatch": {
47820
- capName: "recording",
48049
+ "recordingArchive.applyDeviceSettingsPatch": {
48050
+ capName: "recording-archive",
47821
48051
  capScope: "system",
47822
48052
  addonId: null,
47823
- access: "view"
48053
+ access: "create"
47824
48054
  },
47825
- "recording.getDaysWithRecordings": {
47826
- capName: "recording",
48055
+ "recordingArchive.cancelRelocateJob": {
48056
+ capName: "recording-archive",
47827
48057
  capScope: "system",
47828
48058
  addonId: null,
47829
- access: "view"
48059
+ access: "create"
47830
48060
  },
47831
- "recording.getDaysWithRecordingsBatch": {
47832
- 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",
47833
48075
  capScope: "system",
47834
48076
  addonId: null,
47835
48077
  access: "view"
47836
48078
  },
47837
- "recording.getDeviceConfig": {
47838
- capName: "recording",
48079
+ "recordingArchive.getDaysWithRecordingsBatch": {
48080
+ capName: "recording-archive",
47839
48081
  capScope: "system",
47840
48082
  addonId: null,
47841
48083
  access: "view"
47842
48084
  },
47843
- "recording.getDeviceLiveContribution": {
47844
- capName: "recording",
48085
+ "recordingArchive.getDeviceConfig": {
48086
+ capName: "recording-archive",
47845
48087
  capScope: "system",
47846
48088
  addonId: null,
47847
48089
  access: "view"
47848
48090
  },
47849
- "recording.getDeviceSettingsContribution": {
47850
- capName: "recording",
48091
+ "recordingArchive.getDeviceLiveContribution": {
48092
+ capName: "recording-archive",
47851
48093
  capScope: "system",
47852
48094
  addonId: null,
47853
48095
  access: "view"
47854
48096
  },
47855
- "recording.getPlacement": {
47856
- capName: "recording",
48097
+ "recordingArchive.getDeviceSettingsContribution": {
48098
+ capName: "recording-archive",
47857
48099
  capScope: "system",
47858
48100
  addonId: null,
47859
48101
  access: "view"
47860
48102
  },
47861
- "recording.getPlaybackManifest": {
47862
- capName: "recording",
48103
+ "recordingArchive.getPlacement": {
48104
+ capName: "recording-archive",
47863
48105
  capScope: "system",
47864
48106
  addonId: null,
47865
48107
  access: "view"
47866
48108
  },
47867
- "recording.getRelocateResidue": {
47868
- capName: "recording",
48109
+ "recordingArchive.getRelocateResidue": {
48110
+ capName: "recording-archive",
47869
48111
  capScope: "system",
47870
48112
  addonId: null,
47871
48113
  access: "view"
47872
48114
  },
47873
- "recording.getStatus": {
47874
- capName: "recording",
48115
+ "recordingArchive.getStatus": {
48116
+ capName: "recording-archive",
47875
48117
  capScope: "system",
47876
48118
  addonId: null,
47877
48119
  access: "view"
47878
48120
  },
47879
- "recording.getStorageMigrationMoveStatus": {
47880
- capName: "recording",
48121
+ "recordingArchive.getStorageMigrationMoveStatus": {
48122
+ capName: "recording-archive",
47881
48123
  capScope: "system",
47882
48124
  addonId: null,
47883
48125
  access: "view"
47884
48126
  },
47885
- "recording.getStorageUsage": {
47886
- capName: "recording",
48127
+ "recordingArchive.getStorageUsage": {
48128
+ capName: "recording-archive",
47887
48129
  capScope: "system",
47888
48130
  addonId: null,
47889
48131
  access: "view"
47890
48132
  },
47891
- "recording.listOpsLog": {
47892
- capName: "recording",
48133
+ "recordingArchive.listOpsLog": {
48134
+ capName: "recording-archive",
47893
48135
  capScope: "system",
47894
48136
  addonId: null,
47895
48137
  access: "view"
47896
48138
  },
47897
- "recording.listRelocateJobs": {
47898
- capName: "recording",
48139
+ "recordingArchive.listRelocateJobs": {
48140
+ capName: "recording-archive",
47899
48141
  capScope: "system",
47900
48142
  addonId: null,
47901
48143
  access: "view"
47902
48144
  },
47903
- "recording.locateSegment": {
47904
- capName: "recording",
48145
+ "recordingArchive.locateSegment": {
48146
+ capName: "recording-archive",
47905
48147
  capScope: "system",
47906
48148
  addonId: null,
47907
48149
  access: "view"
47908
48150
  },
47909
- "recording.pauseForStorageMigration": {
47910
- capName: "recording",
48151
+ "recordingArchive.pauseForStorageMigration": {
48152
+ capName: "recording-archive",
47911
48153
  capScope: "system",
47912
48154
  addonId: null,
47913
48155
  access: "create"
47914
48156
  },
47915
- "recording.planStorageRebalance": {
47916
- capName: "recording",
48157
+ "recordingArchive.planStorageRebalance": {
48158
+ capName: "recording-archive",
47917
48159
  capScope: "system",
47918
48160
  addonId: null,
47919
48161
  access: "view"
47920
48162
  },
47921
- "recording.pruneFootage": {
47922
- capName: "recording",
48163
+ "recordingArchive.pruneFootage": {
48164
+ capName: "recording-archive",
47923
48165
  capScope: "system",
47924
48166
  addonId: null,
47925
48167
  access: "create"
47926
48168
  },
47927
- "recording.readGopBytes": {
47928
- capName: "recording",
48169
+ "recordingArchive.readGopBytes": {
48170
+ capName: "recording-archive",
47929
48171
  capScope: "system",
47930
48172
  addonId: null,
47931
48173
  access: "view"
47932
48174
  },
47933
- "recording.readSegmentBytes": {
47934
- capName: "recording",
48175
+ "recordingArchive.readSegmentBytes": {
48176
+ capName: "recording-archive",
47935
48177
  capScope: "system",
47936
48178
  addonId: null,
47937
48179
  access: "view"
47938
48180
  },
47939
- "recording.readWindowBytes": {
47940
- capName: "recording",
48181
+ "recordingArchive.readWindowBytes": {
48182
+ capName: "recording-archive",
47941
48183
  capScope: "system",
47942
48184
  addonId: null,
47943
48185
  access: "view"
47944
48186
  },
47945
- "recording.reconcileLedgerAgainstDisk": {
47946
- capName: "recording",
48187
+ "recordingArchive.reconcileLedgerAgainstDisk": {
48188
+ capName: "recording-archive",
47947
48189
  capScope: "system",
47948
48190
  addonId: null,
47949
48191
  access: "create"
47950
48192
  },
47951
- "recording.refreshStorageLocationsForMigration": {
47952
- capName: "recording",
48193
+ "recordingArchive.refreshStorageLocationsForMigration": {
48194
+ capName: "recording-archive",
47953
48195
  capScope: "system",
47954
48196
  addonId: null,
47955
48197
  access: "create"
47956
48198
  },
47957
- "recording.relocateFootage": {
47958
- capName: "recording",
48199
+ "recordingArchive.relocateFootage": {
48200
+ capName: "recording-archive",
47959
48201
  capScope: "system",
47960
48202
  addonId: null,
47961
48203
  access: "create"
47962
48204
  },
47963
- "recording.renderClip": {
47964
- capName: "recording",
48205
+ "recordingArchive.renderClip": {
48206
+ capName: "recording-archive",
47965
48207
  capScope: "system",
47966
48208
  addonId: null,
47967
48209
  access: "create"
47968
48210
  },
47969
- "recording.renderGif": {
47970
- capName: "recording",
48211
+ "recordingArchive.renderGif": {
48212
+ capName: "recording-archive",
47971
48213
  capScope: "system",
47972
48214
  addonId: null,
47973
48215
  access: "create"
47974
48216
  },
47975
- "recording.rescanStorage": {
47976
- capName: "recording",
48217
+ "recordingArchive.rescanStorage": {
48218
+ capName: "recording-archive",
47977
48219
  capScope: "system",
47978
48220
  addonId: null,
47979
48221
  access: "create"
47980
48222
  },
47981
- "recording.resumeForStorageMigration": {
47982
- capName: "recording",
48223
+ "recordingArchive.resumeForStorageMigration": {
48224
+ capName: "recording-archive",
47983
48225
  capScope: "system",
47984
48226
  addonId: null,
47985
48227
  access: "create"
47986
48228
  },
47987
- "recording.setDeviceConfig": {
47988
- capName: "recording",
48229
+ "recordingArchive.setDeviceConfig": {
48230
+ capName: "recording-archive",
47989
48231
  capScope: "system",
47990
48232
  addonId: null,
47991
48233
  access: "create"
47992
48234
  },
47993
- "recording.setDevicePlacement": {
47994
- capName: "recording",
48235
+ "recordingArchive.setDevicePlacement": {
48236
+ capName: "recording-archive",
47995
48237
  capScope: "system",
47996
48238
  addonId: null,
47997
48239
  access: "create"
47998
48240
  },
47999
- "recording.startStorageMigrationMove": {
48000
- capName: "recording",
48241
+ "recordingArchive.startStorageMigrationMove": {
48242
+ capName: "recording-archive",
48001
48243
  capScope: "system",
48002
48244
  addonId: null,
48003
48245
  access: "create"
48004
48246
  },
48005
- "recording.startStorageRebalance": {
48006
- capName: "recording",
48247
+ "recordingArchive.startStorageRebalance": {
48248
+ capName: "recording-archive",
48007
48249
  capScope: "system",
48008
48250
  addonId: null,
48009
48251
  access: "create"
@@ -50308,21 +50550,6 @@ Object.freeze({
50308
50550
  form: "single",
50309
50551
  optional: false
50310
50552
  }],
50311
- "events.getEventClipUrl": [{
50312
- name: "deviceId",
50313
- form: "single",
50314
- optional: false
50315
- }],
50316
- "events.getEvents": [{
50317
- name: "deviceId",
50318
- form: "single",
50319
- optional: false
50320
- }],
50321
- "events.getEventThumbnail": [{
50322
- name: "deviceId",
50323
- form: "single",
50324
- optional: false
50325
- }],
50326
50553
  "faceGallery.getFaceByTrack": [{
50327
50554
  name: "deviceId",
50328
50555
  form: "single",
@@ -51241,107 +51468,117 @@ Object.freeze({
51241
51468
  form: "single",
51242
51469
  optional: false
51243
51470
  }],
51244
- "recording.deleteFootprint": [{
51471
+ "recording.getAvailability": [{
51245
51472
  name: "deviceId",
51246
51473
  form: "single",
51247
51474
  optional: false
51248
51475
  }],
51249
- "recording.getAvailability": [{
51476
+ "recording.getDaysWithRecordings": [{
51250
51477
  name: "deviceId",
51251
51478
  form: "single",
51252
51479
  optional: false
51253
51480
  }],
51254
- "recording.getAvailabilityBatch": [{
51255
- name: "deviceIds",
51256
- form: "array",
51481
+ "recording.getPlayback": [{
51482
+ name: "deviceId",
51483
+ form: "single",
51257
51484
  optional: false
51258
51485
  }],
51259
- "recording.getDaysWithRecordings": [{
51486
+ "recording.getPlaybackOptions": [{
51260
51487
  name: "deviceId",
51261
51488
  form: "single",
51262
51489
  optional: false
51263
51490
  }],
51264
- "recording.getDaysWithRecordingsBatch": [{
51265
- name: "deviceIds",
51266
- form: "array",
51491
+ "recording.listSources": [{
51492
+ name: "deviceId",
51493
+ form: "single",
51267
51494
  optional: false
51268
51495
  }],
51269
- "recording.getDeviceConfig": [{
51496
+ "recordingArchive.deleteFootprint": [{
51270
51497
  name: "deviceId",
51271
51498
  form: "single",
51272
51499
  optional: false
51273
51500
  }],
51274
- "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": [{
51275
51512
  name: "deviceId",
51276
51513
  form: "single",
51277
51514
  optional: false
51278
51515
  }],
51279
- "recording.listOpsLog": [{
51516
+ "recordingArchive.listOpsLog": [{
51280
51517
  name: "deviceId",
51281
51518
  form: "single",
51282
51519
  optional: true
51283
51520
  }],
51284
- "recording.locateSegment": [{
51521
+ "recordingArchive.locateSegment": [{
51285
51522
  name: "deviceId",
51286
51523
  form: "single",
51287
51524
  optional: false
51288
51525
  }],
51289
- "recording.pruneFootage": [{
51526
+ "recordingArchive.pruneFootage": [{
51290
51527
  name: "deviceId",
51291
51528
  form: "single",
51292
51529
  optional: false
51293
51530
  }],
51294
- "recording.readGopBytes": [{
51531
+ "recordingArchive.readGopBytes": [{
51295
51532
  name: "deviceId",
51296
51533
  form: "single",
51297
51534
  optional: false
51298
51535
  }],
51299
- "recording.readSegmentBytes": [{
51536
+ "recordingArchive.readSegmentBytes": [{
51300
51537
  name: "deviceId",
51301
51538
  form: "single",
51302
51539
  optional: false
51303
51540
  }],
51304
- "recording.readWindowBytes": [{
51541
+ "recordingArchive.readWindowBytes": [{
51305
51542
  name: "deviceId",
51306
51543
  form: "single",
51307
51544
  optional: false
51308
51545
  }],
51309
- "recording.reconcileLedgerAgainstDisk": [{
51546
+ "recordingArchive.reconcileLedgerAgainstDisk": [{
51310
51547
  name: "deviceId",
51311
51548
  form: "single",
51312
51549
  optional: true
51313
51550
  }],
51314
- "recording.relocateFootage": [{
51551
+ "recordingArchive.relocateFootage": [{
51315
51552
  name: "deviceId",
51316
51553
  form: "single",
51317
51554
  optional: true
51318
51555
  }],
51319
- "recording.renderClip": [{
51556
+ "recordingArchive.renderClip": [{
51320
51557
  name: "deviceId",
51321
51558
  form: "single",
51322
51559
  optional: false
51323
51560
  }],
51324
- "recording.renderGif": [{
51561
+ "recordingArchive.renderGif": [{
51325
51562
  name: "deviceId",
51326
51563
  form: "single",
51327
51564
  optional: false
51328
51565
  }],
51329
- "recording.rescanStorage": [{
51566
+ "recordingArchive.rescanStorage": [{
51330
51567
  name: "deviceId",
51331
51568
  form: "single",
51332
51569
  optional: false
51333
51570
  }],
51334
- "recording.setDeviceConfig": [{
51571
+ "recordingArchive.setDeviceConfig": [{
51335
51572
  name: "deviceId",
51336
51573
  form: "single",
51337
51574
  optional: false
51338
51575
  }],
51339
- "recording.setDevicePlacement": [{
51576
+ "recordingArchive.setDevicePlacement": [{
51340
51577
  name: "deviceId",
51341
51578
  form: "single",
51342
51579
  optional: false
51343
51580
  }],
51344
- "recording.startStorageMigrationMove": [{
51581
+ "recordingArchive.startStorageMigrationMove": [{
51345
51582
  name: "deviceId",
51346
51583
  form: "single",
51347
51584
  optional: true
@@ -52933,22 +53170,10 @@ Object.defineProperty(exports, "MOTION_CLOSE_AFTER_MS", {
52933
53170
  return MOTION_CLOSE_AFTER_MS;
52934
53171
  }
52935
53172
  });
52936
- Object.defineProperty(exports, "PEER_BYTES_CHUNK_BYTES", {
52937
- enumerable: true,
52938
- get: function() {
52939
- return PEER_BYTES_CHUNK_BYTES;
52940
- }
52941
- });
52942
- Object.defineProperty(exports, "PEER_BYTES_STALL_MS", {
52943
- enumerable: true,
52944
- get: function() {
52945
- return PEER_BYTES_STALL_MS;
52946
- }
52947
- });
52948
- Object.defineProperty(exports, "PEER_BYTES_TICKET_TTL_MS", {
53173
+ Object.defineProperty(exports, "RECORDING_SOURCE_CAMSTACK_ADDON", {
52949
53174
  enumerable: true,
52950
53175
  get: function() {
52951
- return PEER_BYTES_TICKET_TTL_MS;
53176
+ return RECORDING_SOURCE_CAMSTACK_ADDON;
52952
53177
  }
52953
53178
  });
52954
53179
  Object.defineProperty(exports, "REDACTED_SECRET", {