@camstack/addon-pipeline 1.2.288 → 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 (33) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/{default-detection-model-DTvsx_iX.mjs → default-detection-model-DcTy-nJl.mjs} +1 -1
  4. package/dist/{default-detection-model-D5kMByfZ.js → default-detection-model-jjdmKKr3.js} +1 -1
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-mEtwupOP.mjs → dist-Bn6BQW8q.mjs} +1692 -920
  8. package/dist/{dist-CegiJ2Pw.js → dist-D7FonEUB.js} +1709 -919
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/{node-B8p-rV9H.mjs → node-BN0Na3HN.mjs} +1 -1
  12. package/dist/{node-C8jhiPW2.js → node-D3C1krEi.js} +1 -1
  13. package/dist/pipeline-runner/index.js +3 -3
  14. package/dist/pipeline-runner/index.mjs +3 -3
  15. package/dist/{process-memory-C5bRH8-t.mjs → process-memory-CeZ8ms80.mjs} +1 -1
  16. package/dist/{process-memory-lPbJ1ta2.js → process-memory-DI4jrCtX.js} +1 -1
  17. package/dist/recorder/index.js +7432 -7264
  18. package/dist/recorder/index.mjs +7432 -7264
  19. package/dist/{segment-demux-js-BP5vqRPY.mjs → segment-demux-js-CNPzlcyO.mjs} +1 -1
  20. package/dist/{segment-demux-js-Dv1EPPdH.js → segment-demux-js-COooJ-t0.js} +1 -1
  21. package/dist/stream-broker/_stub.js +2 -2
  22. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BqjkSRCu.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BrD7TK3W.mjs} +2 -2
  23. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-pSFdAaR0.mjs +26 -0
  24. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DhImOdbA.mjs +26 -0
  25. package/dist/stream-broker/demux-worker-child.js +1 -1
  26. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  27. package/dist/stream-broker/{hostInit-C4EuFQ_w.mjs → hostInit-CbSGtrCj.mjs} +2 -2
  28. package/dist/stream-broker/index.js +1465 -133
  29. package/dist/stream-broker/index.mjs +1465 -133
  30. package/dist/stream-broker/remoteEntry.js +1 -1
  31. package/package.json +5 -3
  32. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DqcT4fRG.mjs +0 -26
  33. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DCbVs3IC.mjs +0 -26
@@ -5360,7 +5360,7 @@ var ZodIssueCode = {
5360
5360
  var ZodFirstPartyTypeKind;
5361
5361
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5362
5362
  //#endregion
5363
- //#region ../types/dist/sleep-COWaSCAi.mjs
5363
+ //#region ../types/dist/sleep-D_4W7xuQ.mjs
5364
5364
  /**
5365
5365
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5366
5366
  * window to float samples (D455).
@@ -6544,6 +6544,24 @@ function normalizeAddonInitResult(result) {
6544
6544
  if (Array.isArray(result)) return { providers: result };
6545
6545
  return result;
6546
6546
  }
6547
+ /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
6548
+ var PeerBytesTicketSchema = object({
6549
+ /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
6550
+ url: string().min(1),
6551
+ /**
6552
+ * The HOST node this URL means something on — the hub or a named agent,
6553
+ * never a runner. {@link AddonPeerBytes.open} compares it to its own and
6554
+ * refuses `cross-node` by name when they differ, without dialling.
6555
+ */
6556
+ hostNodeId: string().min(1),
6557
+ expiresAtMs: number().int().nonnegative(),
6558
+ /**
6559
+ * What the producer DECLARED the body to be, when it knows — `null` when it
6560
+ * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
6561
+ * must be able to tell "the producer did not say" from "the body is empty".
6562
+ */
6563
+ declaredBytes: number().int().nonnegative().nullable()
6564
+ });
6547
6565
  /** Shared Zod schemas used across streaming capabilities. */
6548
6566
  var CamProfileSchema = _enum([
6549
6567
  "high",
@@ -7516,11 +7534,17 @@ var RawStateResultSchema = object({
7516
7534
  method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(object({ deviceId: number() }), array(ConfigEntrySchema$1)), method(object({
7517
7535
  deviceId: number(),
7518
7536
  values: record(string(), unknown())
7519
- }), _void(), { kind: "mutation" }), method(object({
7537
+ }), _void(), {
7538
+ kind: "mutation",
7539
+ auth: "admin"
7540
+ }), method(object({
7520
7541
  deviceId: number(),
7521
7542
  action: string().min(1),
7522
7543
  input: unknown()
7523
- }), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
7544
+ }), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), {
7545
+ kind: "mutation",
7546
+ auth: "admin"
7547
+ }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
7524
7548
  /**
7525
7549
  * The ONE retry schedule for work that can fail simply because the node was
7526
7550
  * still booting.
@@ -8740,24 +8764,6 @@ object({
8740
8764
  unreachable: number()
8741
8765
  })
8742
8766
  });
8743
- /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
8744
- var PeerBytesTicketSchema = object({
8745
- /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
8746
- url: string().min(1),
8747
- /**
8748
- * The HOST node this URL means something on — the hub or a named agent,
8749
- * never a runner. {@link AddonPeerBytes.open} compares it to its own and
8750
- * refuses `cross-node` by name when they differ, without dialling.
8751
- */
8752
- hostNodeId: string().min(1),
8753
- expiresAtMs: number().int().nonnegative(),
8754
- /**
8755
- * What the producer DECLARED the body to be, when it knows — `null` when it
8756
- * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
8757
- * must be able to tell "the producer did not say" from "the body is empty".
8758
- */
8759
- declaredBytes: number().int().nonnegative().nullable()
8760
- });
8761
8767
  /**
8762
8768
  * Adoption job — the background form of `device-adoption.adopt`.
8763
8769
  *
@@ -8869,7 +8875,7 @@ var AdoptionJobSchema = object({
8869
8875
  * component's original options — detection to the detection-pipeline wrapper
8870
8876
  * binding, audio analysis to its own, recording to `RecordingConfig.enabled`
8871
8877
  * (which was always first-class; the switch was a veneer over
8872
- * `recording.setDeviceConfig`), notifications to a notification-center
8878
+ * `recordingArchive.setDeviceConfig`), notifications to a notification-center
8873
8879
  * per-device setting, the two camera planes to their own components.
8874
8880
  *
8875
8881
  * What survives is {@link composeSwitchedOff}: `CameraStatus.switchedOff`, the
@@ -8895,7 +8901,7 @@ var AdoptionJobSchema = object({
8895
8901
  * | `stream-broker` | `deviceManager.setDisabled` | `StreamBrokerManager.reconcileAllCatalogs` releases the brokers; `ensureBroker` refuses re-creation |
8896
8902
  * | `object-detection` | `deviceManager.setWrapperActive('detection-pipeline')` | `PipelineSettingsStore.resolvePipelineForDevice` returns `{ steps: [], audio: null }` |
8897
8903
  * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
8898
- * | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
8904
+ * | `recording` | `recordingArchive.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
8899
8905
  * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
8900
8906
  * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
8901
8907
  * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
@@ -13608,60 +13614,8 @@ method(ListInputSchema, array(BrokerInfoSchema$1)), method(GetInputSchema, Broke
13608
13614
  }), method(UnsubscribeInputSchema, _void(), {
13609
13615
  kind: "mutation",
13610
13616
  auth: "admin"
13611
- }), method(GetStateInputSchema, unknown().nullable()), method(_void(), RegistryStatusSchema);
13617
+ }), method(GetStateInputSchema, unknown().nullable(), { auth: "admin" }), method(_void(), RegistryStatusSchema);
13612
13618
  DeviceType.Camera;
13613
- /**
13614
- * The signals a device can emit to WAKE its own stream.
13615
- *
13616
- * A camera whose stream is built on demand sleeps until something asks for it,
13617
- * and "something" cannot be a consumer that is merely attached — a Frigate-style
13618
- * puller holds a session open for ever, and treating that as demand would keep
13619
- * a battery camera awake for ever, which is the whole thing the battery is for
13620
- * (D173). So the wake has to come from the CAMERA: an event it noticed by
13621
- * itself, with no stream running.
13622
- *
13623
- * ## The vocabulary is the PROVIDER'S, not ours
13624
- *
13625
- * Like `consumables`, this cap declares no vocabulary of its own. A provider
13626
- * names each signal with a `code` it chooses and a `label` an operator reads.
13627
- * Reolink offers motion and camera-native detection; another provider may offer
13628
- * a tamper, a doorbell press, a PIR, or something no camera in this fleet has
13629
- * yet. A fixed enum here would mean every new signal is a framework release.
13630
- *
13631
- * It is deliberately NOT derived from the caps a device already binds. Whether
13632
- * a camera CAN push firmware motion is expressed by `motionSources` containing
13633
- * `'onboard'`, and whether it does AI on-camera by the `native-object-detection`
13634
- * binding — but both answer "what drives the detection pipeline", which is a
13635
- * different question from "what may wake a sleeping stream". A camera can do
13636
- * the first and not be trusted with the second, and the operator picks per
13637
- * camera. Two questions, two authorities.
13638
- *
13639
- * ## Availability is not permission
13640
- *
13641
- * `listSignals` says what the device CAN emit. Whether a given signal actually
13642
- * wakes the stream is the operator's per-camera choice, held by the broker
13643
- * alongside the cooldown — see the stream-broker cap's wake settings. A
13644
- * provider declaring a signal is not a provider enabling it.
13645
- */
13646
- /** One signal a device can emit. */
13647
- var StreamSignalSchema = object({
13648
- /** Stable id chosen by the provider, e.g. `'motion'`, `'person'`, `'tamper'`. */
13649
- code: string().min(1),
13650
- /** What an operator reads in the picker. The provider's own wording. */
13651
- label: string().min(1),
13652
- /**
13653
- * Whether the provider recommends this signal ON when a camera is first set
13654
- * up. A provider knows which of its signals are cheap and reliable; an
13655
- * operator should not have to discover that by trial. Reolink recommends
13656
- * both of its own.
13657
- */
13658
- recommended: boolean()
13659
- });
13660
- object({
13661
- signals: array(StreamSignalSchema),
13662
- lastFetchedAt: number()
13663
- });
13664
- Object.values(DeviceType), method(_void(), array(StreamSignalSchema).readonly());
13665
13619
  /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
13666
13620
  var StreamFormatSchema = _enum([
13667
13621
  "webrtc",
@@ -15095,17 +15049,20 @@ method(object({
15095
15049
  namespace: string().optional(),
15096
15050
  collection: string(),
15097
15051
  key: string()
15098
- }), unknown()), method(object({
15052
+ }), unknown(), { auth: "admin" }), method(object({
15099
15053
  namespace: string().optional(),
15100
15054
  collection: string(),
15101
15055
  key: string(),
15102
15056
  value: unknown()
15103
- }), _void(), { kind: "mutation" }), method(object({
15057
+ }), _void(), {
15058
+ kind: "mutation",
15059
+ auth: "admin"
15060
+ }), method(object({
15104
15061
  namespace: string().optional(),
15105
15062
  collection: string(),
15106
15063
  filter: QueryFilterSchema.optional(),
15107
15064
  columns: array(string()).readonly().optional()
15108
- }), array(SettingsRecordSchema).readonly()), method(object({
15065
+ }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
15109
15066
  namespace: string().optional(),
15110
15067
  collection: string(),
15111
15068
  /** Declared columns identifying the slot; must be covered by a UNIQUE
@@ -15115,7 +15072,10 @@ method(object({
15115
15072
  }), object({
15116
15073
  /** The id the row HAS: assigned by SQLite on an insert, or the existing
15117
15074
  * row's own id on an update. */
15118
- id: union([string(), number()]) }), { kind: "mutation" }), method(object({
15075
+ id: union([string(), number()]) }), {
15076
+ kind: "mutation",
15077
+ auth: "admin"
15078
+ }), method(object({
15119
15079
  namespace: string().optional(),
15120
15080
  collection: string(),
15121
15081
  record: object({
@@ -15128,38 +15088,56 @@ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
15128
15088
  * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
15129
15089
  * primary key — which is the only place an auto key is knowable.
15130
15090
  */
15131
- id: union([string(), number()]) }), { kind: "mutation" }), method(object({
15091
+ id: union([string(), number()]) }), {
15092
+ kind: "mutation",
15093
+ auth: "admin"
15094
+ }), method(object({
15132
15095
  namespace: string().optional(),
15133
15096
  collection: string(),
15134
15097
  records: array(BulkRecordSchema).readonly()
15135
- }), object({ inserted: number().int() }), { kind: "mutation" }), method(object({
15098
+ }), object({ inserted: number().int() }), {
15099
+ kind: "mutation",
15100
+ auth: "admin"
15101
+ }), method(object({
15136
15102
  namespace: string().optional(),
15137
15103
  collection: string(),
15138
15104
  id: string(),
15139
15105
  data: record(string(), unknown())
15140
- }), _void(), { kind: "mutation" }), method(object({
15106
+ }), _void(), {
15107
+ kind: "mutation",
15108
+ auth: "admin"
15109
+ }), method(object({
15141
15110
  namespace: string().optional(),
15142
15111
  collection: string(),
15143
15112
  key: string()
15144
- }), _void(), { kind: "mutation" }), method(object({
15113
+ }), _void(), {
15114
+ kind: "mutation",
15115
+ auth: "admin"
15116
+ }), method(object({
15145
15117
  namespace: string().optional(),
15146
15118
  collection: string(),
15147
15119
  filter: MutationFilterSchema
15148
- }), object({ deleted: number().int() }), { kind: "mutation" }), method(object({
15120
+ }), object({ deleted: number().int() }), {
15121
+ kind: "mutation",
15122
+ auth: "admin"
15123
+ }), method(object({
15149
15124
  namespace: string().optional(),
15150
15125
  collection: string(),
15151
15126
  filter: MutationFilterSchema,
15152
15127
  data: record(string(), unknown())
15153
- }), object({ updated: number().int() }), { kind: "mutation" }), method(object({
15128
+ }), object({ updated: number().int() }), {
15129
+ kind: "mutation",
15130
+ auth: "admin"
15131
+ }), method(object({
15154
15132
  namespace: string().optional(),
15155
15133
  collection: string(),
15156
15134
  filter: QueryFilterSchema.optional()
15157
- }), number()), method(object({
15135
+ }), number(), { auth: "admin" }), method(object({
15158
15136
  namespace: string().optional(),
15159
15137
  collection: string(),
15160
15138
  fields: array(AggregateFieldSchema).readonly(),
15161
15139
  filter: QueryFilterSchema.optional()
15162
- }), AggregateResultSchema), method(object({
15140
+ }), AggregateResultSchema, { auth: "admin" }), method(object({
15163
15141
  namespace: string().optional(),
15164
15142
  collection: string(),
15165
15143
  field: string(),
@@ -15169,15 +15147,18 @@ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
15169
15147
  }), array(object({
15170
15148
  bucket: number().int(),
15171
15149
  count: number().int()
15172
- })).readonly()), method(object({
15150
+ })).readonly(), { auth: "admin" }), method(object({
15173
15151
  namespace: string().optional(),
15174
15152
  collection: string()
15175
- }), boolean()), method(object({
15153
+ }), boolean(), { auth: "admin" }), method(object({
15176
15154
  namespace: string().optional(),
15177
15155
  collection: string(),
15178
15156
  columns: array(CollectionColumnSchema).readonly(),
15179
15157
  indexes: array(CollectionIndexSchema).readonly().optional()
15180
- }), _void(), { kind: "mutation" });
15158
+ }), _void(), {
15159
+ kind: "mutation",
15160
+ auth: "admin"
15161
+ });
15181
15162
  /**
15182
15163
  * What one engine says about itself. The orchestrator uses `kind` to pick
15183
15164
  * a registrant for a collection; `engineId` is what a log line names when
@@ -15410,6 +15391,97 @@ var detectionPipelineCapability = {
15410
15391
  methods: {}
15411
15392
  };
15412
15393
  /**
15394
+ * device-admin-link — "this device has a management page of its own, and here
15395
+ * is its address".
15396
+ *
15397
+ * ## Why this is not a `deviceConfig` cap
15398
+ *
15399
+ * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
15400
+ * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
15401
+ * patch back through a setter; it costs a `builderId` reducer in
15402
+ * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
15403
+ * renders a form section. This cap answers ONE question with ONE read and
15404
+ * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
15405
+ * block, no `settings`, no `runtimeState` and no reducer — exactly like
15406
+ * `reboot`, the other pure-RPC device-native cap.
15407
+ *
15408
+ * ## Absent, and the difference between "no page" and "we cannot say"
15409
+ *
15410
+ * The two are answered at DIFFERENT layers, on purpose:
15411
+ *
15412
+ * - **"We cannot say"** → the provider never registers the cap for that
15413
+ * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
15414
+ * fan are reached only through a vendor cloud; there is no address to hand
15415
+ * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
15416
+ * conditioner DO have a LAN IP, and still have no HTTP management page
15417
+ * behind it. None of them register, so `deviceManager.getBindings` never
15418
+ * lists the cap and no surface asks.
15419
+ * - **"This device has no page, and I know that"** → the provider registers
15420
+ * and `getAdminLink` returns `null`. This is the answer for a device whose
15421
+ * sibling DOES have a page: a Reolink battery camera reached over UDP by
15422
+ * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
15423
+ * transport, a Home Assistant broker authenticated by supervisor token
15424
+ * (which carries no `baseUrl` at all).
15425
+ *
15426
+ * Both draw NOTHING. A button that opens a browser error is worse than no
15427
+ * button, and D62 is the same rule from the other side: an off switch is
15428
+ * reported off, never made to look broken. There is no third state where the
15429
+ * UI renders a disabled button "because the device might have a page".
15430
+ *
15431
+ * ## The URL never carries credentials
15432
+ *
15433
+ * Not in userinfo, not in a query string. Every provider builds through
15434
+ * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
15435
+ * scheme and path as separate arguments — there is no parameter a secret could
15436
+ * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
15437
+ * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
15438
+ * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
15439
+ * keeps providers from hand-rolling one anyway.
15440
+ *
15441
+ * This matters here more than anywhere else in the repo, because every provider
15442
+ * that knows a device's host knows its PASSWORD too: `{ host, port, username,
15443
+ * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
15444
+ * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
15445
+ * camera's own page will ask for its own login. That is correct, and pre-
15446
+ * filling it is the operator's business, not ours.
15447
+ *
15448
+ * ## It is a LAN fact
15449
+ *
15450
+ * The URL addresses the device where the NODE can see it. It is not proxied,
15451
+ * not made reachable from outside, and not sent anywhere. A surface renders it
15452
+ * as a link the operator's own browser follows, on the operator's own network,
15453
+ * or renders nothing.
15454
+ */
15455
+ /**
15456
+ * Whose page is it. The distinction is for the OPERATOR, who needs to know
15457
+ * before clicking whether he is about to land on a camera's own web server or
15458
+ * inside Home Assistant.
15459
+ */
15460
+ var AdminLinkTargetEnum = _enum(["device", "integration"]);
15461
+ var DeviceAdminLinkSchema = object({
15462
+ /**
15463
+ * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
15464
+ * free of userinfo and of any credential-shaped query key.
15465
+ */
15466
+ url: string(),
15467
+ /**
15468
+ * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
15469
+ * The PROVIDER names it, because only the provider knows what the page is;
15470
+ * a UI that invented the label from the addon id would call the Home
15471
+ * Assistant device page "Provider Homeassistant".
15472
+ */
15473
+ label: string(),
15474
+ target: AdminLinkTargetEnum,
15475
+ /**
15476
+ * Host the URL points at, without scheme, port or path — for the tooltip, so
15477
+ * an operator can see WHERE the button goes before he follows it. Redundant
15478
+ * with `url` by construction; carried separately so no surface has to parse
15479
+ * a URL to show it.
15480
+ */
15481
+ host: string()
15482
+ });
15483
+ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" });
15484
+ /**
15413
15485
  * Identity envelope for a device's upstream-system metadata.
15414
15486
  *
15415
15487
  * Two jobs:
@@ -15737,97 +15809,6 @@ method(object({ integrationId: string() }), object({ filters: array(AdoptionFilt
15737
15809
  auth: "admin"
15738
15810
  });
15739
15811
  /**
15740
- * device-admin-link — "this device has a management page of its own, and here
15741
- * is its address".
15742
- *
15743
- * ## Why this is not a `deviceConfig` cap
15744
- *
15745
- * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
15746
- * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
15747
- * patch back through a setter; it costs a `builderId` reducer in
15748
- * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
15749
- * renders a form section. This cap answers ONE question with ONE read and
15750
- * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
15751
- * block, no `settings`, no `runtimeState` and no reducer — exactly like
15752
- * `reboot`, the other pure-RPC device-native cap.
15753
- *
15754
- * ## Absent, and the difference between "no page" and "we cannot say"
15755
- *
15756
- * The two are answered at DIFFERENT layers, on purpose:
15757
- *
15758
- * - **"We cannot say"** → the provider never registers the cap for that
15759
- * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
15760
- * fan are reached only through a vendor cloud; there is no address to hand
15761
- * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
15762
- * conditioner DO have a LAN IP, and still have no HTTP management page
15763
- * behind it. None of them register, so `deviceManager.getBindings` never
15764
- * lists the cap and no surface asks.
15765
- * - **"This device has no page, and I know that"** → the provider registers
15766
- * and `getAdminLink` returns `null`. This is the answer for a device whose
15767
- * sibling DOES have a page: a Reolink battery camera reached over UDP by
15768
- * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
15769
- * transport, a Home Assistant broker authenticated by supervisor token
15770
- * (which carries no `baseUrl` at all).
15771
- *
15772
- * Both draw NOTHING. A button that opens a browser error is worse than no
15773
- * button, and D62 is the same rule from the other side: an off switch is
15774
- * reported off, never made to look broken. There is no third state where the
15775
- * UI renders a disabled button "because the device might have a page".
15776
- *
15777
- * ## The URL never carries credentials
15778
- *
15779
- * Not in userinfo, not in a query string. Every provider builds through
15780
- * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
15781
- * scheme and path as separate arguments — there is no parameter a secret could
15782
- * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
15783
- * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
15784
- * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
15785
- * keeps providers from hand-rolling one anyway.
15786
- *
15787
- * This matters here more than anywhere else in the repo, because every provider
15788
- * that knows a device's host knows its PASSWORD too: `{ host, port, username,
15789
- * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
15790
- * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
15791
- * camera's own page will ask for its own login. That is correct, and pre-
15792
- * filling it is the operator's business, not ours.
15793
- *
15794
- * ## It is a LAN fact
15795
- *
15796
- * The URL addresses the device where the NODE can see it. It is not proxied,
15797
- * not made reachable from outside, and not sent anywhere. A surface renders it
15798
- * as a link the operator's own browser follows, on the operator's own network,
15799
- * or renders nothing.
15800
- */
15801
- /**
15802
- * Whose page is it. The distinction is for the OPERATOR, who needs to know
15803
- * before clicking whether he is about to land on a camera's own web server or
15804
- * inside Home Assistant.
15805
- */
15806
- var AdminLinkTargetEnum = _enum(["device", "integration"]);
15807
- var DeviceAdminLinkSchema = object({
15808
- /**
15809
- * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
15810
- * free of userinfo and of any credential-shaped query key.
15811
- */
15812
- url: string(),
15813
- /**
15814
- * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
15815
- * The PROVIDER names it, because only the provider knows what the page is;
15816
- * a UI that invented the label from the addon id would call the Home
15817
- * Assistant device page "Provider Homeassistant".
15818
- */
15819
- label: string(),
15820
- target: AdminLinkTargetEnum,
15821
- /**
15822
- * Host the URL points at, without scheme, port or path — for the tooltip, so
15823
- * an operator can see WHERE the button goes before he follows it. Redundant
15824
- * with `url` by construction; carried separately so no surface has to parse
15825
- * a URL to show it.
15826
- */
15827
- host: string()
15828
- });
15829
- method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" });
15830
- /**
15831
15812
  * `device-export` — collection cap for addons that export camstack
15832
15813
  * devices to external ecosystems (HomeAssistant via MQTT discovery,
15833
15814
  * HomeKit/HAP, Alexa Smart Home, …).
@@ -15892,7 +15873,13 @@ var ExposeInputSchema = object({
15892
15873
  capabilities: array(string()).optional()
15893
15874
  });
15894
15875
  var UnexposeInputSchema = object({ deviceId: string() });
15895
- method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), { kind: "mutation" }), method(UnexposeInputSchema, _void(), { kind: "mutation" });
15876
+ method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
15877
+ kind: "mutation",
15878
+ auth: "admin"
15879
+ }), method(UnexposeInputSchema, _void(), {
15880
+ kind: "mutation",
15881
+ auth: "admin"
15882
+ });
15896
15883
  /** Wire/storage schema for one icon id. */
15897
15884
  var LocationIconIdSchema = _enum([
15898
15885
  "map-pin",
@@ -16396,7 +16383,10 @@ method(object({
16396
16383
  kind: "mutation",
16397
16384
  auth: "admin",
16398
16385
  timeoutMs: 12 * 6e4
16399
- }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), { kind: "mutation" }), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
16386
+ }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), {
16387
+ kind: "mutation",
16388
+ auth: "admin"
16389
+ }), method(object({ deviceId: number() }), record(string(), unknown()), { auth: "admin" }), method(object({ deviceId: number() }), record(string(), unknown()), { auth: "admin" }), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
16400
16390
  deviceId: number(),
16401
16391
  name: string()
16402
16392
  }), _void(), {
@@ -16601,7 +16591,7 @@ method(object({
16601
16591
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
16602
16592
  kind: "mutation",
16603
16593
  auth: "admin"
16604
- }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
16594
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({ capNames: array(string().min(1)).min(1).max(16) }), array(number())), method(object({
16605
16595
  deviceId: number(),
16606
16596
  capName: string(),
16607
16597
  wrapperAddonId: string(),
@@ -18219,7 +18209,22 @@ var StatusSchema = object({
18219
18209
  brokerCount: number(),
18220
18210
  embeddedRunning: boolean()
18221
18211
  });
18222
- method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema), method(AddBrokerInputSchema, AddBrokerResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(IdInputSchema, TestResultSchema$1, { kind: "mutation" }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(_void(), StatusSchema);
18212
+ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
18213
+ kind: "mutation",
18214
+ auth: "admin"
18215
+ }), method(IdInputSchema, _void(), {
18216
+ kind: "mutation",
18217
+ auth: "admin"
18218
+ }), method(IdInputSchema, TestResultSchema$1, {
18219
+ kind: "mutation",
18220
+ auth: "admin"
18221
+ }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
18222
+ kind: "mutation",
18223
+ auth: "admin"
18224
+ }), method(IdInputSchema, _void(), {
18225
+ kind: "mutation",
18226
+ auth: "admin"
18227
+ }), method(_void(), StatusSchema);
18223
18228
  var NetworkEndpointSchema = object({
18224
18229
  url: string(),
18225
18230
  hostname: string(),
@@ -18514,6 +18519,18 @@ var TargetKindSchema = object({
18514
18519
  */
18515
18520
  iconMediaType: string().optional(),
18516
18521
  configSchema: ConfigSchemaPassthrough,
18522
+ /**
18523
+ * The config keys that decide WHERE this kind's secrets are sent (server,
18524
+ * base URL, instance, chat, recipient). A save that changes any of them
18525
+ * while leaving a secret redacted is refused by the provider (D634), so an
18526
+ * editor uses this to ask for the secret on exactly those edits instead of
18527
+ * on every edit.
18528
+ *
18529
+ * Optional: absent from a provider that holds no secret (the Home Assistant
18530
+ * kinds) and from a provider older than this field — an editor that finds
18531
+ * it absent must assume ANY edit may need the secret.
18532
+ */
18533
+ destinationKeys: array(string()).optional(),
18517
18534
  supportsDiscovery: boolean(),
18518
18535
  caps: TargetKindCapsSchema
18519
18536
  });
@@ -18552,19 +18569,34 @@ var SendResultSchema = object({
18552
18569
  });
18553
18570
  /** Same shape as SendResult — kept as a distinct name for the test panel. */
18554
18571
  var TestResultSchema = SendResultSchema;
18555
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
18572
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({
18556
18573
  kind: string(),
18557
18574
  config: record(string(), unknown()).optional()
18558
- }), array(DiscoveredTargetSchema)), method(object({
18575
+ }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
18559
18576
  targetId: string(),
18560
18577
  notification: NotificationSchema
18561
- }), SendResultSchema, { kind: "mutation" }), method(object({
18578
+ }), SendResultSchema, {
18579
+ kind: "mutation",
18580
+ auth: "admin"
18581
+ }), method(object({
18562
18582
  targetId: string(),
18563
18583
  sample: NotificationSchema.optional()
18564
- }), TestResultSchema, { kind: "mutation" }), method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }), method(object({ targetId: string() }), _void(), { kind: "mutation" }), method(object({
18584
+ }), TestResultSchema, {
18585
+ kind: "mutation",
18586
+ auth: "admin"
18587
+ }), method(object({ target: TargetSchema }), TargetSchema, {
18588
+ kind: "mutation",
18589
+ auth: "admin"
18590
+ }), method(object({ targetId: string() }), _void(), {
18591
+ kind: "mutation",
18592
+ auth: "admin"
18593
+ }), method(object({
18565
18594
  targetId: string(),
18566
18595
  enabled: boolean()
18567
- }), _void(), { kind: "mutation" });
18596
+ }), _void(), {
18597
+ kind: "mutation",
18598
+ auth: "admin"
18599
+ });
18568
18600
  var COCO_80_LABELS = [
18569
18601
  {
18570
18602
  id: "person",
@@ -19411,6 +19443,7 @@ sub("presence", "sensor", "sensor", "#22c55e", "presence", "Presence");
19411
19443
  sub("enum-sensor", "sensor", "sensor", TAXONOMY_COLORS.sensor, "generic", "Sensor state");
19412
19444
  sub("device-event", "sensor", "sensor", "#10b981", "button", "Device event");
19413
19445
  sub("lock", "control", "control", "#0ea5e9", "lock", "Lock");
19446
+ sub("cover", "control", "control", "#0ea5e9", "door", "Cover");
19414
19447
  sub("switch", "control", "control", TAXONOMY_COLORS.control, "switch", "Switch");
19415
19448
  sub("siren", "control", "control", "#dc2626", "siren", "Siren");
19416
19449
  sub("button", "control", "control", "#10b981", "button", "Button");
@@ -19691,6 +19724,10 @@ var NcSystemEventKindSchema = _enum([
19691
19724
  "alarm-disarmed",
19692
19725
  "alarm-arming",
19693
19726
  "alarm-arm-refused",
19727
+ "alarm-pending",
19728
+ "alarm-rearmed",
19729
+ "alarm-sensor-bypass",
19730
+ "alarm-check-failed",
19694
19731
  "addon-updated",
19695
19732
  "server-updated",
19696
19733
  "export-completed",
@@ -19728,6 +19765,19 @@ var NcSystemEventConditionSchema = object({
19728
19765
  * does not carry) matches no `deviceTypes` list.
19729
19766
  */
19730
19767
  deviceTypes: array(string().min(1)).min(1).optional(),
19768
+ /**
19769
+ * Devices this rule NEVER speaks about, whatever else matched. Scoped to the
19770
+ * same kind family as `deviceIds` (`systemEventFilterApplies(kind,
19771
+ * 'deviceIds')`) and applied AFTER it: include first, then exclude. A
19772
+ * consumable subject (`consumable:<id>:<key>`) is excluded by its device id.
19773
+ *
19774
+ * It never fails closed: a subject that carries no device id is not in the
19775
+ * list, so it is not excluded. "Every battery except the one thermometer that
19776
+ * reads 0 % forever" is the question this answers — expressed as an include
19777
+ * list it would have to name every other battery device, and every device
19778
+ * adopted later would be silently left out (D636).
19779
+ */
19780
+ excludeDeviceIds: array(number().int()).min(1).optional(),
19731
19781
  nodeIds: array(string().min(1)).min(1).optional(),
19732
19782
  packageNames: array(string().min(1)).min(1).optional()
19733
19783
  });
@@ -20286,6 +20336,19 @@ var NcMediaFrameSchema = _enum([
20286
20336
  "full",
20287
20337
  "boxed"
20288
20338
  ]);
20339
+ /**
20340
+ * How a SENSOR notification shows the cameras that link the sensor.
20341
+ *
20342
+ * `mosaic` — one image composed of every linked camera's photograph, taken at
20343
+ * the trigger. `each` — one image per camera where the target takes several
20344
+ * attachments; a target that takes one keeps the first camera's (the degrade
20345
+ * engine drops the rest by order). Either way ONE notification per event.
20346
+ * Absent = `mosaic`, applied by the Notification Center and deliberately NOT
20347
+ * a Zod default (see NcRulePatchSchema: a default materialises on every
20348
+ * partial patch). Meaningless for a rule whose subject is not a sensor, and a
20349
+ * sensor linked to one camera ships that camera's plain snapshot either way.
20350
+ */
20351
+ var NcLinkedCameraModeSchema = _enum(["mosaic", "each"]);
20289
20352
  var NcMediaPolicySchema = object({
20290
20353
  attach: _enum([
20291
20354
  "best",
@@ -20331,7 +20394,9 @@ var NcMediaPolicySchema = object({
20331
20394
  * A profile that is not assigned falls back to the cheapest, and the render
20332
20395
  * reports which one actually ran.
20333
20396
  */
20334
- profile: CamProfileSchema.optional()
20397
+ profile: CamProfileSchema.optional(),
20398
+ /** See {@link NcLinkedCameraModeSchema}. */
20399
+ linkedCameras: NcLinkedCameraModeSchema.optional()
20335
20400
  });
20336
20401
  /**
20337
20402
  * Cooldown GRANULARITY over the subject's class — how much a fired
@@ -20451,7 +20516,7 @@ var NcRuleInputSchema = object({
20451
20516
  cooldownSec: 60,
20452
20517
  scope: "rule-device"
20453
20518
  }),
20454
- /** `{{var}}` templating over camera/class/label/zones/confidence/time. */
20519
+ /** `{{var}}` title/body. The variables are declared in `NC_TEMPLATE_VARS` (@camstack/types) and depend on the rule kind — see `templateVarsFor`. */
20455
20520
  template: object({
20456
20521
  title: string().max(500).optional(),
20457
20522
  body: string().max(2e3).optional()
@@ -20631,6 +20696,22 @@ var NcRulePatchSchema = NcRuleInputSchema.partial().extend({
20631
20696
  throttle: NcThrottleSchema.optional(),
20632
20697
  priority: number().int().min(1).max(5).optional()
20633
20698
  });
20699
+ var NcRuleClearableKeySchema = _enum([
20700
+ "template",
20701
+ "schedule",
20702
+ "targetUsers",
20703
+ "snoozeOptions",
20704
+ "snoozeAllowGlobal",
20705
+ "waitForEnhancement",
20706
+ "actions",
20707
+ "confirm",
20708
+ "groupIdleSec"
20709
+ ]);
20710
+ var NcRuleUpdateInputSchema = object({
20711
+ ruleId: string(),
20712
+ patch: NcRulePatchSchema,
20713
+ clear: array(NcRuleClearableKeySchema).optional()
20714
+ });
20634
20715
  /** A persisted rule. */
20635
20716
  var NcRuleSchema = NcRuleInputSchema.extend({
20636
20717
  id: string(),
@@ -20662,6 +20743,85 @@ var NcTestResultSchema = object({
20662
20743
  className: string().optional(),
20663
20744
  label: string().optional()
20664
20745
  });
20746
+ /** The five rule kinds, as data. `NcRuleKind` in `notification/rule-kinds.ts`
20747
+ * is the same union; the template-vars spec pins the two together. */
20748
+ var NcRuleKindSchema = _enum([
20749
+ "detection",
20750
+ "sensor",
20751
+ "occupancy",
20752
+ "sound",
20753
+ "system"
20754
+ ]);
20755
+ /** Which producer renders a template: an ordinary rule, a timelapse, a digest. */
20756
+ var NcTemplateFamilySchema = _enum([
20757
+ "rule",
20758
+ "timelapse",
20759
+ "summary"
20760
+ ]);
20761
+ /** Which text field of that producer. `previewText` is the timelapse frame
20762
+ * caption, `captionText` the digest mosaic caption — both see fewer vars. */
20763
+ var NcTemplateFieldSchema = _enum([
20764
+ "title",
20765
+ "body",
20766
+ "previewText",
20767
+ "captionText"
20768
+ ]);
20769
+ var NcTemplateVarGroupSchema = _enum([
20770
+ "subject",
20771
+ "place",
20772
+ "time",
20773
+ "rule",
20774
+ "occupancy",
20775
+ "sound",
20776
+ "sensor",
20777
+ "system",
20778
+ "digest",
20779
+ "ai"
20780
+ ]);
20781
+ /**
20782
+ * ONE `{{var}}` a notification template may name.
20783
+ *
20784
+ * `families` / `fields` / `kinds` / `deliveries` / `systemEventKinds` say WHERE
20785
+ * it has a value; absent = no restriction on that axis. `systemEventKinds` is
20786
+ * read only for a `system` rule. `dynamic` declares a FAMILY of names
20787
+ * (`count_<class>`): the descriptor's `name` is the example member.
20788
+ */
20789
+ var NcTemplateVarDescriptorSchema = object({
20790
+ name: string().regex(/^\w+$/),
20791
+ label: string(),
20792
+ description: string().optional(),
20793
+ example: string(),
20794
+ group: NcTemplateVarGroupSchema,
20795
+ families: array(NcTemplateFamilySchema).min(1),
20796
+ fields: array(NcTemplateFieldSchema).optional(),
20797
+ kinds: array(NcRuleKindSchema).optional(),
20798
+ deliveries: array(NcDeliverySchema).optional(),
20799
+ systemEventKinds: array(NcSystemEventKindSchema).optional(),
20800
+ dynamic: object({
20801
+ prefix: literal("count_"),
20802
+ from: literal("classes")
20803
+ }).optional()
20804
+ });
20805
+ /** Input to `previewTemplate` — the editor's own context, plus the draft text. */
20806
+ var NcTemplatePreviewInputSchema = object({
20807
+ context: object({
20808
+ family: NcTemplateFamilySchema,
20809
+ field: NcTemplateFieldSchema,
20810
+ kind: NcRuleKindSchema.optional(),
20811
+ delivery: NcDeliverySchema.optional(),
20812
+ systemEventKinds: array(NcSystemEventKindSchema).optional()
20813
+ }),
20814
+ template: object({
20815
+ title: string().max(500).optional(),
20816
+ body: string().max(2e3).optional()
20817
+ })
20818
+ });
20819
+ var NcTemplatePreviewSchema = object({
20820
+ title: string().nullable(),
20821
+ body: string().nullable(),
20822
+ /** Names the template uses that this context never fills — they render empty. */
20823
+ unknown: array(string())
20824
+ });
20665
20825
  var NcConditionDescriptorSchema = object({
20666
20826
  /** Field id inside `NcConditions` (or `'schedule'` for the rule-level group). */
20667
20827
  id: string(),
@@ -20927,8 +21087,17 @@ object({
20927
21087
  lastAt: number()
20928
21088
  });
20929
21089
  /**
20930
- * The three durations the panel's state machine runs on, plus who hears about
20931
- * an arm.
21090
+ * The per-mode lists, at most one per mode. Two lists for one mode have no
21091
+ * meaning a reader could agree on (union? the last one?), so they are refused
21092
+ * at the boundary rather than interpreted.
21093
+ */
21094
+ var NcAlarmNonBlockingListSchema = array(object({
21095
+ mode: AlarmArmModeSchema,
21096
+ deviceIds: array(number().int()).max(200)
21097
+ })).max(8).refine((lists) => new Set(lists.map((l) => l.mode)).size === lists.length, { message: "nonBlocking: at most one list per arm mode" });
21098
+ /**
21099
+ * The three durations the panel's state machine runs on, who hears about its
21100
+ * transitions, and which openings each mode tolerates.
20932
21101
  *
20933
21102
  * They live on the NOTIFICATION-RULES cap, not on `alarm-panel`, on purpose:
20934
21103
  * `alarm-panel` is `deviceNative` and its other provider mirrors somebody
@@ -20947,21 +21116,39 @@ var NcAlarmSettingsSchema = object({
20947
21116
  * existed, and therefore what an untouched install keeps doing.
20948
21117
  */
20949
21118
  triggeredDurationSec: number().int().min(0).max(3600),
20950
- /** Send a notification when a mode takes effect. */
20951
- announceArm: boolean(),
20952
21119
  /**
20953
- * Where that notification goes. Target ids from `notification-output`.
21120
+ * Deprecated by D630 — every transition is announced. Parsed, never read;
21121
+ * remove after one release. Optional so an older admin's patch (which still
21122
+ * sends it) parses rather than failing the whole save.
21123
+ */
21124
+ announceArm: boolean().optional(),
21125
+ /**
21126
+ * Target ids from `notification-output`: every alarm transition goes here.
20954
21127
  *
20955
- * Explicit rather than "everyone": an arm announcement is a household
21128
+ * Explicit rather than "everyone": an alarm announcement is a household
20956
21129
  * message, and broadcasting it to every configured endpoint (including a
20957
21130
  * webhook wired to something else) is not a default anybody would choose.
20958
- * Empty with `announceArm: true` sends nothing, and the server logs that —
20959
- * silence must be attributable.
21131
+ * Empty sends nothing, and the server logs that — silence must be
21132
+ * attributable.
21133
+ */
21134
+ announceTargets: array(string().min(1)).max(16),
21135
+ /**
21136
+ * Per arm mode, the covered devices whose being OPEN does not refuse the
21137
+ * arm (a window left ajar for the cat under `home`, say). At most one entry
21138
+ * per mode is meaningful. Defaulted to `[]` so a blob stored before D630
21139
+ * parses as "every opening blocks" — the behaviour it was written under.
20960
21140
  */
20961
- announceTargets: array(string().min(1)).max(16)
21141
+ nonBlocking: NcAlarmNonBlockingListSchema.default([])
20962
21142
  });
20963
- /** Every field optional — a tab edits one control at a time. */
20964
- var NcAlarmSettingsPatchSchema = NcAlarmSettingsSchema.partial();
21143
+ /**
21144
+ * Every field optional — a tab edits one control at a time.
21145
+ *
21146
+ * `nonBlocking` is re-declared WITHOUT its default: `.partial()` does not
21147
+ * remove an inner `.default()`, so a patch that never named the field would
21148
+ * parse to `nonBlocking: []` and wipe every mode's list on an unrelated edit
21149
+ * (the same trap {@link NcRulePatchSchema} documents).
21150
+ */
21151
+ var NcAlarmSettingsPatchSchema = NcAlarmSettingsSchema.extend({ nonBlocking: NcAlarmNonBlockingListSchema }).partial();
20965
21152
  /**
20966
21153
  * What one arm mode actually arms, DERIVED from the enabled rules gated on it.
20967
21154
  * Never authored, never stored — see `alarm-mode-coverage.ts` for why a stored
@@ -21006,6 +21193,36 @@ var NcAlarmModeCoverageSchema = object({
21006
21193
  */
21007
21194
  skippedDevices: array(NcAlarmSkippedDeviceSchema).default([])
21008
21195
  });
21196
+ /** One covered device that reads open, and whether that refuses the arm. */
21197
+ var NcAlarmOpeningSchema = object({
21198
+ deviceId: number().int(),
21199
+ name: string(),
21200
+ /** The device's state word as read (`open`, `unlocked`, …). */
21201
+ state: string(),
21202
+ /** False when the mode's non-blocking list names this device. */
21203
+ blocking: boolean()
21204
+ });
21205
+ /** A device the panel is currently ignoring, and which side of it it is on. */
21206
+ var NcAlarmExclusionViewSchema = object({
21207
+ deviceId: number().int(),
21208
+ name: string(),
21209
+ phase: _enum(["open", "closed"])
21210
+ });
21211
+ /** One mode's openings: devices read open, plus those whose state is unknown. */
21212
+ var NcAlarmModeOpeningsSchema = object({
21213
+ mode: AlarmArmModeSchema,
21214
+ devices: array(NcAlarmOpeningSchema),
21215
+ /** Covered devices whose state could not be read (D49): neither open nor closed. */
21216
+ unknown: array(number().int())
21217
+ });
21218
+ var NcAlarmLiveSchema = object({
21219
+ state: AlarmStateSchema,
21220
+ /** The mode being armed into (exit delay) or held; null when disarmed. */
21221
+ targetMode: AlarmArmModeSchema.nullable(),
21222
+ availableModes: array(AlarmArmModeSchema),
21223
+ openings: array(NcAlarmModeOpeningsSchema),
21224
+ exclusions: array(NcAlarmExclusionViewSchema)
21225
+ });
21009
21226
  var NcAlarmConfigSchema = object({
21010
21227
  /**
21011
21228
  * The panel's device id, or null when this install has no panel (the ensure
@@ -21015,7 +21232,13 @@ var NcAlarmConfigSchema = object({
21015
21232
  */
21016
21233
  deviceId: number().int().nullable(),
21017
21234
  settings: NcAlarmSettingsSchema,
21018
- coverage: array(NcAlarmModeCoverageSchema)
21235
+ coverage: array(NcAlarmModeCoverageSchema),
21236
+ /**
21237
+ * The panel as it is NOW — state, what each mode would find open, what is
21238
+ * excluded. Optional: a panel-less hub answers without it, and a client
21239
+ * must not read its absence as "nothing is open".
21240
+ */
21241
+ live: NcAlarmLiveSchema.optional()
21019
21242
  });
21020
21243
  /**
21021
21244
  * ONE rule's demand on ONE camera's clip ring.
@@ -21063,10 +21286,7 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
21063
21286
  kind: "mutation",
21064
21287
  auth: "admin",
21065
21288
  caller: "required"
21066
- }), method(object({
21067
- ruleId: string(),
21068
- patch: NcRulePatchSchema
21069
- }), object({ rule: NcRuleSchema }), {
21289
+ }), method(NcRuleUpdateInputSchema, object({ rule: NcRuleSchema }), {
21070
21290
  kind: "mutation",
21071
21291
  auth: "admin",
21072
21292
  caller: "required"
@@ -21094,7 +21314,10 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
21094
21314
  }), method(object({}), object({
21095
21315
  catalog: array(NcConditionDescriptorSchema),
21096
21316
  taxonomy: NcTaxonomySchema.optional()
21097
- })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }), method(object({ artifactId: string().min(1) }), object({ url: string().nullable() }), { auth: "admin" }), method(object({}), object({ snoozes: array(NcSnoozeSchema) }), { caller: "required" }), method(object({ snooze: NcSnoozeInputSchema }), object({ snooze: NcSnoozeSchema }), {
21317
+ })), method(object({}), object({ vars: array(NcTemplateVarDescriptorSchema) })), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
21318
+ kind: "mutation",
21319
+ access: "view"
21320
+ }), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }), method(object({ artifactId: string().min(1) }), object({ url: string().nullable() }), { auth: "admin" }), method(object({}), object({ snoozes: array(NcSnoozeSchema) }), { caller: "required" }), method(object({ snooze: NcSnoozeInputSchema }), object({ snooze: NcSnoozeSchema }), {
21098
21321
  kind: "mutation",
21099
21322
  caller: "required"
21100
21323
  }), method(object({ snoozeId: string() }), object({ success: literal(true) }), {
@@ -25903,6 +26126,7 @@ var CAP_TO_KIND = {
25903
26126
  "enum-sensor": "enum-sensor",
25904
26127
  "event-emitter": "device-event",
25905
26128
  "lock-control": "lock",
26129
+ cover: "cover",
25906
26130
  switch: "switch",
25907
26131
  button: "button",
25908
26132
  doorbell: "doorbell"
@@ -26463,41 +26687,65 @@ var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object(
26463
26687
  method(object({
26464
26688
  location: StorageLocationRefSchema,
26465
26689
  relativePath: string()
26466
- }), string()), method(object({
26690
+ }), string(), { auth: "admin" }), method(object({
26467
26691
  location: StorageLocationRefSchema,
26468
26692
  relativePath: string(),
26469
26693
  data: _instanceof(Uint8Array)
26470
- }), _void(), { kind: "mutation" }), method(object({
26694
+ }), _void(), {
26695
+ kind: "mutation",
26696
+ auth: "admin"
26697
+ }), method(object({
26471
26698
  location: StorageLocationRefSchema,
26472
26699
  relativePath: string()
26473
- }), _instanceof(Uint8Array)), method(object({
26700
+ }), _instanceof(Uint8Array), { auth: "admin" }), method(object({
26474
26701
  location: StorageLocationRefSchema,
26475
26702
  relativePath: string()
26476
- }), boolean()), method(object({
26703
+ }), boolean(), { auth: "admin" }), method(object({
26477
26704
  location: StorageLocationRefSchema,
26478
26705
  prefix: string().optional()
26479
- }), array(string()).readonly()), method(object({
26706
+ }), array(string()).readonly(), { auth: "admin" }), method(object({
26480
26707
  location: StorageLocationRefSchema,
26481
26708
  relativePath: string()
26482
- }), _void(), { kind: "mutation" }), method(object({ location: StorageLocationRefSchema }), number().nullable()), method(object({
26709
+ }), _void(), {
26710
+ kind: "mutation",
26711
+ auth: "admin"
26712
+ }), method(object({ location: StorageLocationRefSchema }), number().nullable()), method(object({
26483
26713
  location: StorageLocationRefSchema,
26484
26714
  relativePath: string(),
26485
26715
  sizeBytes: number().optional()
26486
- }), object({ uploadId: string() }), { kind: "mutation" }), method(object({
26716
+ }), object({ uploadId: string() }), {
26717
+ kind: "mutation",
26718
+ auth: "admin"
26719
+ }), method(object({
26487
26720
  uploadId: string(),
26488
26721
  offset: number(),
26489
26722
  data: _instanceof(Uint8Array)
26490
- }), _void(), { kind: "mutation" }), method(object({ uploadId: string() }), _void(), { kind: "mutation" }), method(object({ uploadId: string() }), _void(), { kind: "mutation" }), method(object({
26723
+ }), _void(), {
26724
+ kind: "mutation",
26725
+ auth: "admin"
26726
+ }), method(object({ uploadId: string() }), _void(), {
26727
+ kind: "mutation",
26728
+ auth: "admin"
26729
+ }), method(object({ uploadId: string() }), _void(), {
26730
+ kind: "mutation",
26731
+ auth: "admin"
26732
+ }), method(object({
26491
26733
  location: StorageLocationRefSchema,
26492
26734
  relativePath: string()
26493
26735
  }), object({
26494
26736
  downloadId: string(),
26495
26737
  sizeBytes: number()
26496
- }), { kind: "mutation" }), method(object({
26738
+ }), {
26739
+ kind: "mutation",
26740
+ auth: "admin"
26741
+ }), method(object({
26497
26742
  downloadId: string(),
26498
26743
  offset: number(),
26499
26744
  length: number()
26500
- }), _instanceof(Uint8Array)), method(object({ downloadId: string() }), _void(), { kind: "mutation" }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
26745
+ }), _instanceof(Uint8Array), { auth: "admin" }), method(object({ downloadId: string() }), _void(), {
26746
+ kind: "mutation",
26747
+ auth: "admin"
26748
+ }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
26501
26749
  createdAt: true,
26502
26750
  updatedAt: true
26503
26751
  }), StorageLocationSchema, {
@@ -26762,6 +27010,58 @@ method(_void(), ProviderInfoSchema, { auth: "admin" }), method(object({ config:
26762
27010
  kind: "mutation",
26763
27011
  auth: "admin"
26764
27012
  });
27013
+ /**
27014
+ * The signals a device can emit to WAKE its own stream.
27015
+ *
27016
+ * A camera whose stream is built on demand sleeps until something asks for it,
27017
+ * and "something" cannot be a consumer that is merely attached — a Frigate-style
27018
+ * puller holds a session open for ever, and treating that as demand would keep
27019
+ * a battery camera awake for ever, which is the whole thing the battery is for
27020
+ * (D173). So the wake has to come from the CAMERA: an event it noticed by
27021
+ * itself, with no stream running.
27022
+ *
27023
+ * ## The vocabulary is the PROVIDER'S, not ours
27024
+ *
27025
+ * Like `consumables`, this cap declares no vocabulary of its own. A provider
27026
+ * names each signal with a `code` it chooses and a `label` an operator reads.
27027
+ * Reolink offers motion and camera-native detection; another provider may offer
27028
+ * a tamper, a doorbell press, a PIR, or something no camera in this fleet has
27029
+ * yet. A fixed enum here would mean every new signal is a framework release.
27030
+ *
27031
+ * It is deliberately NOT derived from the caps a device already binds. Whether
27032
+ * a camera CAN push firmware motion is expressed by `motionSources` containing
27033
+ * `'onboard'`, and whether it does AI on-camera by the `native-object-detection`
27034
+ * binding — but both answer "what drives the detection pipeline", which is a
27035
+ * different question from "what may wake a sleeping stream". A camera can do
27036
+ * the first and not be trusted with the second, and the operator picks per
27037
+ * camera. Two questions, two authorities.
27038
+ *
27039
+ * ## Availability is not permission
27040
+ *
27041
+ * `listSignals` says what the device CAN emit. Whether a given signal actually
27042
+ * wakes the stream is the operator's per-camera choice, held by the broker
27043
+ * alongside the cooldown — see the stream-broker cap's wake settings. A
27044
+ * provider declaring a signal is not a provider enabling it.
27045
+ */
27046
+ /** One signal a device can emit. */
27047
+ var StreamSignalSchema = object({
27048
+ /** Stable id chosen by the provider, e.g. `'motion'`, `'person'`, `'tamper'`. */
27049
+ code: string().min(1),
27050
+ /** What an operator reads in the picker. The provider's own wording. */
27051
+ label: string().min(1),
27052
+ /**
27053
+ * Whether the provider recommends this signal ON when a camera is first set
27054
+ * up. A provider knows which of its signals are cheap and reliable; an
27055
+ * operator should not have to discover that by trial. Reolink recommends
27056
+ * both of its own.
27057
+ */
27058
+ recommended: boolean()
27059
+ });
27060
+ object({
27061
+ signals: array(StreamSignalSchema),
27062
+ lastFetchedAt: number()
27063
+ });
27064
+ Object.values(DeviceType), method(_void(), array(StreamSignalSchema).readonly());
26765
27065
  /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
26766
27066
  var ProfileSettingsSchemaBridge = unknown().nullable();
26767
27067
  var ProfileSettingsBagSchema = record(string(), unknown());
@@ -27745,25 +28045,44 @@ var ClipStreamDialSchema = object({
27745
28045
  audioReason: ClipStreamAudioReasonSchema.optional()
27746
28046
  });
27747
28047
  /**
27748
- * The playback rates a clip can be DELIVERED at, ascending, always with `1`.
28048
+ * The playback rates the broker can pace over media it HOLDS, ascending,
28049
+ * always with `1` — and the WIDEST ladder there is. It is the domain the
28050
+ * broker's `clampPlaybackRate` is derived from, at both ends.
27749
28051
  *
27750
28052
  * These are the BROKER's: it re-paces frames it has already demuxed — the
27751
28053
  * `MonotonicClock` divides source elapsed time by the factor and the pacer
27752
- * pushes that much faster — so the domain is the recorded one, {0} ∪ [0.25, 4],
27753
- * and this is the discrete ladder drawn from it. `1.5` is in it because a
27754
- * re-pacer has no reason to refuse it.
27755
- *
27756
- * `8` and `16` are deliberately NOT here. They are the CAMERA's own
27757
- * `<playSpeed>` (Reolink cmd-5 replay), a different mechanism, still unwired
27758
- * (D597, D600) — a rate change there costs a new dial and a new stream, and
27759
- * the camera's 8x would need a resample the clip audio path has no decoder
27760
- * for. Offering them today accepts a rate and delivers 1x, which is the whole
27761
- * defect this list exists to end (D612). When `playSpeed` IS wired its rates
27762
- * join THIS array — a second list elsewhere is the second authority D62
27763
- * forbids.
27764
- *
27765
- * Every entry must survive the broker's `clampPlaybackRate` unchanged: a set
27766
- * that offers what the clamp then moves is the same lie one step later.
28054
+ * pushes that much faster. `1.5` is in it because a re-pacer has no reason to
28055
+ * refuse it.
28056
+ *
28057
+ * **`8` is here since D621, and it is the pacer's, not a camera's.** D620
28058
+ * measured the thing that was assumed for a year and was never true: a
28059
+ * Reolink `<playSpeed>` does not time-compress anything. At 1, 2, 4 and 8 the
28060
+ * transfer is byte-identical — same 947 472 bytes, same 594 access units,
28061
+ * same 39.855 s presentation span, same 624 AAC frames — and only the wall
28062
+ * clock moves. It buys SUPPLY, never speed. So there was never a second
28063
+ * mechanism to wire for 8×: there is one, this one, and what it needs is
28064
+ * media in hand and a clamp that does not move the number.
28065
+ *
28066
+ * **`16` is deliberately NOT here, and will not join by widening this array.**
28067
+ * The camera's `playSpeed 16` is not a rate at all but an I-frame-only MODE —
28068
+ * measured on 592: 10 access units for a whole 36.3 s sub clip, 20 for the
28069
+ * main twin at a 2 025 ms median spacing, and no audio track. That is
28070
+ * different CONTENT, a stream of its own, and the honest broker-side twin of
28071
+ * it would be a pacer that DROPS whole GOPs rather than one that pushes 16×
28072
+ * the bitrate at a live WebRTC track. Neither exists. A `16` in this array
28073
+ * today would be a rate accepted and delivered at 8 — D612's defect, which
28074
+ * this list exists to end, one number further along.
28075
+ *
28076
+ * Every entry must survive the broker's `clampPlaybackRate` unchanged. Since
28077
+ * D621 that is structural rather than a discipline: the clamp reads this
28078
+ * array's own ends. What still has to be proved behaviourally — and is, in
28079
+ * the broker's `clip-stream-feeder.spec` — is that the PACER really empties a
28080
+ * clip `rate` times faster, because a clamp agreeing with a ladder is a
28081
+ * constant compared against itself.
28082
+ *
28083
+ * Narrower ladders exist for transports whose SUPPLY is bounded; they are
28084
+ * subsets of this one ({@link CLIP_STREAM_SOURCE_RATES},
28085
+ * {@link CLIP_REALTIME_SOURCE_RATES}).
27767
28086
  */
27768
28087
  var CLIP_BROKER_PACED_RATES = [
27769
28088
  .25,
@@ -27771,7 +28090,8 @@ var CLIP_BROKER_PACED_RATES = [
27771
28090
  1,
27772
28091
  1.5,
27773
28092
  2,
27774
- 4
28093
+ 4,
28094
+ 8
27775
28095
  ];
27776
28096
  /**
27777
28097
  * What a surface may DRAW for this provider's clips — the answer to
@@ -28297,7 +28617,67 @@ var WebrtcLadderDecisionSchema = object({
28297
28617
  serverSending: boolean().nullable(),
28298
28618
  /** Epoch ms. The overlay prints its age; a decision from 40 minutes ago
28299
28619
  * must not read like one from this second. */
28300
- at: number()
28620
+ at: number(),
28621
+ /** The broker's own cause when it is finer than `cause` can carry, e.g.
28622
+ * `trial-revert` (reported as `stall`/`loss` for older consumers). Optional:
28623
+ * an older server strips it instead of rejecting the answer. */
28624
+ causeDetail: string().optional()
28625
+ });
28626
+ var WebrtcClimbHoldSchema = object({
28627
+ /** `stall-building` — the client receives ≪ what is sent; `delivery` — the
28628
+ * current rung is not landing whole; `no-evidence` — no receiver feedback;
28629
+ * `warming` — healthy, accumulating the streak; `cooldown` — too soon after
28630
+ * a change; `capacity` — a MEASURED probe below the next rung's bar;
28631
+ * `try-unclean` — no capacity evidence, and the path is not clean enough to
28632
+ * try; `try-backoff` — a try reverted, waiting out its backoff; `trial` —
28633
+ * this rung IS a try, reverting if it does not land; `suspended` —
28634
+ * recorded playback on the track. */
28635
+ code: string(),
28636
+ /** The rung a climb would go to. */
28637
+ nextTier: CamProfileSchema,
28638
+ /** Bytes received ÷ bytes sent over the delivery window (≥ one GOP). */
28639
+ deliveryRatio: number().nullable(),
28640
+ deliveryWindowMs: number().nullable(),
28641
+ /** The ratio a climb requires. */
28642
+ climbDeliveryRatio: number(),
28643
+ /** The receiver's bandwidth estimate (REMB), kbps. Capped by what is sent. */
28644
+ capacityKbps: number().nullable(),
28645
+ /** The viewer's downlink probe for this session, kbps. */
28646
+ probeKbps: number().nullable(),
28647
+ /** The next rung's measured bitrate, kbps. */
28648
+ nextTierKbps: number().nullable(),
28649
+ capacityMargin: number(),
28650
+ /** The capacity a climb to `nextTier` needs, kbps (bitrate × margin, or the
28651
+ * client ladder threshold when the rung has measured nothing). */
28652
+ neededKbps: number().nullable(),
28653
+ /** For `try-unclean`: which clean-path condition failed (a `WebrtcTryBlocker`,
28654
+ * carried as a string for the same reason as `code`). */
28655
+ tryBlockedBy: string().nullable(),
28656
+ /** The delivery ratio and the loss ratio a try requires / must stay under.
28657
+ * Optional, like every field added to this schema after it first shipped:
28658
+ * the router validates output, so a REQUIRED new field makes a newer server
28659
+ * reject an older broker's whole answer (review m1). */
28660
+ tryDeliveryRatio: number().optional(),
28661
+ tryMaxLossRatio: number().optional(),
28662
+ /** For `try-backoff`: time until the next try, ms. */
28663
+ retryInMs: number().nullable(),
28664
+ /** The last try that reverted in this viewer session. */
28665
+ lastRevert: object({
28666
+ tier: CamProfileSchema,
28667
+ deliveryRatio: number().nullable(),
28668
+ stalled: boolean(),
28669
+ at: number()
28670
+ }).nullable(),
28671
+ /** For `trial`: how long this rung has been on trial, and for how long it must hold. */
28672
+ trialForMs: number().nullable(),
28673
+ trialMs: number().nullable(),
28674
+ healthyForMs: number().nullable(),
28675
+ requiredHealthyMs: number().nullable(),
28676
+ /** Epoch ms this hold's code began. */
28677
+ since: number(),
28678
+ /** The same one-line account the broker logs, e.g.
28679
+ * `held: delivery 0.97 over 4.0s ok, REMB 480 kbps < mid 1800 kbps × 1.2`. */
28680
+ summary: string()
28301
28681
  });
28302
28682
  /** Per-tier count of MEASURED downgrades this viewer session has suffered. */
28303
28683
  var WebrtcTierFailureSchema = object({
@@ -28361,7 +28741,12 @@ var WebrtcSessionDebugSchema = object({
28361
28741
  tierFailures: array(WebrtcTierFailureSchema),
28362
28742
  /** Successful promotions in this viewer session (drives the progressive
28363
28743
  * healthy timer). Null when no ladder is running. */
28364
- upgradeCount: number().nullable()
28744
+ upgradeCount: number().nullable(),
28745
+ /** Why the ladder is not climbing right now. Null when no ladder runs, it
28746
+ * is at the top rung, or it just moved. Optional because this output is
28747
+ * validated by the router: a server carrying this schema must still accept
28748
+ * the answer of a stream-broker deployed before the field existed. */
28749
+ climbHold: WebrtcClimbHoldSchema.nullable().optional()
28365
28750
  });
28366
28751
  /**
28367
28752
  * WebRTC session — device-scoped facade over the system `webrtc` cap.
@@ -29442,6 +29827,143 @@ DeviceType.Camera, method(object({ deviceId: number() }), CameraCredentialsSchem
29442
29827
  kind: "query",
29443
29828
  auth: "admin"
29444
29829
  });
29830
+ /**
29831
+ * camera-grid-layout — the geometry of a COMPOSITE camera, on the camera's own
29832
+ * page.
29833
+ *
29834
+ * ## Why this is a capability and not an addon settings schema
29835
+ *
29836
+ * It was one, and it did not render. The addon declared the editor as a
29837
+ * `type: 'widget'` field inside its own `deviceSettingsSchema()`; the hub
29838
+ * returned that section correctly and `ConfigFormField` renders `type:'widget'`
29839
+ * perfectly well — and nothing ever asked camera-grid for it. The Cluster →
29840
+ * Pipeline → Device Overrides page interrogates a HAND-WRITTEN list of four
29841
+ * addons (`PIPELINE_CLUSTER_DEVICE_ADDONS`), whose own comment says an addon
29842
+ * not on it "falls off silently".
29843
+ *
29844
+ * Adding a fifth name to that list would have been the wrong fix twice over:
29845
+ * that page is per-camera DETECTION tuning, and a grid's geometry belongs
29846
+ * beside PTZ and motion zones on the camera itself. The device page is
29847
+ * BINDING-driven (D12), so the way in is a capability bound to the device —
29848
+ * and this cap carries its section the way `recording` does, by RETURNING it
29849
+ * from `getDeviceSettingsContribution`.
29850
+ *
29851
+ * Seven other widgets are still declared the other way, through a
29852
+ * `deviceConfig.ui` block the framework derives a section from. That route
29853
+ * gives the addon no say in where its own panel lands and no way to decline
29854
+ * for a device the panel does not suit, which is why this one does not use it.
29855
+ *
29856
+ * ## Why one addon may implement it
29857
+ *
29858
+ * It is a device-scoped NATIVE cap, registered by the grid camera device
29859
+ * itself. Nothing else declares a composite camera, so nothing else has a
29860
+ * layout — and the device-scoped route means the widget asks THE camera, not
29861
+ * "the camera-grid addon", which is what let the old custom-action pair be
29862
+ * reached only by a caller that already knew the addon id.
29863
+ *
29864
+ * ## The tab
29865
+ *
29866
+ * `streaming`, not a top-tab of its own. A grid's geometry IS what its stream
29867
+ * is, so the Streaming tab is where it belongs; a `grid` top-tab would need an
29868
+ * entry in `WELL_KNOWN_TAB_MAP` or the device page renders the raw id as the
29869
+ * label (measured on the robot camera, 2026-09-06 — a tab called "navigation"
29870
+ * next to "PTZ").
29871
+ */
29872
+ /** A rectangle in normalized [0,1] coordinates of whatever contains it. */
29873
+ var GridNormalizedRectSchema = object({
29874
+ x: number().min(0).max(1),
29875
+ y: number().min(0).max(1),
29876
+ width: number().gt(0).max(1),
29877
+ height: number().gt(0).max(1)
29878
+ });
29879
+ /**
29880
+ * One source camera, the part of its picture taken, and where that part lands.
29881
+ *
29882
+ * Both rectangles are NORMALIZED (D519): a source camera can change resolution
29883
+ * — a profile switch, a firmware update, a substream that comes back different
29884
+ * — and a stored PIXEL rectangle would quietly start cutting the wrong region,
29885
+ * which is the class of bug nobody files.
29886
+ */
29887
+ var GridLayoutCellSchema = object({
29888
+ deviceId: number().int().positive(),
29889
+ /** The part of the SOURCE taken, normalized against the source. */
29890
+ source: GridNormalizedRectSchema,
29891
+ /** Where it lands, normalized against the CANVAS. */
29892
+ cell: GridNormalizedRectSchema
29893
+ });
29894
+ /**
29895
+ * Which profiles this grid can actually compose, and why not.
29896
+ *
29897
+ * A grid's `high` composes its sources' `high` and its `low` their `low`, so a
29898
+ * profile is on offer only when EVERY source can serve it. The refusal NAMES
29899
+ * the sources, because "this grid has no low" is not a finding — "615 has no
29900
+ * low" is, and it is the one an operator can act on.
29901
+ */
29902
+ var GridProfileOfferSchema = object({
29903
+ profile: _enum([
29904
+ "high",
29905
+ "mid",
29906
+ "low"
29907
+ ]),
29908
+ offered: boolean(),
29909
+ /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
29910
+ missingSources: array(number().int().positive()),
29911
+ /**
29912
+ * The canvas this profile composes onto, `WxH`, or empty when it is not
29913
+ * offered. DERIVED from the cells and the sources' own size at this profile —
29914
+ * it is reported because nothing else in the system would ever say what the
29915
+ * grid came out as, and because it is the number an operator would otherwise
29916
+ * expect to type.
29917
+ */
29918
+ canvas: string(),
29919
+ /**
29920
+ * Whether this profile is PUBLISHED, of the ones the grid could serve.
29921
+ *
29922
+ * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
29923
+ * a 4K canvas built from 4K decodes — something to opt into, not something a
29924
+ * viewer's adaptive should be handed by climbing to the top rung it can see.
29925
+ * Default is `mid` + `low`.
29926
+ */
29927
+ published: boolean()
29928
+ });
29929
+ var GridLayoutViewSchema = object({
29930
+ /** The persisted grid row this camera was declared from. */
29931
+ instanceId: string(),
29932
+ deviceId: number().int().nonnegative(),
29933
+ name: string(),
29934
+ /**
29935
+ * NO canvas size. A grid's resolution is not authored: each profile derives
29936
+ * its own from the cells and its sources' dimensions. The two numbers that
29937
+ * used to be here were a text field that silently decided both how much the
29938
+ * composite cost and how sharp it was — see `profiles[].canvas` for what it
29939
+ * came out as.
29940
+ */
29941
+ fps: number().int(),
29942
+ cells: array(GridLayoutCellSchema),
29943
+ /** What the catalog will publish, and what it refuses to. Read-only. */
29944
+ profiles: array(GridProfileOfferSchema)
29945
+ });
29946
+ var GridLayoutPatchSchema = object({
29947
+ deviceId: number().int().nonnegative(),
29948
+ name: string().min(1).max(160).optional(),
29949
+ fps: number().int().min(1).max(60).optional(),
29950
+ /** Which profiles to publish. See `GridProfileOffer.published`. */
29951
+ publishedProfiles: array(_enum([
29952
+ "high",
29953
+ "mid",
29954
+ "low"
29955
+ ])).max(3).optional(),
29956
+ /**
29957
+ * The whole cell list at once. A per-cell patch would need an ordering the
29958
+ * editor does not have, and a half-applied layout is a picture nobody asked
29959
+ * for.
29960
+ */
29961
+ cells: array(GridLayoutCellSchema).max(16)
29962
+ });
29963
+ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), GridLayoutViewSchema.nullable(), { auth: "admin" }), method(GridLayoutPatchSchema, GridLayoutViewSchema, {
29964
+ kind: "mutation",
29965
+ auth: "admin"
29966
+ });
29445
29967
  object({
29446
29968
  detected: boolean(),
29447
29969
  /** Ms epoch of the last transition. 0 if never observed. */
@@ -29915,307 +30437,6 @@ DeviceType.Camera, method(object({ deviceId: number() }), DayNightOptionsSchema)
29915
30437
  auth: "admin"
29916
30438
  });
29917
30439
  /**
29918
- * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
29919
- * writes to the CAMERA's own card, on the camera's own schedule.
29920
- *
29921
- * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
29922
- * footage ledger, our storage locations, our retention. This one has a
29923
- * different authority — the camera's firmware — and per D62 it stores
29924
- * nothing of its own. Every value here is read from the camera and every
29925
- * write goes back to the camera; there is no CamStack-side mirror that
29926
- * could disagree with the device.
29927
- *
29928
- * ## One shape, two firmwares
29929
- *
29930
- * Measured 2026-09-22 against the live fleet:
29931
- *
29932
- * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
29933
- * | --- | --- | --- |
29934
- * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
29935
- * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
29936
- * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
29937
- * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
29938
- * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
29939
- * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
29940
- * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
29941
- * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
29942
- *
29943
- * The two schedule models look different and are the same thing in
29944
- * different coordinates: both answer "for this trigger, during which
29945
- * weekly windows does the camera record". {@link RecordWindow} is that
29946
- * question in one shape — Hikvision's ranges map straight onto it,
29947
- * Reolink's mask expands into hour-aligned windows.
29948
- *
29949
- * ## Union, not intersection
29950
- *
29951
- * **The same fields exist on every camera.** What differs per device is
29952
- * which VALUES that device accepts, and that is what {@link
29953
- * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
29954
- * per field plus the schedule's own limits. A control a camera cannot
29955
- * honour is rendered DISABLED WITH ITS REASON, never missing and never
29956
- * dead: disabled must not look like broken.
29957
- *
29958
- * ## Refusal by name
29959
- *
29960
- * A write a camera cannot honour is refused with a sentence the operator
29961
- * can read — never accepted and dropped. Both providers refuse through
29962
- * {@link describeOnboardRefusal}, so the vocabulary is one function and
29963
- * one test, not two hand-written vendor opinions.
29964
- *
29965
- * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
29966
- * `getOptions` advertises per-camera availability, `getStatus` (auto-
29967
- * injected from `status`) reports the live values, and a single
29968
- * `setSettings` mutation applies a partial change. No hand-written
29969
- * settings-contribution methods.
29970
- */
29971
- /**
29972
- * What makes the camera start recording during a window.
29973
- *
29974
- * The union of both vendors' vocabularies. `continuous` is Hikvision's
29975
- * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
29976
- * object-class triggers are Reolink-only today and the smart-event ones
29977
- * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
29978
- * firmwares measured — a camera that cannot record on a trigger simply
29979
- * does not list it in `options.schedule.triggers`, and a window naming
29980
- * it is REFUSED, not dropped.
29981
- */
29982
- var RecordTriggerSchema = _enum([
29983
- "continuous",
29984
- "motion",
29985
- "person",
29986
- "vehicle",
29987
- "animal",
29988
- "lineCrossing",
29989
- "intrusion",
29990
- "loitering",
29991
- "alarmInput"
29992
- ]);
29993
- /**
29994
- * One weekly recording window: "on `day`, from `startMinute` to
29995
- * `endMinute`, record on `trigger`".
29996
- *
29997
- * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
29998
- * both firmwares enumerate). Minutes are local camera time since
29999
- * midnight; `endMinute` may be 1440, meaning end of day — that is
30000
- * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
30001
- * collapsing it to 0 would turn a whole-day window into an empty one.
30002
- */
30003
- var RecordWindowSchema = object({
30004
- trigger: RecordTriggerSchema,
30005
- day: number().int().min(0).max(6),
30006
- startMinute: number().int().min(0).max(1439),
30007
- endMinute: number().int().min(1).max(1440)
30008
- });
30009
- /** Status of one physical volume, as the camera itself describes it. */
30010
- var OnboardStorageVolumeSchema = object({
30011
- /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
30012
- id: string(),
30013
- /** The camera's own name for it, when it gives one (`hddName`). */
30014
- label: string().optional(),
30015
- status: _enum([
30016
- "ok",
30017
- "unformatted",
30018
- "error",
30019
- "offline",
30020
- "unknown"
30021
- ]),
30022
- /**
30023
- * Total size in MB, or **null when the camera did not say**.
30024
- *
30025
- * Never 0 for an unreadable value: a measurement that failed is not a
30026
- * measurement (D393), and a card whose size is unknown must not be
30027
- * rendered as a card of size zero.
30028
- */
30029
- capacityMb: number().nullable(),
30030
- /**
30031
- * Free space in MB, or null when unknown.
30032
- *
30033
- * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
30034
- * 1439 both report exactly 11776 MB free — the fixed reserve a looping
30035
- * card converges on once it has wrapped. At loop steady state the
30036
- * number is identical whether the camera recorded yesterday or stopped
30037
- * a month ago.
30038
- */
30039
- freeMb: number().nullable(),
30040
- /** True when the camera reports the volume writable (`property` RW). */
30041
- writable: boolean().optional()
30042
- });
30043
- object({
30044
- storage: discriminatedUnion("kind", [
30045
- object({
30046
- kind: literal("present"),
30047
- volumes: array(OnboardStorageVolumeSchema)
30048
- }),
30049
- object({
30050
- kind: literal("absent"),
30051
- reason: string()
30052
- }),
30053
- object({
30054
- kind: literal("unknown"),
30055
- reason: string()
30056
- })
30057
- ]),
30058
- tracks: array(object({
30059
- id: string(),
30060
- enabled: boolean(),
30061
- isVideo: boolean(),
30062
- /** From the camera's own track description. Null when it does not say. */
30063
- codec: string().nullable(),
30064
- resolution: string().nullable(),
30065
- /** Per-track overwrite flag, where the firmware keeps it per track. */
30066
- overwriteWhenFull: boolean().nullable()
30067
- })),
30068
- /**
30069
- * The track the write path targets — the enabled VIDEO one. Null when
30070
- * no track could be identified, which is itself a refusal reason.
30071
- */
30072
- primaryTrackId: string().nullable(),
30073
- /** Master "record to the card at all" switch. */
30074
- enabled: boolean().nullable(),
30075
- overwriteWhenFull: boolean().nullable(),
30076
- preRecordSec: number().nullable(),
30077
- postRecordSec: number().nullable(),
30078
- /** Length of one recorded file, in minutes. */
30079
- segmentMinutes: number().nullable(),
30080
- /** The primary track's weekly windows, flattened. */
30081
- windows: array(RecordWindowSchema),
30082
- /**
30083
- * How many windows the camera described that CamStack could NOT read —
30084
- * an unrecognised trigger, an unparseable clock, a weekday it does not
30085
- * name.
30086
- *
30087
- * A dropped window is work the reader threw away, and a schedule that
30088
- * silently shows fewer rows than the camera holds is how an operator
30089
- * saves back a schedule shorter than the one they were looking at
30090
- * (D391). Non-zero means the window list is INCOMPLETE and a write
30091
- * that replaces it would delete what was not shown — which is why a
30092
- * provider reporting a non-zero count also reports the schedule as not
30093
- * writable.
30094
- */
30095
- unreadableWindows: number(),
30096
- /**
30097
- * The camera is scheduled to record and has NO usable storage.
30098
- *
30099
- * A first-class fact because it is the fleet's most common silent
30100
- * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
30101
- * to a card that is not there. Neither the schedule nor the storage
30102
- * read says anything wrong on its own; only the pair does.
30103
- */
30104
- recordingToNowhere: boolean(),
30105
- lastFetchedAt: number()
30106
- });
30107
- /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
30108
- var RangeSchema = object({
30109
- min: number(),
30110
- max: number(),
30111
- step: number()
30112
- });
30113
- /**
30114
- * The values a camera actually takes for a numeric field, when they are a SET
30115
- * rather than a range.
30116
- *
30117
- * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
30118
- * (I91DN) on 2026-09-22 by writing each value and reading it back:
30119
- *
30120
- * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
30121
- * camera's "no limit" — `-1` and `4294967295` both land on it);
30122
- * - post-record: `5, 10, 30, 60, 120, 300, 600`.
30123
- *
30124
- * Neither is expressible as a step: the first has a sentinel two billion away
30125
- * from its neighbours, the second doubles and then jumps. A range that tried
30126
- * would forbid values the camera takes AND permit values it silently replaces
30127
- * with 5 — wrong in both directions at once.
30128
- *
30129
- * `sentinel` names the member that is not a duration, so a surface can render
30130
- * "no limit" instead of `2147483647` seconds.
30131
- */
30132
- var AllowedValuesSchema = object({
30133
- values: array(number()).min(1),
30134
- sentinel: object({
30135
- value: number(),
30136
- meaning: _enum(["no-limit", "disabled"])
30137
- }).optional()
30138
- });
30139
- /**
30140
- * Per-field availability on ONE camera.
30141
- *
30142
- * The field exists on every camera — this says whether this one can be
30143
- * read and whether it can be written, and `reason` says why not when
30144
- * either is false. The UI renders the control DISABLED with the reason
30145
- * rather than hiding it, so a limitation is legible instead of looking
30146
- * like a missing feature.
30147
- */
30148
- var OnboardFieldSupportSchema = object({
30149
- readable: boolean(),
30150
- writable: boolean(),
30151
- /** Required whenever `readable` or `writable` is false. */
30152
- reason: string().optional()
30153
- });
30154
- /** What this camera's schedule model can express. */
30155
- var OnboardScheduleSupportSchema = object({
30156
- support: OnboardFieldSupportSchema,
30157
- /**
30158
- * The smallest time step the camera can express, in minutes.
30159
- *
30160
- * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
30161
- * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
30162
- * window whose edges are not a multiple of this is REFUSED rather than
30163
- * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
30164
- * and nothing says so.
30165
- */
30166
- granularityMinutes: number(),
30167
- /** Triggers this camera can record on. A window naming another is refused. */
30168
- triggers: array(RecordTriggerSchema),
30169
- /**
30170
- * False when the camera stores ONE trigger per time range, so two
30171
- * windows overlapping on the same day cannot carry different triggers.
30172
- * True on Reolink, whose mask is per-trigger and independent.
30173
- */
30174
- supportsOverlappingTriggers: boolean()
30175
- });
30176
- var RecordingOnboardOptionsSchema = object({
30177
- enabled: OnboardFieldSupportSchema,
30178
- overwriteWhenFull: OnboardFieldSupportSchema,
30179
- preRecordSec: OnboardFieldSupportSchema,
30180
- preRecordSecRange: RangeSchema.optional(),
30181
- /** Preferred over the range when the camera takes a SET, not a span. */
30182
- preRecordSecAllowed: AllowedValuesSchema.optional(),
30183
- postRecordSec: OnboardFieldSupportSchema,
30184
- postRecordSecRange: RangeSchema.optional(),
30185
- /** Preferred over the range when the camera takes a SET, not a span. */
30186
- postRecordSecAllowed: AllowedValuesSchema.optional(),
30187
- segmentMinutes: OnboardFieldSupportSchema,
30188
- segmentMinutesRange: RangeSchema.optional(),
30189
- /** Preferred over the range when the camera takes a SET, not a span. */
30190
- segmentMinutesAllowed: AllowedValuesSchema.optional(),
30191
- schedule: OnboardScheduleSupportSchema
30192
- });
30193
- /**
30194
- * A partial change. Every field optional.
30195
- *
30196
- * Unlike the other `deviceConfig` caps, a provider here does **NOT**
30197
- * silently ignore a field it cannot support — it refuses, by name,
30198
- * through {@link describeOnboardRefusal}. Silence on a recording setting
30199
- * is the failure D62 exists to prevent: the operator believes the camera
30200
- * is recording the way the form says, and it is not.
30201
- */
30202
- var RecordingOnboardPatchSchema = object({
30203
- enabled: boolean().optional(),
30204
- overwriteWhenFull: boolean().optional(),
30205
- preRecordSec: number().optional(),
30206
- postRecordSec: number().optional(),
30207
- segmentMinutes: number().optional(),
30208
- /** The complete new window set for the primary track — not a delta. */
30209
- windows: array(RecordWindowSchema).optional()
30210
- });
30211
- DeviceType.Camera, method(object({ deviceId: number() }), RecordingOnboardOptionsSchema), method(object({
30212
- deviceId: number(),
30213
- settings: RecordingOnboardPatchSchema
30214
- }), _void(), {
30215
- kind: "mutation",
30216
- auth: "admin"
30217
- });
30218
- /**
30219
30440
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
30220
30441
  * for every device, regardless of provider — the kernel needs a uniform
30221
30442
  * cap-keyed slice for the basic device flags every consumer expects to
@@ -30309,30 +30530,6 @@ object({
30309
30530
  eventCountSinceStart: number()
30310
30531
  });
30311
30532
  DeviceType.EventEmitter;
30312
- var EventItemSchema = object({
30313
- id: string(),
30314
- type: string(),
30315
- timestamp: number(),
30316
- label: string().optional(),
30317
- thumbnailUrl: string().optional(),
30318
- clipUrl: string().optional(),
30319
- metadata: record(string(), unknown()).optional()
30320
- });
30321
- DeviceType.Camera, method(object({
30322
- deviceId: number(),
30323
- from: number().optional(),
30324
- to: number().optional(),
30325
- limit: number().optional()
30326
- }), array(EventItemSchema)), method(object({
30327
- deviceId: number(),
30328
- eventId: string()
30329
- }), object({
30330
- base64: string(),
30331
- contentType: string()
30332
- }).nullable()), method(object({
30333
- deviceId: number(),
30334
- eventId: string()
30335
- }), string().nullable());
30336
30533
  var IdentitySchema = object({
30337
30534
  id: string(),
30338
30535
  name: string(),
@@ -32029,143 +32226,6 @@ DeviceType.Light, DeviceType.Siren, DeviceType.Switch, method(object({
32029
32226
  lastChangedAt: number()
32030
32227
  });
32031
32228
  /**
32032
- * camera-grid-layout — the geometry of a COMPOSITE camera, on the camera's own
32033
- * page.
32034
- *
32035
- * ## Why this is a capability and not an addon settings schema
32036
- *
32037
- * It was one, and it did not render. The addon declared the editor as a
32038
- * `type: 'widget'` field inside its own `deviceSettingsSchema()`; the hub
32039
- * returned that section correctly and `ConfigFormField` renders `type:'widget'`
32040
- * perfectly well — and nothing ever asked camera-grid for it. The Cluster →
32041
- * Pipeline → Device Overrides page interrogates a HAND-WRITTEN list of four
32042
- * addons (`PIPELINE_CLUSTER_DEVICE_ADDONS`), whose own comment says an addon
32043
- * not on it "falls off silently".
32044
- *
32045
- * Adding a fifth name to that list would have been the wrong fix twice over:
32046
- * that page is per-camera DETECTION tuning, and a grid's geometry belongs
32047
- * beside PTZ and motion zones on the camera itself. The device page is
32048
- * BINDING-driven (D12), so the way in is a capability bound to the device —
32049
- * and this cap carries its section the way `recording` does, by RETURNING it
32050
- * from `getDeviceSettingsContribution`.
32051
- *
32052
- * Seven other widgets are still declared the other way, through a
32053
- * `deviceConfig.ui` block the framework derives a section from. That route
32054
- * gives the addon no say in where its own panel lands and no way to decline
32055
- * for a device the panel does not suit, which is why this one does not use it.
32056
- *
32057
- * ## Why one addon may implement it
32058
- *
32059
- * It is a device-scoped NATIVE cap, registered by the grid camera device
32060
- * itself. Nothing else declares a composite camera, so nothing else has a
32061
- * layout — and the device-scoped route means the widget asks THE camera, not
32062
- * "the camera-grid addon", which is what let the old custom-action pair be
32063
- * reached only by a caller that already knew the addon id.
32064
- *
32065
- * ## The tab
32066
- *
32067
- * `streaming`, not a top-tab of its own. A grid's geometry IS what its stream
32068
- * is, so the Streaming tab is where it belongs; a `grid` top-tab would need an
32069
- * entry in `WELL_KNOWN_TAB_MAP` or the device page renders the raw id as the
32070
- * label (measured on the robot camera, 2026-09-06 — a tab called "navigation"
32071
- * next to "PTZ").
32072
- */
32073
- /** A rectangle in normalized [0,1] coordinates of whatever contains it. */
32074
- var GridNormalizedRectSchema = object({
32075
- x: number().min(0).max(1),
32076
- y: number().min(0).max(1),
32077
- width: number().gt(0).max(1),
32078
- height: number().gt(0).max(1)
32079
- });
32080
- /**
32081
- * One source camera, the part of its picture taken, and where that part lands.
32082
- *
32083
- * Both rectangles are NORMALIZED (D519): a source camera can change resolution
32084
- * — a profile switch, a firmware update, a substream that comes back different
32085
- * — and a stored PIXEL rectangle would quietly start cutting the wrong region,
32086
- * which is the class of bug nobody files.
32087
- */
32088
- var GridLayoutCellSchema = object({
32089
- deviceId: number().int().positive(),
32090
- /** The part of the SOURCE taken, normalized against the source. */
32091
- source: GridNormalizedRectSchema,
32092
- /** Where it lands, normalized against the CANVAS. */
32093
- cell: GridNormalizedRectSchema
32094
- });
32095
- /**
32096
- * Which profiles this grid can actually compose, and why not.
32097
- *
32098
- * A grid's `high` composes its sources' `high` and its `low` their `low`, so a
32099
- * profile is on offer only when EVERY source can serve it. The refusal NAMES
32100
- * the sources, because "this grid has no low" is not a finding — "615 has no
32101
- * low" is, and it is the one an operator can act on.
32102
- */
32103
- var GridProfileOfferSchema = object({
32104
- profile: _enum([
32105
- "high",
32106
- "mid",
32107
- "low"
32108
- ]),
32109
- offered: boolean(),
32110
- /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
32111
- missingSources: array(number().int().positive()),
32112
- /**
32113
- * The canvas this profile composes onto, `WxH`, or empty when it is not
32114
- * offered. DERIVED from the cells and the sources' own size at this profile —
32115
- * it is reported because nothing else in the system would ever say what the
32116
- * grid came out as, and because it is the number an operator would otherwise
32117
- * expect to type.
32118
- */
32119
- canvas: string(),
32120
- /**
32121
- * Whether this profile is PUBLISHED, of the ones the grid could serve.
32122
- *
32123
- * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
32124
- * a 4K canvas built from 4K decodes — something to opt into, not something a
32125
- * viewer's adaptive should be handed by climbing to the top rung it can see.
32126
- * Default is `mid` + `low`.
32127
- */
32128
- published: boolean()
32129
- });
32130
- var GridLayoutViewSchema = object({
32131
- /** The persisted grid row this camera was declared from. */
32132
- instanceId: string(),
32133
- deviceId: number().int().nonnegative(),
32134
- name: string(),
32135
- /**
32136
- * NO canvas size. A grid's resolution is not authored: each profile derives
32137
- * its own from the cells and its sources' dimensions. The two numbers that
32138
- * used to be here were a text field that silently decided both how much the
32139
- * composite cost and how sharp it was — see `profiles[].canvas` for what it
32140
- * came out as.
32141
- */
32142
- fps: number().int(),
32143
- cells: array(GridLayoutCellSchema),
32144
- /** What the catalog will publish, and what it refuses to. Read-only. */
32145
- profiles: array(GridProfileOfferSchema)
32146
- });
32147
- var GridLayoutPatchSchema = object({
32148
- deviceId: number().int().nonnegative(),
32149
- name: string().min(1).max(160).optional(),
32150
- fps: number().int().min(1).max(60).optional(),
32151
- /** Which profiles to publish. See `GridProfileOffer.published`. */
32152
- publishedProfiles: array(_enum([
32153
- "high",
32154
- "mid",
32155
- "low"
32156
- ])).max(3).optional(),
32157
- /**
32158
- * The whole cell list at once. A per-cell patch would need an ordering the
32159
- * editor does not have, and a half-applied layout is a picture nobody asked
32160
- * for.
32161
- */
32162
- cells: array(GridLayoutCellSchema).max(16)
32163
- });
32164
- DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), GridLayoutViewSchema.nullable(), { auth: "admin" }), method(GridLayoutPatchSchema, GridLayoutViewSchema, {
32165
- kind: "mutation",
32166
- auth: "admin"
32167
- });
32168
- /**
32169
32229
  * Motion-zones share the same MaskShape vocabulary as privacy-mask — the
32170
32230
  * on-camera motion-detection mask is a single `grid` region (a row-major
32171
32231
  * boolean cell lattice the camera's onboard VMD evaluates). Composing it as
@@ -32567,6 +32627,11 @@ var ClientNetworkStatsSchema = object({
32567
32627
  jitterMs: number(),
32568
32628
  estimatedBandwidthKbps: number(),
32569
32629
  packetLossPercent: number().min(0).max(100),
32630
+ /** The viewer's latest `/adaptive-probe` reading, Mbps — whatever it read
32631
+ * (D635 I4). Absent when it never probed or the probe failed (unknown). */
32632
+ downlinkMbps: number().optional(),
32633
+ /** Age of that reading when this report was sent, ms. */
32634
+ downlinkAgeMs: number().optional(),
32570
32635
  lastUpdated: number()
32571
32636
  });
32572
32637
  var DeviceNetworkStatsSchema = object({
@@ -32579,7 +32644,11 @@ method(object({ deviceId: number() }), DeviceNetworkStatsSchema.nullable()), met
32579
32644
  rttMs: number().min(0).max(6e4),
32580
32645
  jitterMs: number().min(0).max(1e4),
32581
32646
  estimatedBandwidthKbps: number().min(0).max(1e6),
32582
- packetLossPercent: number().min(0).max(100)
32647
+ packetLossPercent: number().min(0).max(100),
32648
+ /** Latest downlink probe, Mbps, whatever it read; absent = never / failed. */
32649
+ downlinkMbps: number().min(0).max(1e5).optional(),
32650
+ /** Age of that probe when this report is sent, ms. */
32651
+ downlinkAgeMs: number().min(0).max(864e5).optional()
32583
32652
  }), _void(), { kind: "mutation" });
32584
32653
  /**
32585
32654
  * nodes — system-scoped singleton capability for cluster topology
@@ -33978,36 +34047,95 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceI
33978
34047
  auth: "admin"
33979
34048
  });
33980
34049
  /**
33981
- * `recording` cap — footage availability + HLS playback manifests + per-device
33982
- * recording config. NOTE on events (source of truth, R5/C3): this cap carries
33983
- * NO event surface — `getPlaybackManifest` returns playlist URLs only. Timeline
33984
- * events (motion/object/audio) come from `pipelineAnalytics` (durable SQLite
33985
- * rows) and are the ONLY event surface — the recorder has none. The in-RAM
33986
- * playback markers it used to build were deleted on 2026-08-29 because nothing
33987
- * ever read them. Event<->footage joins are by time, padded with the shared
33988
- * `EVENT_PAD_MS` (`interfaces/recording-config.ts`).
34050
+ * `recording` — a camera's recorded COVERAGE and PLAYBACK, from whichever
34051
+ * source the operator picked.
34052
+ *
34053
+ * A device-scoped wrapper COLLECTION since D625, and the five lines that make
34054
+ * it one (`scope`/`mode`/`kind`/`defaultActive`/`deviceTypes`) are copied from
34055
+ * `videoclips.cap.ts` deliberately: `scope: 'device'` + `mode: 'collection'` is
34056
+ * what makes `resolveCapMount` keep the PER-DEVICE mount while the collection
34057
+ * dispatcher fans out, and a device's sources are then its BINDINGS
34058
+ * (`deviceManager.getBindings`, D554 amended) rather than "every addon that
34059
+ * registered the cap somewhere".
34060
+ *
34061
+ * ## What is here and what is not
34062
+ *
34063
+ * FIVE of them — `listSources`, `getAvailability`, `getDaysWithRecordings`,
34064
+ * `getPlayback`, `getPlaybackOptions`. Every source must be able to answer
34065
+ * these or it is not a source. Everything else that used to live on this cap —
34066
+ * storage locations, retention, relocation, rebalance, the ops log, the
34067
+ * placement table, the `mfra` byte plane, `getDeviceConfig`/`setDeviceConfig`
34068
+ * and the two batch reads — is on `recording-archive`, a system singleton, and
34069
+ * the reasons are argued there.
34070
+ *
34071
+ * ## The default is OURS, and there is nowhere to say otherwise
34072
+ *
34073
+ * {@link RECORDING_SOURCE_CAMSTACK} is RESERVED. The absence of an operator
34074
+ * choice MEANS that value; it is not a row that happened to sort first. Clips
34075
+ * end their resolver with `?? sources[0]` and that is right for clips — a
34076
+ * camera full of onboard clips that says "no clips" is the defect that work
34077
+ * exists to end. For a TIMELINE it is wrong and the measurement says why: on
34078
+ * 592, ours covers 85 362 s of the day and the card 21 388 s, and
34079
+ * `videoclips.listSources` answers the ONBOARD row first. A silent fall to the
34080
+ * first row would swap a 98.8 % bar for a 24.8 % one with nothing saying so.
34081
+ *
34082
+ * So a surface that cannot find our row says `ours-absent` and offers the
34083
+ * others as a CHOICE; it never takes one on the operator's behalf. The
34084
+ * reservation is enforced in the collection dispatcher — a `listSources` row
34085
+ * carrying this id from any addon but the recorder is refused by name — and by
34086
+ * `scripts/check-reserved-collection-sources.ts` in CI.
34087
+ *
34088
+ * NOTE on events (source of truth, R5/C3): this cap carries NO event surface.
34089
+ * Timeline events (motion/object/audio) come from `pipelineAnalytics` (durable
34090
+ * SQLite rows) and are the ONLY event surface — the recorder has none.
34091
+ * Event<->footage joins are by time, padded with the shared `EVENT_PAD_MS`
34092
+ * (`interfaces/recording-config.ts`).
33989
34093
  */
33990
- var RecordingStatusSchema = object({
33991
- deviceId: number(),
33992
- enabled: boolean(),
33993
- /** THE derived storage mode, from the one definition
33994
- * (`deriveRecordingMode`) — never a second enum. A duplicated list is how
33995
- * `on-device-decision` could have reached the recorder and not the status. */
33996
- activeMode: RecordingStorageModeSchema,
33997
- nodeId: string(),
33998
- storageBytes: number()
33999
- });
34094
+ /**
34095
+ * OUR recorder's source id. RESERVED: no other provider may return it from
34096
+ * `listSources`, and the absence of an operator choice MEANS this value.
34097
+ *
34098
+ * Reserved is ENFORCED, not requested — `device-collection-dispatch.ts` drops a
34099
+ * row spelling it from any addon but {@link RECORDING_SOURCE_CAMSTACK_ADDON},
34100
+ * naming the impostor, and `scripts/check-reserved-collection-sources.ts`
34101
+ * fails CI on a provider package that spells it at all. A rule nothing checks
34102
+ * has already been broken somewhere.
34103
+ */
34104
+ var RECORDING_SOURCE_CAMSTACK = "camstack";
34105
+ /** The one addon allowed to serve {@link RECORDING_SOURCE_CAMSTACK}: the
34106
+ * recorder, which is the addon that writes the footage (D560). */
34107
+ var RECORDING_SOURCE_CAMSTACK_ADDON = "recorder";
34000
34108
  var RecordingRangeSchema = object({
34001
34109
  profile: string(),
34002
34110
  startMs: number(),
34003
34111
  endMs: number()
34004
34112
  });
34113
+ /**
34114
+ * How a source ANSWERED, on every singular read of this cap.
34115
+ *
34116
+ * `'read'` — it looked. `ranges: []` / `days: []` is then a real claim: this
34117
+ * source has no coverage in the window. `'unreadable'` — nobody could look
34118
+ * (the camera was unreachable, the calendar rung threw, the location is
34119
+ * unmounted, the node is still on the old build), and the emptiness beside it
34120
+ * means NOTHING.
34121
+ *
34122
+ * The batch rows have carried this since the grid existed; the SINGULAR
34123
+ * answers gained it with the collection (D625 §10.4), because they are the
34124
+ * ones the single-camera picker uses and because a half-converted fleet makes
34125
+ * "nobody looked" common for the length of a deploy. Without it the timeline
34126
+ * has no vocabulary for it, and `(data ?? [])` in a viewer turns a rollout into
34127
+ * a fleet of cameras that appear to have lost their recordings (D315, D393).
34128
+ */
34129
+ var RecordingReadSchema = _enum(["read", "unreadable"]);
34005
34130
  var RecordingAvailabilitySchema = object({
34006
34131
  deviceId: number(),
34132
+ /** See {@link RecordingReadSchema}. An `'unreadable'` answer carries an empty
34133
+ * `ranges` that means nothing — never draw it as "no footage". */
34134
+ read: RecordingReadSchema,
34007
34135
  ranges: array(RecordingRangeSchema),
34008
34136
  /**
34009
- * Every profile this camera has footage in — not only the one `ranges`
34010
- * describes (D433).
34137
+ * Every profile this camera has footage in AT THIS SOURCE — not only the one
34138
+ * `ranges` describes (D433).
34011
34139
  *
34012
34140
  * `ranges` answers for ONE profile by design: the timeline is a single bar,
34013
34141
  * and enumerating all of them triples the directory reads for a bar that
@@ -34024,15 +34152,344 @@ var RecordingAvailabilitySchema = object({
34024
34152
  });
34025
34153
  var RecordingDaysSchema = object({
34026
34154
  deviceId: number(),
34155
+ /** See {@link RecordingReadSchema}. `days: []` on an `'unreadable'` answer is
34156
+ * "nobody could look", and the date-picker must not spell it the same as
34157
+ * "no footage this month". */
34158
+ read: RecordingReadSchema,
34027
34159
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
34028
34160
  days: array(number())
34029
34161
  });
34162
+ var RecordingManifestSchema = object({
34163
+ deviceId: number(),
34164
+ /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
34165
+ localMasterPath: string().nullable(),
34166
+ /** HTTP(S) URL to the master playlist on the recording node's playback server
34167
+ * (the PRIMARY candidate); null when no recording / server. Carries the
34168
+ * scoped playback token in its path. */
34169
+ playbackUrl: string().nullable(),
34170
+ /**
34171
+ * Candidate master-playlist URLs the client tries in order (LAN first, then
34172
+ * remote — Tailscale/Cloudflare if the operator configured extra hosts), each
34173
+ * carrying the same scoped token. `playbackUrl` is the first entry. Empty when
34174
+ * there is no recording / server.
34175
+ */
34176
+ playbackEndpoints: array(string())
34177
+ });
34178
+ var RecordingSourceAvailabilitySchema = object({
34179
+ state: _enum([
34180
+ "ok",
34181
+ "sleeping",
34182
+ "unreachable",
34183
+ "no-storage",
34184
+ "index-empty"
34185
+ ]),
34186
+ /** Free text, shown verbatim. Names the camera's own refusal when there is one. */
34187
+ reason: string().optional(),
34188
+ /** When this source's coverage was last CONFIRMED. A cached answer is never
34189
+ * drawn as current: the surface shows the age whenever it is older than the
34190
+ * refresh interval. The clip catalog's `catalogAsOf`, under the name the
34191
+ * timeline uses for it. */
34192
+ coverageAsOf: number().optional()
34193
+ });
34194
+ /**
34195
+ * One SOURCE of recorded coverage for a camera — a row of the picker.
34196
+ *
34197
+ * A provider lists the sources IT serves for that device, and answers for each
34198
+ * of them whether it can answer at all. A provider with nothing to offer on a
34199
+ * camera returns `[]` — it is not that camera's business. The five availability
34200
+ * states are `ClipSourceAvailability`'s verbatim: they mean exactly the same
34201
+ * things about a coverage index as about a clip catalog, and `sleeping` in
34202
+ * particular is what stops a battery camera being woken to paint a bar.
34203
+ */
34204
+ var RecordingSourceSchema = object({
34205
+ /** The source id. {@link RECORDING_SOURCE_CAMSTACK} for ours (RESERVED), a
34206
+ * vendor namespace (`native:reolink:onboard`, …) for a camera's own store. */
34207
+ source: string(),
34208
+ /** Operator-facing name of the source ("CamStack recordings", "SD card"). */
34209
+ label: string(),
34210
+ /**
34211
+ * The addon that SERVES this row, and the value a later call passes as
34212
+ * `provider`.
34213
+ *
34214
+ * Optional for version skew only. The collection dispatcher stamps it from
34215
+ * the registry, so a row that travelled through the fan-out carries the
34216
+ * authoritative id whatever the provider filled in (D557 §4).
34217
+ */
34218
+ addonId: string().optional(),
34219
+ availability: RecordingSourceAvailabilitySchema
34220
+ });
34221
+ /**
34222
+ * What a surface may DRAW for this (camera, source) — D612's rule applied to a
34223
+ * timeline: **the source declares what it can do, and the surface draws what
34224
+ * was declared. It never assumes, and never offers a gesture it will then
34225
+ * refuse.** D612 exists because `8` and `16` were offered as clip rates, the
34226
+ * broker clamped them to `4`, and no line anywhere said so.
34227
+ *
34228
+ * Asked once per (camera, source) before anything is drawn — never replaced by
34229
+ * a constant the surface keeps, which is the second authority D612 ends.
34230
+ */
34231
+ var RecordingSourceOptionsSchema = object({
34232
+ /** How this source's media reaches the player.
34233
+ * `archive` = our own indexed segment tree; `stream` = the provider's
34234
+ * forward-only fMP4 (D597); `realtime` = a replay bound to wall clock. */
34235
+ transport: _enum([
34236
+ "archive",
34237
+ "stream",
34238
+ "realtime"
34239
+ ]),
34240
+ /** What the BAR means. `continuous` = gaps are holes in a recording;
34241
+ * `sparse` = gaps are the absence of one, and must be drawn as such.
34242
+ *
34243
+ * Not an onboard-only concession: measured 2026-09-24, OUR bar covers 98.8 %
34244
+ * of 592's day and 1.2 % of 1436's. It is a fact about a (source, camera)
34245
+ * pair, and ours answers it per camera from `deriveRecordingMode`. */
34246
+ coverage: _enum(["continuous", "sparse"]),
34247
+ /** Where the playhead may be put.
34248
+ * `free` — anywhere, to the frame.
34249
+ * `forward` — only ahead of the current position.
34250
+ * `segment` — a position SNAPS to the head of the covering segment; a finer
34251
+ * ask is accepted by the camera and SILENTLY IGNORED. Measured
34252
+ * on 1436 (Hikvision V5.7.1, 2026-09-23): a window-narrowed
34253
+ * `ContentMgmt/search` returns a row and a `playbackURI`, the
34254
+ * replay opens 200 and delivers media — and the burned-in OSD of
34255
+ * the first frame reads the SEGMENT HEAD every time. Calling
34256
+ * that `forward` would tell the surface it may move the playhead
34257
+ * ahead within a loaded segment, which it may not. */
34258
+ seek: _enum([
34259
+ "free",
34260
+ "forward",
34261
+ "segment"
34262
+ ]),
34263
+ /** Frame-step BACKWARD is meaningful. */
34264
+ stepBack: boolean(),
34265
+ /** Whether the drag-scrub gesture is served, as opposed to refused by name. */
34266
+ scrub: boolean(),
34267
+ /** Deliverable rates, ascending, always containing `1`. The surface draws its
34268
+ * picker from this and from NOTHING else (D612, D620, D621). `0` is not a
34269
+ * member: pause is the absence of a rate. */
34270
+ rates: array(number().positive()).min(1).readonly(),
34271
+ /** TRUE when a read of this source HOLDS the camera's only playback session.
34272
+ * A surface with this set makes at most ONE read at a time and draws no
34273
+ * scrub-thumbnail strip, no hover preview, no prefetch and no background
34274
+ * refresh. The precedent is exact and expensive: filling one screen of
34275
+ * Hikvision thumbnails at 1.01× realtime consumed fifteen minutes of that
34276
+ * camera's only playback session (1.2.126, reported within minutes), and a
34277
+ * timeline is a screenful of reads by construction. */
34278
+ exclusive: boolean()
34279
+ });
34280
+ /**
34281
+ * How to PLAY the instant that was asked for, from the chosen source.
34282
+ *
34283
+ * No new media transport is built for onboard sources: the `clip` arm is a
34284
+ * DELEGATION to the `videoclips` transport that vendor already has (D597 /
34285
+ * D616 / D617). The onboard half of this collection is a PROJECTION of
34286
+ * `videoclips` for coverage and a delegation to it for bytes.
34287
+ */
34288
+ var RecordingPlaybackSchema = discriminatedUnion("kind", [
34289
+ object({
34290
+ kind: literal("hls"),
34291
+ manifest: RecordingManifestSchema
34292
+ }),
34293
+ object({
34294
+ kind: literal("clip"),
34295
+ /** The `videoclips` source namespace this clip id belongs to. */
34296
+ source: string(),
34297
+ clipId: string(),
34298
+ /** Where this clip actually STARTS. On a `seek: 'segment'` source the
34299
+ * playhead lands here, not at the requested instant — the surface must be
34300
+ * TOLD, not left to discover it from a burned-in OSD. */
34301
+ startsAtMs: number()
34302
+ }),
34303
+ object({
34304
+ kind: literal("none"),
34305
+ reason: string()
34306
+ })
34307
+ ]);
34308
+ var recordingCapability = {
34309
+ name: "recording",
34310
+ scope: "device",
34311
+ /** Several sources per camera, listed beside each other. The mount stays
34312
+ * `device-scoped` — see `resolveCapMount`'s ordering and D554: per-device
34313
+ * wins over the global collection fan-out. */
34314
+ mode: "collection",
34315
+ kind: "wrapper",
34316
+ defaultActive: true,
34317
+ /** Recorded coverage is a property of a camera — the cap is meaningless on a
34318
+ * sensor, a button or an event emitter, and the `defaultActive` auto-bind
34319
+ * reads this to decide which devices it may claim. */
34320
+ deviceTypes: [DeviceType.Camera],
34321
+ methods: {
34322
+ /**
34323
+ * The sources this camera has, WITH the reason any of them cannot answer.
34324
+ *
34325
+ * Asked separately from `getAvailability` because an empty bar is
34326
+ * ambiguous and this is the only place the ambiguity is resolved: every
34327
+ * bound provider contributes its own rows, and a provider that could not be
34328
+ * reached at all still produces one row saying so. A surface that draws "no
34329
+ * recordings" without reading this is drawing a guess.
34330
+ *
34331
+ * The ONLY method here without a `provider` — it is the call that tells the
34332
+ * caller what to put there.
34333
+ */
34334
+ listSources: method(object({ deviceId: number() }), array(RecordingSourceSchema).readonly(), {
34335
+ kind: "query",
34336
+ auth: "protected"
34337
+ }),
34338
+ /**
34339
+ * Recorded coverage of `[fromMs, toMs)` at ONE source — the timeline bar.
34340
+ *
34341
+ * `protected`, not `admin`: a per-camera read is exactly what a camera
34342
+ * viewer is FOR, and the device-scoped mount routes through
34343
+ * `getProviderForDevice`, so a camera outside the caller's scope is refused
34344
+ * before a provider is reached.
34345
+ */
34346
+ getAvailability: method(object({
34347
+ deviceId: number(),
34348
+ /**
34349
+ * WHICH provider to ask — the `addonId` a {@link RecordingSourceSchema}
34350
+ * row carries, never a source id and never a list. **REQUIRED**, in the
34351
+ * schema, where the generated types make it unomittable rather than
34352
+ * merely discouraged (D554 amended).
34353
+ *
34354
+ * It was learned the expensive way on `videoclips.listClips`: measured
34355
+ * on the live hub 2026-09-20, device 592 bound to `recorder` AND
34356
+ * `provider-reolink`, a bare call with `limit: 3` answered SIX rows,
34357
+ * three from each source, merged — `device-collection-dispatch.ts`
34358
+ * leaves an unpinned fan-out un-narrowed, so absence buys the union the
34359
+ * method exists not to be. An un-narrowed `getAvailability` would do
34360
+ * that to a TIMELINE: our ranges and the card's clips unioned into one
34361
+ * bar, which is "two sources are never drawn together" broken in the
34362
+ * one place it matters most.
34363
+ *
34364
+ * A provider the device is not bound to is refused BY NAME (D552's
34365
+ * `rejectUnresolvedAddonPin`), never answered by another one.
34366
+ */
34367
+ provider: string().min(1),
34368
+ fromMs: number(),
34369
+ toMs: number(),
34370
+ /**
34371
+ * Answer for THIS profile instead of the source's preferred one (D433).
34372
+ * Absent keeps the timeline's behaviour — one bar, one profile, one set
34373
+ * of reads. `profilesWithFootage` on the answer says what may be asked
34374
+ * for.
34375
+ */
34376
+ profile: string().optional()
34377
+ }), RecordingAvailabilitySchema, {
34378
+ kind: "query",
34379
+ auth: "protected"
34380
+ }),
34381
+ /** Which calendar days in [fromMs,toMs) this source has ≥1 recording in,
34382
+ * bucketed by the client's local day (`tzOffsetMinutes` = minutes to add
34383
+ * to UTC). Drives the theater date-picker's day dots. `provider` is
34384
+ * REQUIRED for the reason `getAvailability` states. */
34385
+ getDaysWithRecordings: method(object({
34386
+ deviceId: number(),
34387
+ provider: string().min(1),
34388
+ fromMs: number(),
34389
+ toMs: number(),
34390
+ tzOffsetMinutes: number()
34391
+ }), RecordingDaysSchema, {
34392
+ kind: "query",
34393
+ auth: "protected"
34394
+ }),
34395
+ /**
34396
+ * How to PLAY `[fromMs, toMs)` at this source.
34397
+ *
34398
+ * It was `getPlaybackManifest`, and the rename is not cosmetic: a
34399
+ * "manifest" is an HLS master playlist, which is a property of OUR recorder
34400
+ * and of nothing else. Keeping the name would make every onboard
34401
+ * implementation a lie in its signature. The old shape survives verbatim
34402
+ * inside the union's `hls` arm, so the recorder's implementation is
34403
+ * unchanged behind it.
34404
+ */
34405
+ getPlayback: method(object({
34406
+ deviceId: number(),
34407
+ provider: string().min(1),
34408
+ fromMs: number(),
34409
+ toMs: number(),
34410
+ profile: CamProfileSchema.optional()
34411
+ }), RecordingPlaybackSchema, {
34412
+ kind: "query",
34413
+ auth: "protected"
34414
+ }),
34415
+ /**
34416
+ * What this (camera, source) can actually DO — asked before anything is
34417
+ * drawn. See {@link RecordingSourceOptionsSchema}; a constant the surface
34418
+ * keeps instead is the second authority D612 exists to end.
34419
+ */
34420
+ getPlaybackOptions: method(object({
34421
+ deviceId: number(),
34422
+ provider: string().min(1)
34423
+ }), RecordingSourceOptionsSchema, {
34424
+ kind: "query",
34425
+ auth: "protected"
34426
+ })
34427
+ }
34428
+ };
34429
+ /**
34430
+ * `recording-archive` — OUR archive, and the intent that fills it.
34431
+ *
34432
+ * The system-singleton half of the 2026-09-24 cut (D625). `recording` used to
34433
+ * be one 33-method system singleton holding two unrelated subjects: three
34434
+ * per-camera READS about coverage and playback, and everything else — storage
34435
+ * locations, retention, relocation, rebalance, the ops log, the placement
34436
+ * table and the byte-plane primitives our scrub and export are built on.
34437
+ *
34438
+ * The reads became a device-scoped COLLECTION, so a camera's own card can be a
34439
+ * source beside ours (`recording.cap.ts`). Everything that is about OUR store,
34440
+ * or unimplementable by a camera, stayed here.
34441
+ *
34442
+ * ## On the name
34443
+ *
34444
+ * `recording-storage` was the obvious choice and is wrong: this cap also holds
34445
+ * `getDeviceConfig`/`setDeviceConfig`, which are recording INTENT — bands,
34446
+ * retention, the D62 switch authority — and a name that says "storage" invites
34447
+ * the next reader to move them out again. An archive is a thing we keep, and
34448
+ * what we keep it under is a policy; the name covers both halves honestly and
34449
+ * sits in the existing family (`recording-onboard`, `recording-export`,
34450
+ * `recording-signal`).
34451
+ *
34452
+ * ## What must NOT happen to it
34453
+ *
34454
+ * It stays a SINGLETON. It is registered by `recorder`, which is
34455
+ * `placement: 'any-node'` and runs on every recording node; the hub dispatches
34456
+ * to one of them. Putting the ledger, the placement table or the relocation
34457
+ * jobs behind a fan-out is the one genuinely dangerous move in this cut.
34458
+ *
34459
+ * `getDeviceConfig` / `setDeviceConfig` in particular are the D62 recording
34460
+ * authority (`CameraSwitch.authority`). If a write reached a different provider
34461
+ * than the read — which a collection fan-out permits — two authorities would
34462
+ * decide when one camera records, and the symptom (recording silently off, or
34463
+ * a `bands` array clobbered by a partial write) is durable and silent. Keeping
34464
+ * them here means the worst case during a rollout is a 412: the switch refuses
34465
+ * to flip and SAYS so. **Do not move them into the collection, at any point,
34466
+ * for any reason.**
34467
+ *
34468
+ * ## The two batch reads
34469
+ *
34470
+ * `getAvailabilityBatch` / `getDaysWithRecordingsBatch` take `deviceIds:
34471
+ * number[]` with no single `deviceId`, and a device-scoped mount routes
34472
+ * through `getProviderForDevice(deviceId)` — there is nothing for it to route
34473
+ * on. They stay here, and on this cap the batch is explicitly OURS: a grid has
34474
+ * no per-camera picker, and a caller that wants another source's coverage asks
34475
+ * `recording.getAvailability` per device with that source's `provider`.
34476
+ */
34477
+ var RecordingStatusSchema = object({
34478
+ deviceId: number(),
34479
+ enabled: boolean(),
34480
+ /** THE derived storage mode, from the one definition
34481
+ * (`deriveRecordingMode`) — never a second enum. A duplicated list is how
34482
+ * `on-device-decision` could have reached the recorder and not the status. */
34483
+ activeMode: RecordingStorageModeSchema,
34484
+ nodeId: string(),
34485
+ storageBytes: number()
34486
+ });
34030
34487
  /**
34031
34488
  * One camera's row in a `getAvailabilityBatch` answer.
34032
34489
  *
34033
- * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
34034
- * batch collapses the transport, not the work — plus the one thing the singular
34035
- * method never had to say:
34490
+ * `ranges` is EXACTLY what `recording.getAvailability` returns for that camera
34491
+ * at OUR source — the batch collapses the transport, not the work — plus the
34492
+ * `read` mark the singular answer now carries too (D625):
34036
34493
  *
34037
34494
  * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
34038
34495
  * no footage in the window", which is a real claim.
@@ -34064,22 +34521,6 @@ var RecordingDaysForDeviceSchema = object({
34064
34521
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
34065
34522
  days: array(number()).readonly()
34066
34523
  });
34067
- var RecordingManifestSchema = object({
34068
- deviceId: number(),
34069
- /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
34070
- localMasterPath: string().nullable(),
34071
- /** HTTP(S) URL to the master playlist on the recording node's playback server
34072
- * (the PRIMARY candidate); null when no recording / server. Carries the
34073
- * scoped playback token in its path. */
34074
- playbackUrl: string().nullable(),
34075
- /**
34076
- * Candidate master-playlist URLs the client tries in order (LAN first, then
34077
- * remote — Tailscale/Cloudflare if the operator configured extra hosts), each
34078
- * carrying the same scoped token. `playbackUrl` is the first entry. Empty when
34079
- * there is no recording / server.
34080
- */
34081
- playbackEndpoints: array(string())
34082
- });
34083
34524
  /**
34084
34525
  * Recording storage usage for one camera — what the ARCHIVE holds for it,
34085
34526
  * across every profile and every resolvable location on this node.
@@ -34337,33 +34778,22 @@ var ReadWindowBytesResultSchema = discriminatedUnion("kind", [object({
34337
34778
  * not a retry hint (retrying the same window would refuse again). */
34338
34779
  segmentEndMs: number()
34339
34780
  })]);
34340
- var recordingCapability = {
34341
- name: "recording",
34781
+ var recordingArchiveCapability = {
34782
+ name: "recording-archive",
34342
34783
  scope: "system",
34343
34784
  mode: "singleton",
34785
+ /** Moves here with the config pair — the derived Recording settings
34786
+ * section is a view over `getDeviceConfig`/`setDeviceConfig` (D14), and
34787
+ * those did not move (D625). */
34344
34788
  exposesDeviceSettings: true,
34345
34789
  status: {
34346
34790
  schema: RecordingStatusSchema,
34347
34791
  kind: "command-driven"
34348
34792
  },
34349
34793
  methods: {
34350
- getAvailability: method(object({
34351
- deviceId: number(),
34352
- fromMs: number(),
34353
- toMs: number(),
34354
- /**
34355
- * Answer for THIS profile instead of the preferred one (D433). Absent
34356
- * keeps the timeline's behaviour — one bar, one profile, one set of
34357
- * reads. `profilesWithFootage` on the answer says what may be asked
34358
- * for.
34359
- */
34360
- profile: string().optional()
34361
- }), RecordingAvailabilitySchema, {
34362
- kind: "query",
34363
- auth: "protected"
34364
- }),
34365
34794
  /**
34366
- * `getAvailability` for a SET of cameras, in one round trip.
34795
+ * `recording.getAvailability` for a SET of cameras, in one round trip, at
34796
+ * OUR source.
34367
34797
  *
34368
34798
  * A multi-camera timeline re-asks availability for every camera in the grid
34369
34799
  * on every day change; fanned out that is one request per camera for N
@@ -34371,6 +34801,13 @@ var recordingCapability = {
34371
34801
  * `availabilityProfileFor` + `rangesIn`, run concurrently inside the
34372
34802
  * recorder) — only the transport collapses.
34373
34803
  *
34804
+ * It lives on the ARCHIVE, not on the `recording` collection, because it
34805
+ * takes `deviceIds` with no single `deviceId` and a device-scoped mount has
34806
+ * nothing to route on (D625 §10.2). The consequence is stated rather than
34807
+ * hidden: the batch answers for OURS. A caller that needs another source's
34808
+ * coverage asks `recording.getAvailability` per device, naming that
34809
+ * source's provider.
34810
+ *
34374
34811
  * `protected` for the same reason the singular method is: every id in
34375
34812
  * `deviceIds` is a device reference, so the F1 #3 gate refuses any camera
34376
34813
  * outside the caller's scope — one id out of scope refuses the CALL, it
@@ -34388,20 +34825,10 @@ var recordingCapability = {
34388
34825
  kind: "query",
34389
34826
  auth: "protected"
34390
34827
  }),
34391
- /** Which calendar days in [fromMs,toMs) have ≥1 recorded segment, bucketed by
34392
- * the client's local day (`tzOffsetMinutes` = minutes to add to UTC). Drives
34393
- * the theater date-picker's day dots. */
34394
- getDaysWithRecordings: method(object({
34395
- deviceId: number(),
34396
- fromMs: number(),
34397
- toMs: number(),
34398
- tzOffsetMinutes: number()
34399
- }), RecordingDaysSchema, {
34400
- kind: "query",
34401
- auth: "protected"
34402
- }),
34403
34828
  /**
34404
- * `getDaysWithRecordings` for a SET of cameras, in one round trip.
34829
+ * `recording.getDaysWithRecordings` for a SET of cameras, in one round
34830
+ * trip, at OUR source. Same placement argument as
34831
+ * {@link getAvailabilityBatch}.
34405
34832
  *
34406
34833
  * The cheapest question in the product, asked once per camera per month
34407
34834
  * change. One directory read per day per camera at the owner, unchanged;
@@ -34420,14 +34847,6 @@ var recordingCapability = {
34420
34847
  kind: "query",
34421
34848
  auth: "protected"
34422
34849
  }),
34423
- getPlaybackManifest: method(object({
34424
- deviceId: number(),
34425
- fromMs: number(),
34426
- toMs: number()
34427
- }), RecordingManifestSchema, {
34428
- kind: "query",
34429
- auth: "protected"
34430
- }),
34431
34850
  getStorageUsage: method(object({}), RecordingStorageUsageSchema, {
34432
34851
  kind: "query",
34433
34852
  auth: "admin"
@@ -34443,6 +34862,10 @@ var recordingCapability = {
34443
34862
  * value (D315, D393, D590). It carries schedules and retention, no secret,
34444
34863
  * and the per-device gate in `scope-access.ts` still applies; `setDeviceConfig`
34445
34864
  * stays `admin`.
34865
+ *
34866
+ * It is a SINGLETON method and stays one (D625 §16.2): it is the D62
34867
+ * recording authority, and an authority that several providers could answer
34868
+ * is the "two knobs over one decision" D62 forbids.
34446
34869
  */
34447
34870
  getDeviceConfig: method(object({ deviceId: number() }), RecordingConfigSchema, {
34448
34871
  kind: "query",
@@ -34450,7 +34873,13 @@ var recordingCapability = {
34450
34873
  }),
34451
34874
  /** Locate footage at a wall-clock instant: the covering segment's window,
34452
34875
  * or a gap with the forward nearest covered edge. Used by a feeder running
34453
- * in another addon process to seek recorded footage over tRPC. */
34876
+ * in another addon process to seek recorded footage over tRPC.
34877
+ *
34878
+ * BYTE PLANE: defined by `mfra` byte ranges over OUR own MP4 segment tree.
34879
+ * There is no vendor-neutral statement of it, its only callers are feeders
34880
+ * in other addon processes (the stream broker, the replay-clip source),
34881
+ * and an onboard source that ever needs bytes delegates to the
34882
+ * `videoclips` transport its vendor already has (D625 §9.3, §10.5). */
34454
34883
  locateSegment: method(object({
34455
34884
  deviceId: number(),
34456
34885
  profile: string(),
@@ -35183,6 +35612,307 @@ var recordingExportCapability = {
35183
35612
  }
35184
35613
  };
35185
35614
  /**
35615
+ * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
35616
+ * writes to the CAMERA's own card, on the camera's own schedule.
35617
+ *
35618
+ * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
35619
+ * footage ledger, our storage locations, our retention. This one has a
35620
+ * different authority — the camera's firmware — and per D62 it stores
35621
+ * nothing of its own. Every value here is read from the camera and every
35622
+ * write goes back to the camera; there is no CamStack-side mirror that
35623
+ * could disagree with the device.
35624
+ *
35625
+ * ## One shape, two firmwares
35626
+ *
35627
+ * Measured 2026-09-22 against the live fleet:
35628
+ *
35629
+ * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
35630
+ * | --- | --- | --- |
35631
+ * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
35632
+ * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
35633
+ * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
35634
+ * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
35635
+ * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
35636
+ * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
35637
+ * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
35638
+ * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
35639
+ *
35640
+ * The two schedule models look different and are the same thing in
35641
+ * different coordinates: both answer "for this trigger, during which
35642
+ * weekly windows does the camera record". {@link RecordWindow} is that
35643
+ * question in one shape — Hikvision's ranges map straight onto it,
35644
+ * Reolink's mask expands into hour-aligned windows.
35645
+ *
35646
+ * ## Union, not intersection
35647
+ *
35648
+ * **The same fields exist on every camera.** What differs per device is
35649
+ * which VALUES that device accepts, and that is what {@link
35650
+ * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
35651
+ * per field plus the schedule's own limits. A control a camera cannot
35652
+ * honour is rendered DISABLED WITH ITS REASON, never missing and never
35653
+ * dead: disabled must not look like broken.
35654
+ *
35655
+ * ## Refusal by name
35656
+ *
35657
+ * A write a camera cannot honour is refused with a sentence the operator
35658
+ * can read — never accepted and dropped. Both providers refuse through
35659
+ * {@link describeOnboardRefusal}, so the vocabulary is one function and
35660
+ * one test, not two hand-written vendor opinions.
35661
+ *
35662
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
35663
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
35664
+ * injected from `status`) reports the live values, and a single
35665
+ * `setSettings` mutation applies a partial change. No hand-written
35666
+ * settings-contribution methods.
35667
+ */
35668
+ /**
35669
+ * What makes the camera start recording during a window.
35670
+ *
35671
+ * The union of both vendors' vocabularies. `continuous` is Hikvision's
35672
+ * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
35673
+ * object-class triggers are Reolink-only today and the smart-event ones
35674
+ * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
35675
+ * firmwares measured — a camera that cannot record on a trigger simply
35676
+ * does not list it in `options.schedule.triggers`, and a window naming
35677
+ * it is REFUSED, not dropped.
35678
+ */
35679
+ var RecordTriggerSchema = _enum([
35680
+ "continuous",
35681
+ "motion",
35682
+ "person",
35683
+ "vehicle",
35684
+ "animal",
35685
+ "lineCrossing",
35686
+ "intrusion",
35687
+ "loitering",
35688
+ "alarmInput"
35689
+ ]);
35690
+ /**
35691
+ * One weekly recording window: "on `day`, from `startMinute` to
35692
+ * `endMinute`, record on `trigger`".
35693
+ *
35694
+ * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
35695
+ * both firmwares enumerate). Minutes are local camera time since
35696
+ * midnight; `endMinute` may be 1440, meaning end of day — that is
35697
+ * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
35698
+ * collapsing it to 0 would turn a whole-day window into an empty one.
35699
+ */
35700
+ var RecordWindowSchema = object({
35701
+ trigger: RecordTriggerSchema,
35702
+ day: number().int().min(0).max(6),
35703
+ startMinute: number().int().min(0).max(1439),
35704
+ endMinute: number().int().min(1).max(1440)
35705
+ });
35706
+ /** Status of one physical volume, as the camera itself describes it. */
35707
+ var OnboardStorageVolumeSchema = object({
35708
+ /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
35709
+ id: string(),
35710
+ /** The camera's own name for it, when it gives one (`hddName`). */
35711
+ label: string().optional(),
35712
+ status: _enum([
35713
+ "ok",
35714
+ "unformatted",
35715
+ "error",
35716
+ "offline",
35717
+ "unknown"
35718
+ ]),
35719
+ /**
35720
+ * Total size in MB, or **null when the camera did not say**.
35721
+ *
35722
+ * Never 0 for an unreadable value: a measurement that failed is not a
35723
+ * measurement (D393), and a card whose size is unknown must not be
35724
+ * rendered as a card of size zero.
35725
+ */
35726
+ capacityMb: number().nullable(),
35727
+ /**
35728
+ * Free space in MB, or null when unknown.
35729
+ *
35730
+ * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
35731
+ * 1439 both report exactly 11776 MB free — the fixed reserve a looping
35732
+ * card converges on once it has wrapped. At loop steady state the
35733
+ * number is identical whether the camera recorded yesterday or stopped
35734
+ * a month ago.
35735
+ */
35736
+ freeMb: number().nullable(),
35737
+ /** True when the camera reports the volume writable (`property` RW). */
35738
+ writable: boolean().optional()
35739
+ });
35740
+ object({
35741
+ storage: discriminatedUnion("kind", [
35742
+ object({
35743
+ kind: literal("present"),
35744
+ volumes: array(OnboardStorageVolumeSchema)
35745
+ }),
35746
+ object({
35747
+ kind: literal("absent"),
35748
+ reason: string()
35749
+ }),
35750
+ object({
35751
+ kind: literal("unknown"),
35752
+ reason: string()
35753
+ })
35754
+ ]),
35755
+ tracks: array(object({
35756
+ id: string(),
35757
+ enabled: boolean(),
35758
+ isVideo: boolean(),
35759
+ /** From the camera's own track description. Null when it does not say. */
35760
+ codec: string().nullable(),
35761
+ resolution: string().nullable(),
35762
+ /** Per-track overwrite flag, where the firmware keeps it per track. */
35763
+ overwriteWhenFull: boolean().nullable()
35764
+ })),
35765
+ /**
35766
+ * The track the write path targets — the enabled VIDEO one. Null when
35767
+ * no track could be identified, which is itself a refusal reason.
35768
+ */
35769
+ primaryTrackId: string().nullable(),
35770
+ /** Master "record to the card at all" switch. */
35771
+ enabled: boolean().nullable(),
35772
+ overwriteWhenFull: boolean().nullable(),
35773
+ preRecordSec: number().nullable(),
35774
+ postRecordSec: number().nullable(),
35775
+ /** Length of one recorded file, in minutes. */
35776
+ segmentMinutes: number().nullable(),
35777
+ /** The primary track's weekly windows, flattened. */
35778
+ windows: array(RecordWindowSchema),
35779
+ /**
35780
+ * How many windows the camera described that CamStack could NOT read —
35781
+ * an unrecognised trigger, an unparseable clock, a weekday it does not
35782
+ * name.
35783
+ *
35784
+ * A dropped window is work the reader threw away, and a schedule that
35785
+ * silently shows fewer rows than the camera holds is how an operator
35786
+ * saves back a schedule shorter than the one they were looking at
35787
+ * (D391). Non-zero means the window list is INCOMPLETE and a write
35788
+ * that replaces it would delete what was not shown — which is why a
35789
+ * provider reporting a non-zero count also reports the schedule as not
35790
+ * writable.
35791
+ */
35792
+ unreadableWindows: number(),
35793
+ /**
35794
+ * The camera is scheduled to record and has NO usable storage.
35795
+ *
35796
+ * A first-class fact because it is the fleet's most common silent
35797
+ * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
35798
+ * to a card that is not there. Neither the schedule nor the storage
35799
+ * read says anything wrong on its own; only the pair does.
35800
+ */
35801
+ recordingToNowhere: boolean(),
35802
+ lastFetchedAt: number()
35803
+ });
35804
+ /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
35805
+ var RangeSchema = object({
35806
+ min: number(),
35807
+ max: number(),
35808
+ step: number()
35809
+ });
35810
+ /**
35811
+ * The values a camera actually takes for a numeric field, when they are a SET
35812
+ * rather than a range.
35813
+ *
35814
+ * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
35815
+ * (I91DN) on 2026-09-22 by writing each value and reading it back:
35816
+ *
35817
+ * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
35818
+ * camera's "no limit" — `-1` and `4294967295` both land on it);
35819
+ * - post-record: `5, 10, 30, 60, 120, 300, 600`.
35820
+ *
35821
+ * Neither is expressible as a step: the first has a sentinel two billion away
35822
+ * from its neighbours, the second doubles and then jumps. A range that tried
35823
+ * would forbid values the camera takes AND permit values it silently replaces
35824
+ * with 5 — wrong in both directions at once.
35825
+ *
35826
+ * `sentinel` names the member that is not a duration, so a surface can render
35827
+ * "no limit" instead of `2147483647` seconds.
35828
+ */
35829
+ var AllowedValuesSchema = object({
35830
+ values: array(number()).min(1),
35831
+ sentinel: object({
35832
+ value: number(),
35833
+ meaning: _enum(["no-limit", "disabled"])
35834
+ }).optional()
35835
+ });
35836
+ /**
35837
+ * Per-field availability on ONE camera.
35838
+ *
35839
+ * The field exists on every camera — this says whether this one can be
35840
+ * read and whether it can be written, and `reason` says why not when
35841
+ * either is false. The UI renders the control DISABLED with the reason
35842
+ * rather than hiding it, so a limitation is legible instead of looking
35843
+ * like a missing feature.
35844
+ */
35845
+ var OnboardFieldSupportSchema = object({
35846
+ readable: boolean(),
35847
+ writable: boolean(),
35848
+ /** Required whenever `readable` or `writable` is false. */
35849
+ reason: string().optional()
35850
+ });
35851
+ /** What this camera's schedule model can express. */
35852
+ var OnboardScheduleSupportSchema = object({
35853
+ support: OnboardFieldSupportSchema,
35854
+ /**
35855
+ * The smallest time step the camera can express, in minutes.
35856
+ *
35857
+ * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
35858
+ * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
35859
+ * window whose edges are not a multiple of this is REFUSED rather than
35860
+ * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
35861
+ * and nothing says so.
35862
+ */
35863
+ granularityMinutes: number(),
35864
+ /** Triggers this camera can record on. A window naming another is refused. */
35865
+ triggers: array(RecordTriggerSchema),
35866
+ /**
35867
+ * False when the camera stores ONE trigger per time range, so two
35868
+ * windows overlapping on the same day cannot carry different triggers.
35869
+ * True on Reolink, whose mask is per-trigger and independent.
35870
+ */
35871
+ supportsOverlappingTriggers: boolean()
35872
+ });
35873
+ var RecordingOnboardOptionsSchema = object({
35874
+ enabled: OnboardFieldSupportSchema,
35875
+ overwriteWhenFull: OnboardFieldSupportSchema,
35876
+ preRecordSec: OnboardFieldSupportSchema,
35877
+ preRecordSecRange: RangeSchema.optional(),
35878
+ /** Preferred over the range when the camera takes a SET, not a span. */
35879
+ preRecordSecAllowed: AllowedValuesSchema.optional(),
35880
+ postRecordSec: OnboardFieldSupportSchema,
35881
+ postRecordSecRange: RangeSchema.optional(),
35882
+ /** Preferred over the range when the camera takes a SET, not a span. */
35883
+ postRecordSecAllowed: AllowedValuesSchema.optional(),
35884
+ segmentMinutes: OnboardFieldSupportSchema,
35885
+ segmentMinutesRange: RangeSchema.optional(),
35886
+ /** Preferred over the range when the camera takes a SET, not a span. */
35887
+ segmentMinutesAllowed: AllowedValuesSchema.optional(),
35888
+ schedule: OnboardScheduleSupportSchema
35889
+ });
35890
+ /**
35891
+ * A partial change. Every field optional.
35892
+ *
35893
+ * Unlike the other `deviceConfig` caps, a provider here does **NOT**
35894
+ * silently ignore a field it cannot support — it refuses, by name,
35895
+ * through {@link describeOnboardRefusal}. Silence on a recording setting
35896
+ * is the failure D62 exists to prevent: the operator believes the camera
35897
+ * is recording the way the form says, and it is not.
35898
+ */
35899
+ var RecordingOnboardPatchSchema = object({
35900
+ enabled: boolean().optional(),
35901
+ overwriteWhenFull: boolean().optional(),
35902
+ preRecordSec: number().optional(),
35903
+ postRecordSec: number().optional(),
35904
+ segmentMinutes: number().optional(),
35905
+ /** The complete new window set for the primary track — not a delta. */
35906
+ windows: array(RecordWindowSchema).optional()
35907
+ });
35908
+ DeviceType.Camera, method(object({ deviceId: number() }), RecordingOnboardOptionsSchema), method(object({
35909
+ deviceId: number(),
35910
+ settings: RecordingOnboardPatchSchema
35911
+ }), _void(), {
35912
+ kind: "mutation",
35913
+ auth: "admin"
35914
+ });
35915
+ /**
35186
35916
  * A camera's own "record me NOW" LEVEL — a signal the device raises while
35187
35917
  * something it knows about is happening (a robot vacuum cleaning, a machine
35188
35918
  * running, a gate open) and lowers when it stops.
@@ -40179,6 +40909,12 @@ Object.freeze({
40179
40909
  addonId: null,
40180
40910
  access: "view"
40181
40911
  },
40912
+ "deviceManager.listDeviceIdsByCaps": {
40913
+ capName: "device-manager",
40914
+ capScope: "system",
40915
+ addonId: null,
40916
+ access: "view"
40917
+ },
40182
40918
  "deviceManager.listLocationDescriptions": {
40183
40919
  capName: "device-manager",
40184
40920
  capScope: "system",
@@ -40665,24 +41401,6 @@ Object.freeze({
40665
41401
  addonId: null,
40666
41402
  access: "view"
40667
41403
  },
40668
- "events.getEventClipUrl": {
40669
- capName: "events",
40670
- capScope: "device",
40671
- addonId: null,
40672
- access: "view"
40673
- },
40674
- "events.getEvents": {
40675
- capName: "events",
40676
- capScope: "device",
40677
- addonId: null,
40678
- access: "view"
40679
- },
40680
- "events.getEventThumbnail": {
40681
- capName: "events",
40682
- capScope: "device",
40683
- addonId: null,
40684
- access: "view"
40685
- },
40686
41404
  "faceGallery.assignFace": {
40687
41405
  capName: "face-gallery",
40688
41406
  capScope: "system",
@@ -42003,6 +42721,12 @@ Object.freeze({
42003
42721
  addonId: null,
42004
42722
  access: "view"
42005
42723
  },
42724
+ "notificationRules.getTemplateCatalog": {
42725
+ capName: "notification-rules",
42726
+ capScope: "system",
42727
+ addonId: null,
42728
+ access: "view"
42729
+ },
42006
42730
  "notificationRules.listDeviceMutes": {
42007
42731
  capName: "notification-rules",
42008
42732
  capScope: "system",
@@ -42021,6 +42745,12 @@ Object.freeze({
42021
42745
  addonId: null,
42022
42746
  access: "view"
42023
42747
  },
42748
+ "notificationRules.previewTemplate": {
42749
+ capName: "notification-rules",
42750
+ capScope: "system",
42751
+ addonId: null,
42752
+ access: "view"
42753
+ },
42024
42754
  "notificationRules.resolveArtifactUrl": {
42025
42755
  capName: "notification-rules",
42026
42756
  capScope: "system",
@@ -43557,224 +44287,236 @@ Object.freeze({
43557
44287
  addonId: null,
43558
44288
  access: "create"
43559
44289
  },
43560
- "recording.applyDeviceSettingsPatch": {
44290
+ "recording.getAvailability": {
43561
44291
  capName: "recording",
43562
- capScope: "system",
44292
+ capScope: "device",
43563
44293
  addonId: null,
43564
- access: "create"
44294
+ access: "view"
43565
44295
  },
43566
- "recording.cancelRelocateJob": {
44296
+ "recording.getDaysWithRecordings": {
43567
44297
  capName: "recording",
43568
- capScope: "system",
44298
+ capScope: "device",
43569
44299
  addonId: null,
43570
- access: "create"
44300
+ access: "view"
43571
44301
  },
43572
- "recording.cancelStorageMigrationMove": {
44302
+ "recording.getPlayback": {
43573
44303
  capName: "recording",
43574
- capScope: "system",
44304
+ capScope: "device",
43575
44305
  addonId: null,
43576
- access: "create"
44306
+ access: "view"
43577
44307
  },
43578
- "recording.deleteFootprint": {
44308
+ "recording.getPlaybackOptions": {
43579
44309
  capName: "recording",
43580
- capScope: "system",
44310
+ capScope: "device",
43581
44311
  addonId: null,
43582
- access: "delete"
44312
+ access: "view"
43583
44313
  },
43584
- "recording.getAvailability": {
44314
+ "recording.listSources": {
43585
44315
  capName: "recording",
43586
- capScope: "system",
44316
+ capScope: "device",
43587
44317
  addonId: null,
43588
44318
  access: "view"
43589
44319
  },
43590
- "recording.getAvailabilityBatch": {
43591
- capName: "recording",
44320
+ "recordingArchive.applyDeviceSettingsPatch": {
44321
+ capName: "recording-archive",
43592
44322
  capScope: "system",
43593
44323
  addonId: null,
43594
- access: "view"
44324
+ access: "create"
43595
44325
  },
43596
- "recording.getDaysWithRecordings": {
43597
- capName: "recording",
44326
+ "recordingArchive.cancelRelocateJob": {
44327
+ capName: "recording-archive",
43598
44328
  capScope: "system",
43599
44329
  addonId: null,
43600
- access: "view"
44330
+ access: "create"
43601
44331
  },
43602
- "recording.getDaysWithRecordingsBatch": {
43603
- capName: "recording",
44332
+ "recordingArchive.cancelStorageMigrationMove": {
44333
+ capName: "recording-archive",
44334
+ capScope: "system",
44335
+ addonId: null,
44336
+ access: "create"
44337
+ },
44338
+ "recordingArchive.deleteFootprint": {
44339
+ capName: "recording-archive",
44340
+ capScope: "system",
44341
+ addonId: null,
44342
+ access: "delete"
44343
+ },
44344
+ "recordingArchive.getAvailabilityBatch": {
44345
+ capName: "recording-archive",
43604
44346
  capScope: "system",
43605
44347
  addonId: null,
43606
44348
  access: "view"
43607
44349
  },
43608
- "recording.getDeviceConfig": {
43609
- capName: "recording",
44350
+ "recordingArchive.getDaysWithRecordingsBatch": {
44351
+ capName: "recording-archive",
43610
44352
  capScope: "system",
43611
44353
  addonId: null,
43612
44354
  access: "view"
43613
44355
  },
43614
- "recording.getDeviceLiveContribution": {
43615
- capName: "recording",
44356
+ "recordingArchive.getDeviceConfig": {
44357
+ capName: "recording-archive",
43616
44358
  capScope: "system",
43617
44359
  addonId: null,
43618
44360
  access: "view"
43619
44361
  },
43620
- "recording.getDeviceSettingsContribution": {
43621
- capName: "recording",
44362
+ "recordingArchive.getDeviceLiveContribution": {
44363
+ capName: "recording-archive",
43622
44364
  capScope: "system",
43623
44365
  addonId: null,
43624
44366
  access: "view"
43625
44367
  },
43626
- "recording.getPlacement": {
43627
- capName: "recording",
44368
+ "recordingArchive.getDeviceSettingsContribution": {
44369
+ capName: "recording-archive",
43628
44370
  capScope: "system",
43629
44371
  addonId: null,
43630
44372
  access: "view"
43631
44373
  },
43632
- "recording.getPlaybackManifest": {
43633
- capName: "recording",
44374
+ "recordingArchive.getPlacement": {
44375
+ capName: "recording-archive",
43634
44376
  capScope: "system",
43635
44377
  addonId: null,
43636
44378
  access: "view"
43637
44379
  },
43638
- "recording.getRelocateResidue": {
43639
- capName: "recording",
44380
+ "recordingArchive.getRelocateResidue": {
44381
+ capName: "recording-archive",
43640
44382
  capScope: "system",
43641
44383
  addonId: null,
43642
44384
  access: "view"
43643
44385
  },
43644
- "recording.getStatus": {
43645
- capName: "recording",
44386
+ "recordingArchive.getStatus": {
44387
+ capName: "recording-archive",
43646
44388
  capScope: "system",
43647
44389
  addonId: null,
43648
44390
  access: "view"
43649
44391
  },
43650
- "recording.getStorageMigrationMoveStatus": {
43651
- capName: "recording",
44392
+ "recordingArchive.getStorageMigrationMoveStatus": {
44393
+ capName: "recording-archive",
43652
44394
  capScope: "system",
43653
44395
  addonId: null,
43654
44396
  access: "view"
43655
44397
  },
43656
- "recording.getStorageUsage": {
43657
- capName: "recording",
44398
+ "recordingArchive.getStorageUsage": {
44399
+ capName: "recording-archive",
43658
44400
  capScope: "system",
43659
44401
  addonId: null,
43660
44402
  access: "view"
43661
44403
  },
43662
- "recording.listOpsLog": {
43663
- capName: "recording",
44404
+ "recordingArchive.listOpsLog": {
44405
+ capName: "recording-archive",
43664
44406
  capScope: "system",
43665
44407
  addonId: null,
43666
44408
  access: "view"
43667
44409
  },
43668
- "recording.listRelocateJobs": {
43669
- capName: "recording",
44410
+ "recordingArchive.listRelocateJobs": {
44411
+ capName: "recording-archive",
43670
44412
  capScope: "system",
43671
44413
  addonId: null,
43672
44414
  access: "view"
43673
44415
  },
43674
- "recording.locateSegment": {
43675
- capName: "recording",
44416
+ "recordingArchive.locateSegment": {
44417
+ capName: "recording-archive",
43676
44418
  capScope: "system",
43677
44419
  addonId: null,
43678
44420
  access: "view"
43679
44421
  },
43680
- "recording.pauseForStorageMigration": {
43681
- capName: "recording",
44422
+ "recordingArchive.pauseForStorageMigration": {
44423
+ capName: "recording-archive",
43682
44424
  capScope: "system",
43683
44425
  addonId: null,
43684
44426
  access: "create"
43685
44427
  },
43686
- "recording.planStorageRebalance": {
43687
- capName: "recording",
44428
+ "recordingArchive.planStorageRebalance": {
44429
+ capName: "recording-archive",
43688
44430
  capScope: "system",
43689
44431
  addonId: null,
43690
44432
  access: "view"
43691
44433
  },
43692
- "recording.pruneFootage": {
43693
- capName: "recording",
44434
+ "recordingArchive.pruneFootage": {
44435
+ capName: "recording-archive",
43694
44436
  capScope: "system",
43695
44437
  addonId: null,
43696
44438
  access: "create"
43697
44439
  },
43698
- "recording.readGopBytes": {
43699
- capName: "recording",
44440
+ "recordingArchive.readGopBytes": {
44441
+ capName: "recording-archive",
43700
44442
  capScope: "system",
43701
44443
  addonId: null,
43702
44444
  access: "view"
43703
44445
  },
43704
- "recording.readSegmentBytes": {
43705
- capName: "recording",
44446
+ "recordingArchive.readSegmentBytes": {
44447
+ capName: "recording-archive",
43706
44448
  capScope: "system",
43707
44449
  addonId: null,
43708
44450
  access: "view"
43709
44451
  },
43710
- "recording.readWindowBytes": {
43711
- capName: "recording",
44452
+ "recordingArchive.readWindowBytes": {
44453
+ capName: "recording-archive",
43712
44454
  capScope: "system",
43713
44455
  addonId: null,
43714
44456
  access: "view"
43715
44457
  },
43716
- "recording.reconcileLedgerAgainstDisk": {
43717
- capName: "recording",
44458
+ "recordingArchive.reconcileLedgerAgainstDisk": {
44459
+ capName: "recording-archive",
43718
44460
  capScope: "system",
43719
44461
  addonId: null,
43720
44462
  access: "create"
43721
44463
  },
43722
- "recording.refreshStorageLocationsForMigration": {
43723
- capName: "recording",
44464
+ "recordingArchive.refreshStorageLocationsForMigration": {
44465
+ capName: "recording-archive",
43724
44466
  capScope: "system",
43725
44467
  addonId: null,
43726
44468
  access: "create"
43727
44469
  },
43728
- "recording.relocateFootage": {
43729
- capName: "recording",
44470
+ "recordingArchive.relocateFootage": {
44471
+ capName: "recording-archive",
43730
44472
  capScope: "system",
43731
44473
  addonId: null,
43732
44474
  access: "create"
43733
44475
  },
43734
- "recording.renderClip": {
43735
- capName: "recording",
44476
+ "recordingArchive.renderClip": {
44477
+ capName: "recording-archive",
43736
44478
  capScope: "system",
43737
44479
  addonId: null,
43738
44480
  access: "create"
43739
44481
  },
43740
- "recording.renderGif": {
43741
- capName: "recording",
44482
+ "recordingArchive.renderGif": {
44483
+ capName: "recording-archive",
43742
44484
  capScope: "system",
43743
44485
  addonId: null,
43744
44486
  access: "create"
43745
44487
  },
43746
- "recording.rescanStorage": {
43747
- capName: "recording",
44488
+ "recordingArchive.rescanStorage": {
44489
+ capName: "recording-archive",
43748
44490
  capScope: "system",
43749
44491
  addonId: null,
43750
44492
  access: "create"
43751
44493
  },
43752
- "recording.resumeForStorageMigration": {
43753
- capName: "recording",
44494
+ "recordingArchive.resumeForStorageMigration": {
44495
+ capName: "recording-archive",
43754
44496
  capScope: "system",
43755
44497
  addonId: null,
43756
44498
  access: "create"
43757
44499
  },
43758
- "recording.setDeviceConfig": {
43759
- capName: "recording",
44500
+ "recordingArchive.setDeviceConfig": {
44501
+ capName: "recording-archive",
43760
44502
  capScope: "system",
43761
44503
  addonId: null,
43762
44504
  access: "create"
43763
44505
  },
43764
- "recording.setDevicePlacement": {
43765
- capName: "recording",
44506
+ "recordingArchive.setDevicePlacement": {
44507
+ capName: "recording-archive",
43766
44508
  capScope: "system",
43767
44509
  addonId: null,
43768
44510
  access: "create"
43769
44511
  },
43770
- "recording.startStorageMigrationMove": {
43771
- capName: "recording",
44512
+ "recordingArchive.startStorageMigrationMove": {
44513
+ capName: "recording-archive",
43772
44514
  capScope: "system",
43773
44515
  addonId: null,
43774
44516
  access: "create"
43775
44517
  },
43776
- "recording.startStorageRebalance": {
43777
- capName: "recording",
44518
+ "recordingArchive.startStorageRebalance": {
44519
+ capName: "recording-archive",
43778
44520
  capScope: "system",
43779
44521
  addonId: null,
43780
44522
  access: "create"
@@ -46079,21 +46821,6 @@ Object.freeze({
46079
46821
  form: "single",
46080
46822
  optional: false
46081
46823
  }],
46082
- "events.getEventClipUrl": [{
46083
- name: "deviceId",
46084
- form: "single",
46085
- optional: false
46086
- }],
46087
- "events.getEvents": [{
46088
- name: "deviceId",
46089
- form: "single",
46090
- optional: false
46091
- }],
46092
- "events.getEventThumbnail": [{
46093
- name: "deviceId",
46094
- form: "single",
46095
- optional: false
46096
- }],
46097
46824
  "faceGallery.getFaceByTrack": [{
46098
46825
  name: "deviceId",
46099
46826
  form: "single",
@@ -47012,107 +47739,117 @@ Object.freeze({
47012
47739
  form: "single",
47013
47740
  optional: false
47014
47741
  }],
47015
- "recording.deleteFootprint": [{
47742
+ "recording.getAvailability": [{
47016
47743
  name: "deviceId",
47017
47744
  form: "single",
47018
47745
  optional: false
47019
47746
  }],
47020
- "recording.getAvailability": [{
47747
+ "recording.getDaysWithRecordings": [{
47021
47748
  name: "deviceId",
47022
47749
  form: "single",
47023
47750
  optional: false
47024
47751
  }],
47025
- "recording.getAvailabilityBatch": [{
47026
- name: "deviceIds",
47027
- form: "array",
47752
+ "recording.getPlayback": [{
47753
+ name: "deviceId",
47754
+ form: "single",
47028
47755
  optional: false
47029
47756
  }],
47030
- "recording.getDaysWithRecordings": [{
47757
+ "recording.getPlaybackOptions": [{
47031
47758
  name: "deviceId",
47032
47759
  form: "single",
47033
47760
  optional: false
47034
47761
  }],
47035
- "recording.getDaysWithRecordingsBatch": [{
47036
- name: "deviceIds",
47037
- form: "array",
47762
+ "recording.listSources": [{
47763
+ name: "deviceId",
47764
+ form: "single",
47038
47765
  optional: false
47039
47766
  }],
47040
- "recording.getDeviceConfig": [{
47767
+ "recordingArchive.deleteFootprint": [{
47041
47768
  name: "deviceId",
47042
47769
  form: "single",
47043
47770
  optional: false
47044
47771
  }],
47045
- "recording.getPlaybackManifest": [{
47772
+ "recordingArchive.getAvailabilityBatch": [{
47773
+ name: "deviceIds",
47774
+ form: "array",
47775
+ optional: false
47776
+ }],
47777
+ "recordingArchive.getDaysWithRecordingsBatch": [{
47778
+ name: "deviceIds",
47779
+ form: "array",
47780
+ optional: false
47781
+ }],
47782
+ "recordingArchive.getDeviceConfig": [{
47046
47783
  name: "deviceId",
47047
47784
  form: "single",
47048
47785
  optional: false
47049
47786
  }],
47050
- "recording.listOpsLog": [{
47787
+ "recordingArchive.listOpsLog": [{
47051
47788
  name: "deviceId",
47052
47789
  form: "single",
47053
47790
  optional: true
47054
47791
  }],
47055
- "recording.locateSegment": [{
47792
+ "recordingArchive.locateSegment": [{
47056
47793
  name: "deviceId",
47057
47794
  form: "single",
47058
47795
  optional: false
47059
47796
  }],
47060
- "recording.pruneFootage": [{
47797
+ "recordingArchive.pruneFootage": [{
47061
47798
  name: "deviceId",
47062
47799
  form: "single",
47063
47800
  optional: false
47064
47801
  }],
47065
- "recording.readGopBytes": [{
47802
+ "recordingArchive.readGopBytes": [{
47066
47803
  name: "deviceId",
47067
47804
  form: "single",
47068
47805
  optional: false
47069
47806
  }],
47070
- "recording.readSegmentBytes": [{
47807
+ "recordingArchive.readSegmentBytes": [{
47071
47808
  name: "deviceId",
47072
47809
  form: "single",
47073
47810
  optional: false
47074
47811
  }],
47075
- "recording.readWindowBytes": [{
47812
+ "recordingArchive.readWindowBytes": [{
47076
47813
  name: "deviceId",
47077
47814
  form: "single",
47078
47815
  optional: false
47079
47816
  }],
47080
- "recording.reconcileLedgerAgainstDisk": [{
47817
+ "recordingArchive.reconcileLedgerAgainstDisk": [{
47081
47818
  name: "deviceId",
47082
47819
  form: "single",
47083
47820
  optional: true
47084
47821
  }],
47085
- "recording.relocateFootage": [{
47822
+ "recordingArchive.relocateFootage": [{
47086
47823
  name: "deviceId",
47087
47824
  form: "single",
47088
47825
  optional: true
47089
47826
  }],
47090
- "recording.renderClip": [{
47827
+ "recordingArchive.renderClip": [{
47091
47828
  name: "deviceId",
47092
47829
  form: "single",
47093
47830
  optional: false
47094
47831
  }],
47095
- "recording.renderGif": [{
47832
+ "recordingArchive.renderGif": [{
47096
47833
  name: "deviceId",
47097
47834
  form: "single",
47098
47835
  optional: false
47099
47836
  }],
47100
- "recording.rescanStorage": [{
47837
+ "recordingArchive.rescanStorage": [{
47101
47838
  name: "deviceId",
47102
47839
  form: "single",
47103
47840
  optional: false
47104
47841
  }],
47105
- "recording.setDeviceConfig": [{
47842
+ "recordingArchive.setDeviceConfig": [{
47106
47843
  name: "deviceId",
47107
47844
  form: "single",
47108
47845
  optional: false
47109
47846
  }],
47110
- "recording.setDevicePlacement": [{
47847
+ "recordingArchive.setDevicePlacement": [{
47111
47848
  name: "deviceId",
47112
47849
  form: "single",
47113
47850
  optional: false
47114
47851
  }],
47115
- "recording.startStorageMigrationMove": [{
47852
+ "recordingArchive.startStorageMigrationMove": [{
47116
47853
  name: "deviceId",
47117
47854
  form: "single",
47118
47855
  optional: true
@@ -47631,6 +48368,41 @@ var NC_AUDIO_DEFAULTS = {
47631
48368
  };
47632
48369
  new Set(["devices", "classes"]);
47633
48370
  NC_AUDIO_DEFAULTS.hitPercent, NC_AUDIO_DEFAULTS.samplingSeconds;
48371
+ var RULE_BODY_ONLY = ["body"];
48372
+ var RULE_TEXT = ["title", "body"];
48373
+ var BATTERY = ["device-battery-low", "device-battery-normal"];
48374
+ var CONSUMABLE = ["device-consumable-low", "device-consumable-normal"];
48375
+ var UPDATES = [
48376
+ "addon-update-available",
48377
+ "server-update-available",
48378
+ "wrapper-update-available"
48379
+ ];
48380
+ var PACKAGE_UPDATES = ["addon-update-available", "server-update-available"];
48381
+ var ALARM = [
48382
+ "alarm-triggered",
48383
+ "alarm-armed",
48384
+ "alarm-disarmed",
48385
+ "alarm-arming",
48386
+ "alarm-arm-refused",
48387
+ "alarm-pending",
48388
+ "alarm-rearmed",
48389
+ "alarm-sensor-bypass",
48390
+ "alarm-check-failed"
48391
+ ];
48392
+ var COMBINED_ALARM = ["alarm-triggered"];
48393
+ var DEVICE_EVENTS = [
48394
+ "device-online",
48395
+ "device-offline",
48396
+ "device-disabled",
48397
+ "device-enabled",
48398
+ ...BATTERY,
48399
+ ...CONSUMABLE,
48400
+ "stream-online",
48401
+ "stream-offline",
48402
+ "detection-blind",
48403
+ ...ALARM
48404
+ ];
48405
+ [...DEVICE_EVENTS], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...PACKAGE_UPDATES], [...RULE_TEXT], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_BODY_ONLY], [...RULE_TEXT], [...RULE_TEXT], [...DEVICE_EVENTS], [...RULE_TEXT], [...DEVICE_EVENTS], [...RULE_TEXT], [...BATTERY, ...CONSUMABLE], [...RULE_TEXT], [...CONSUMABLE], [...RULE_TEXT], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...PACKAGE_UPDATES], [...RULE_TEXT], [...PACKAGE_UPDATES], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...PACKAGE_UPDATES], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT];
47634
48406
  /**
47635
48407
  * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
47636
48408
  *
@@ -49409,6 +50181,18 @@ Object.defineProperty(exports, "RECORDING_EXPORT_MAX_READ_BYTES", {
49409
50181
  return RECORDING_EXPORT_MAX_READ_BYTES;
49410
50182
  }
49411
50183
  });
50184
+ Object.defineProperty(exports, "RECORDING_SOURCE_CAMSTACK", {
50185
+ enumerable: true,
50186
+ get: function() {
50187
+ return RECORDING_SOURCE_CAMSTACK;
50188
+ }
50189
+ });
50190
+ Object.defineProperty(exports, "RECORDING_SOURCE_CAMSTACK_ADDON", {
50191
+ enumerable: true,
50192
+ get: function() {
50193
+ return RECORDING_SOURCE_CAMSTACK_ADDON;
50194
+ }
50195
+ });
49412
50196
  Object.defineProperty(exports, "RecordingConfigSchema", {
49413
50197
  enumerable: true,
49414
50198
  get: function() {
@@ -49883,6 +50667,12 @@ Object.defineProperty(exports, "record", {
49883
50667
  return record;
49884
50668
  }
49885
50669
  });
50670
+ Object.defineProperty(exports, "recordingArchiveCapability", {
50671
+ enumerable: true,
50672
+ get: function() {
50673
+ return recordingArchiveCapability;
50674
+ }
50675
+ });
49886
50676
  Object.defineProperty(exports, "recordingCapability", {
49887
50677
  enumerable: true,
49888
50678
  get: function() {