@camstack/addon-osd-manager 0.1.75 → 0.1.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (18) hide show
  1. package/dist/{MotionZonesSettings-C7jRsout.mjs → MotionZonesSettings-CcUQv9US.mjs} +2 -2
  2. package/dist/{PrivacyMaskSettings-BdztqQd2.mjs → PrivacyMaskSettings-C1kY8VDL.mjs} +4 -4
  3. package/dist/{SceneMonitorEditor-Cg47gSZn.mjs → SceneMonitorEditor-DSLR3HyX.mjs} +3 -3
  4. package/dist/_stub.js +10 -10
  5. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-Dzr3iybL.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-CBDnIQTl.mjs} +4 -4
  6. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-zKEKO1Le.mjs +26 -0
  7. package/dist/{hostInit-C0fK-ddn.mjs → hostInit-DSL_s_Qu.mjs} +3 -3
  8. package/dist/index.js +646 -487
  9. package/dist/index.mjs +646 -487
  10. package/dist/{player-overlays-o65lZSbk.mjs → player-overlays-DPEo1CnK.mjs} +1 -1
  11. package/dist/remoteEntry.js +1 -1
  12. package/dist/{responsive-DbTv1Tw_.mjs → responsive-veJ_u_lF.mjs} +1 -1
  13. package/dist/{square-3lb1NRdx.mjs → square-BKRxPrj2.mjs} +1 -1
  14. package/dist/{trash-2-C80GFjip.mjs → trash-2-B_FOJHPE.mjs} +1 -1
  15. package/dist/{use-device-snapshot-C6nbTUeb.mjs → use-device-snapshot-CCUZGgXd.mjs} +1 -1
  16. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-BsrrBUK-.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-CtVrWNnx.mjs} +1 -1
  17. package/package.json +1 -1
  18. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-bYmvh4b8.mjs +0 -26
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- //#region ../types/dist/event-category-zAv7pMUz.mjs
1
+ //#region ../types/dist/event-category-CnLqLOKs.mjs
2
2
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
3
3
  EventCategory["SystemBoot"] = "system.boot";
4
4
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -193,6 +193,19 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
193
193
  EventCategory["ProcessCrashed"] = "process.crashed";
194
194
  EventCategory["ProcessRestartScheduled"] = "process.restart_scheduled";
195
195
  EventCategory["ProcessRestarted"] = "process.restarted";
196
+ /**
197
+ * The SET of storage locations changed — one was created, edited, enabled,
198
+ * disabled or deleted through `storage.upsertLocation` / `deleteLocation`.
199
+ *
200
+ * Telemetry, not a transaction (D8/D11): every consumer that re-resolves on
201
+ * it must also converge on its own periodic path, because a dropped event
202
+ * must not leave a node writing to yesterday's disk set forever. It exists
203
+ * because there was NO signal at all — an operator who added a second
204
+ * recordings disk in the admin UI got nothing, and the recorder kept its
205
+ * resolved locations until something else happened to re-resolve them
206
+ * (D387). Payload `StorageLocationsChangedPayload`.
207
+ */
208
+ EventCategory["StorageLocationsChanged"] = "storage.locations-changed";
196
209
  EventCategory["RecordingStarted"] = "recording.started";
197
210
  EventCategory["RecordingStopped"] = "recording.stopped";
198
211
  EventCategory["RecordingError"] = "recording.error";
@@ -7710,111 +7723,6 @@ var CameraSwitchGroupSchema = object({
7710
7723
  fetchedAt: number()
7711
7724
  });
7712
7725
  /**
7713
- * Per-component log CHANNELS — the gate a hot path consults, and the registry
7714
- * an addon declares its channels in.
7715
- *
7716
- * ## Two axes, deliberately separated
7717
- *
7718
- * - **DECLARATION** — which channels exist. Only the addon knows:
7719
- * `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
7720
- * baichuan/handshake. A hand-wired central list rots at the first addition,
7721
- * and rots silently. So a channel is declared where it is consulted, and the
7722
- * `log-channels` capability enumerates the declarations.
7723
- * - **VALUE** — at which level, for which scope, until when. That stays ONE
7724
- * thing: the logging settings document on the `system` cap. Two authorities
7725
- * over the values is the exact defect
7726
- * `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
7727
- * remove; re-introducing it from the cure side would be grotesque.
7728
- *
7729
- * Nothing in this file reads a clock, an env var or a store. The registry is
7730
- * a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
7731
- * the hot path with a value somebody actually read, and by
7732
- * {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
7733
- * never reaches here, so it can neither disarm an armed channel nor arm a
7734
- * disarmed one (D49).
7735
- *
7736
- * ## The canonical call shape
7737
- *
7738
- * ```ts
7739
- * if (CH_RTP.on && CH_RTP.wants(deviceId)) {
7740
- * CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
7741
- * }
7742
- * ```
7743
- *
7744
- * `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
7745
- * read. Disarmed, a call site costs one load and one branch, and the `extras`
7746
- * object literal is never constructed because it lives inside the branch. It
7747
- * is the same shape already proven in production at `stream-broker.ts:1650`,
7748
- * and the same discipline `LoggingGate.allowsDestination` uses for the
7749
- * destination floor (measured at 1.93 ns/call when off).
7750
- *
7751
- * ## Why a channel emits at `info`
7752
- *
7753
- * `loki-logging.addon.ts` pins the destination default at `info` and
7754
- * `loki-destination.ts` drops everything below it, so a line emitted at
7755
- * `debug` never reaches Loki and the hub's in-memory ring only holds ~35
7756
- * minutes. A diagnostic that cannot be read an hour later is worse than no
7757
- * diagnostic, because it looks done. {@link LogChannelGate.log} therefore
7758
- * emits at the channel's declared level, whose schema floor is `info`.
7759
- */
7760
- /**
7761
- * The level a channel writes at once armed.
7762
- *
7763
- * `debug` is absent ON PURPOSE and not by omission: below `info` the line does
7764
- * not leave the process for Loki, and the whole point of arming a channel is
7765
- * to read it later.
7766
- */
7767
- var LogChannelLevelSchema = _enum([
7768
- "info",
7769
- "warn",
7770
- "error"
7771
- ]);
7772
- /**
7773
- * What an addon declares about one channel. No value, no state — a
7774
- * declaration is inert.
7775
- */
7776
- var LogChannelDescriptorSchema = object({
7777
- /**
7778
- * Dotted `area.thing`, unique across the workspace. `area` is conventionally
7779
- * the addon's short name so an operator reading a channel list can tell who
7780
- * owns it without a second lookup.
7781
- */
7782
- name: string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
7783
- /** One sentence: what the operator will SEE after arming it. */
7784
- description: string().min(1),
7785
- /** The level its lines are emitted at. Never below `info`. */
7786
- defaultLevel: LogChannelLevelSchema,
7787
- /**
7788
- * Whether this channel can be narrowed to a camera.
7789
- *
7790
- * `true` is a PROMISE with two halves, and both must hold: the gate is
7791
- * consulted with the numeric device id, AND every line the channel admits
7792
- * carries `tags: { deviceId }` with that same numeric id. The second half is
7793
- * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
7794
- * keeps `deviceId` out of the stream labels for cardinality, so the tag in
7795
- * the body is the only way to filter.
7796
- *
7797
- * A channel whose lines carry the device only in `meta` (or not at all) is
7798
- * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
7799
- * the operator narrows to one camera, sees nothing, and concludes the code
7800
- * path was never taken.
7801
- */
7802
- perDevice: boolean()
7803
- });
7804
- /**
7805
- * An armed window over one channel, as the document hands it to a mirror.
7806
- *
7807
- * A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
7808
- * expires by itself, which is the one failure a boolean cannot avoid.
7809
- */
7810
- var LogChannelWindowSchema = object({
7811
- channel: string().min(1),
7812
- /** Epoch ms the window closes at. */
7813
- armedUntilMs: number(),
7814
- /** `null` = every camera. A non-empty list narrows to those numeric ids. */
7815
- deviceIds: array(number().int()).readonly().nullable()
7816
- });
7817
- /**
7818
7726
  * Ops-log — the durable, append-only operations audit shared by the
7819
7727
  * recordings and events management surfaces.
7820
7728
  *
@@ -8736,6 +8644,21 @@ var StorageCleanupJobSchema = object({
8736
8644
  });
8737
8645
  var StorageCleanupStatusInputSchema = object({ jobId: string().optional() });
8738
8646
  /**
8647
+ * The one typed state of a storage location. Authoritative Zod schema — the TS
8648
+ * alias below is `z.infer<>` of it, never a second spelling.
8649
+ */
8650
+ var StorageLocationModeSchema = _enum([
8651
+ "active",
8652
+ "readonly",
8653
+ "drain",
8654
+ "disabled"
8655
+ ]);
8656
+ _enum([
8657
+ "normal",
8658
+ "never",
8659
+ "drain"
8660
+ ]);
8661
+ /**
8739
8662
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
8740
8663
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
8741
8664
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -8760,8 +8683,11 @@ var StorageLocationTypeSchema = string().regex(/^[a-z][a-zA-Z0-9-]*$/);
8760
8683
  * `STORAGE_LOCATION_CARDINALITY` map has been removed.
8761
8684
  *
8762
8685
  * `id` is a stable namespaced string of the form `<type>:<slug>`.
8763
- * The default location for a type uses `id === <type>:default` by
8764
- * convention (the bare type ref like `'backups'` resolves to it).
8686
+ * The seed names its first instance `<type>:default` — a NAME, not a flag.
8687
+ * There is no default location any more (D383): `enabled` is the whole write
8688
+ * model, and a bare type ref resolves to the sole location of the type, or —
8689
+ * transitionally, only while legacy NULL-stamped rows exist — to the row whose
8690
+ * slug is `default`.
8765
8691
  *
8766
8692
  * `isSystem` is a legacy persisted flag. Seed still creates the initial
8767
8693
  * `<type>:default` locations; the flag is no longer a lock, a badge, or a
@@ -8782,23 +8708,37 @@ var StorageLocationSchema = object({
8782
8708
  * flag at upsert time, not here (the schema is provider-agnostic).
8783
8709
  */
8784
8710
  nodeId: string().optional(),
8785
- isDefault: boolean().default(false),
8786
8711
  isSystem: boolean().default(false),
8787
8712
  /**
8788
- * Operator opt-in: whether consumers that BALANCE across several locations
8789
- * of a type may write here. Recordings reads it today; event media and
8790
- * backups are the next consumers, which is why the flag lives on the
8791
- * location rather than in any one addon's store nothing has to be
8792
- * extended to add the next consumer.
8713
+ * THE write switch, and the only one (D383). `enabled: true` means every
8714
+ * consumer that chooses a write target for this type may write here, and all
8715
+ * enabled locations of a type are used TOGETHER; `false` means read-only
8716
+ * still read, still played back, still age-swept, still drained, never
8717
+ * written.
8793
8718
  *
8794
- * OPTIONAL, and ABSENT MEANS ACTIVE. Every location persisted before the
8795
- * flag existed reads back with no flag and keeps working exactly as before;
8796
- * that is the whole compat story, and it is why no migration ships with it.
8797
- * A newly CREATED sibling is stamped `false` by the orchestrator (creating a
8798
- * disk must not silently start writing to it); the default of a type is
8799
- * always stamped `true`.
8719
+ * OPTIONAL only for the wire: an upsert that omits it means "leave what is
8720
+ * stored" on an update and "born inert unless it is the first location of its
8721
+ * type" on a create. On a PERSISTED row absence is legacy and it means
8722
+ * enabled {@link isLocationEnabled} is the one place that says so, and the
8723
+ * orchestrator stamps every flagless row `true` once at hydrate so absence
8724
+ * stops existing rather than being re-derived on every read.
8800
8725
  */
8801
8726
  enabled: boolean().optional(),
8727
+ /**
8728
+ * THE state of this location (D385), and the only authority on what may be
8729
+ * written, read or evicted here. Interpreted in exactly one place —
8730
+ * `storage-location-mode.ts` — which also folds the legacy
8731
+ * `enabled` / `config.readOnly` pair into a mode so an old row is never
8732
+ * ambiguous.
8733
+ *
8734
+ * OPTIONAL only for the wire and for rows written before D385: absence is
8735
+ * resolved by `resolveLocationMode`, and the orchestrator stamps every
8736
+ * unstamped row ONCE at hydrate so absence stops existing rather than being
8737
+ * re-derived on every read. `enabled` survives one release as a DERIVED
8738
+ * mirror (`mode === 'active'`); `withLocationMode` is the only writer of
8739
+ * either, so the two cannot disagree.
8740
+ */
8741
+ mode: StorageLocationModeSchema.optional(),
8802
8742
  /** COMPUTED at read time by the orchestrator (statfs of the backing volume
8803
8743
  * for node-local locations it can reach) — never persisted, absent when the
8804
8744
  * volume is remote/unreachable. The single capacity truth every UI reads. */
@@ -8806,13 +8746,50 @@ var StorageLocationSchema = object({
8806
8746
  totalBytes: number(),
8807
8747
  availableBytes: number()
8808
8748
  }).nullable().optional(),
8749
+ /**
8750
+ * How much of that volume CamStack ITSELF holds on this location (D388) —
8751
+ * COMPUTED at read time from the `storage-occupancy` providers' own figures,
8752
+ * never persisted, never a filesystem walk.
8753
+ *
8754
+ * **ABSENT MEANS UNKNOWN, never zero.** No provider has reported for this
8755
+ * location yet — nobody stores here, the owning addon is down, or the first
8756
+ * refresh has not completed. A UI must omit the segment rather than draw it
8757
+ * at zero, which would claim we occupy nothing (D315). It is an OBJECT and
8758
+ * not a bare number precisely so that a `?? 0` on the consuming side has to
8759
+ * be spelled out loud instead of appearing by accident.
8760
+ *
8761
+ * `measuredAtMs` is the OLDEST contributing measurement, so it is honest
8762
+ * about the whole figure rather than about its freshest part.
8763
+ */
8764
+ owned: object({
8765
+ bytes: number().int().nonnegative(),
8766
+ measuredAtMs: number().int().nonnegative()
8767
+ }).optional(),
8809
8768
  createdAt: number(),
8810
8769
  updatedAt: number()
8811
8770
  });
8771
+ object({ isDefault: boolean().optional() });
8772
+ /**
8773
+ * How far a `drain` has got (D386) — the read a UI renders, and nothing more.
8774
+ *
8775
+ * `estimatedEmptyAtMs` is derived from the growth the ratchet has actually
8776
+ * OBSERVED and is `null` when it has observed none. Never a fabricated date: a
8777
+ * drain with no observed growth has no honest ETA, and inventing one is how an
8778
+ * operator learns not to believe the screen.
8779
+ */
8780
+ var StorageDrainProgressSchema = object({
8781
+ locationId: string(),
8782
+ startedAtMs: number(),
8783
+ startBytes: number(),
8784
+ bytesRemaining: number(),
8785
+ drained: boolean(),
8786
+ estimatedEmptyAtMs: number().nullable()
8787
+ });
8812
8788
  /**
8813
8789
  * Reference accepted by consumer-facing `api.storage.*` calls.
8814
8790
  * Either:
8815
- * - a `StorageLocationType` (e.g. `'backups'`) → orchestrator resolves to the default of that type
8791
+ * - a `StorageLocationType` (e.g. `'backups'`) → the sole location of that type
8792
+ * (transitionally, the `<type>:default`-slugged row when several exist)
8816
8793
  * - a fully-qualified id (e.g. `'backups:nas-01'`) → addresses a specific instance
8817
8794
  *
8818
8795
  * The orchestrator's `resolveRef(ref)` handles both cases.
@@ -8988,6 +8965,111 @@ var DecoderSessionConfigSchema = object({
8988
8965
  */
8989
8966
  debug: boolean().optional()
8990
8967
  });
8968
+ /**
8969
+ * Per-component log CHANNELS — the gate a hot path consults, and the registry
8970
+ * an addon declares its channels in.
8971
+ *
8972
+ * ## Two axes, deliberately separated
8973
+ *
8974
+ * - **DECLARATION** — which channels exist. Only the addon knows:
8975
+ * `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
8976
+ * baichuan/handshake. A hand-wired central list rots at the first addition,
8977
+ * and rots silently. So a channel is declared where it is consulted, and the
8978
+ * `log-channels` capability enumerates the declarations.
8979
+ * - **VALUE** — at which level, for which scope, until when. That stays ONE
8980
+ * thing: the logging settings document on the `system` cap. Two authorities
8981
+ * over the values is the exact defect
8982
+ * `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
8983
+ * remove; re-introducing it from the cure side would be grotesque.
8984
+ *
8985
+ * Nothing in this file reads a clock, an env var or a store. The registry is
8986
+ * a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
8987
+ * the hot path with a value somebody actually read, and by
8988
+ * {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
8989
+ * never reaches here, so it can neither disarm an armed channel nor arm a
8990
+ * disarmed one (D49).
8991
+ *
8992
+ * ## The canonical call shape
8993
+ *
8994
+ * ```ts
8995
+ * if (CH_RTP.on && CH_RTP.wants(deviceId)) {
8996
+ * CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
8997
+ * }
8998
+ * ```
8999
+ *
9000
+ * `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
9001
+ * read. Disarmed, a call site costs one load and one branch, and the `extras`
9002
+ * object literal is never constructed because it lives inside the branch. It
9003
+ * is the same shape already proven in production at `stream-broker.ts:1650`,
9004
+ * and the same discipline `LoggingGate.allowsDestination` uses for the
9005
+ * destination floor (measured at 1.93 ns/call when off).
9006
+ *
9007
+ * ## Why a channel emits at `info`
9008
+ *
9009
+ * `loki-logging.addon.ts` pins the destination default at `info` and
9010
+ * `loki-destination.ts` drops everything below it, so a line emitted at
9011
+ * `debug` never reaches Loki and the hub's in-memory ring only holds ~35
9012
+ * minutes. A diagnostic that cannot be read an hour later is worse than no
9013
+ * diagnostic, because it looks done. {@link LogChannelGate.log} therefore
9014
+ * emits at the channel's declared level, whose schema floor is `info`.
9015
+ */
9016
+ /**
9017
+ * The level a channel writes at once armed.
9018
+ *
9019
+ * `debug` is absent ON PURPOSE and not by omission: below `info` the line does
9020
+ * not leave the process for Loki, and the whole point of arming a channel is
9021
+ * to read it later.
9022
+ */
9023
+ var LogChannelLevelSchema = _enum([
9024
+ "info",
9025
+ "warn",
9026
+ "error"
9027
+ ]);
9028
+ /**
9029
+ * What an addon declares about one channel. No value, no state — a
9030
+ * declaration is inert.
9031
+ */
9032
+ var LogChannelDescriptorSchema = object({
9033
+ /**
9034
+ * Dotted `area.thing`, unique across the workspace. `area` is conventionally
9035
+ * the addon's short name so an operator reading a channel list can tell who
9036
+ * owns it without a second lookup.
9037
+ */
9038
+ name: string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
9039
+ /** One sentence: what the operator will SEE after arming it. */
9040
+ description: string().min(1),
9041
+ /** The level its lines are emitted at. Never below `info`. */
9042
+ defaultLevel: LogChannelLevelSchema,
9043
+ /**
9044
+ * Whether this channel can be narrowed to a camera.
9045
+ *
9046
+ * `true` is a PROMISE with two halves, and both must hold: the gate is
9047
+ * consulted with the numeric device id, AND every line the channel admits
9048
+ * carries `tags: { deviceId }` with that same numeric id. The second half is
9049
+ * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
9050
+ * keeps `deviceId` out of the stream labels for cardinality, so the tag in
9051
+ * the body is the only way to filter.
9052
+ *
9053
+ * A channel whose lines carry the device only in `meta` (or not at all) is
9054
+ * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
9055
+ * the operator narrows to one camera, sees nothing, and concludes the code
9056
+ * path was never taken.
9057
+ */
9058
+ perDevice: boolean()
9059
+ });
9060
+ /**
9061
+ * An armed window over one channel, as the document hands it to a mirror.
9062
+ *
9063
+ * A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
9064
+ * expires by itself, which is the one failure a boolean cannot avoid.
9065
+ */
9066
+ var LogChannelWindowSchema = object({
9067
+ channel: string().min(1),
9068
+ /** Epoch ms the window closes at. */
9069
+ armedUntilMs: number(),
9070
+ /** `null` = every camera. A non-empty list narrows to those numeric ids. */
9071
+ deviceIds: array(number().int()).readonly().nullable()
9072
+ });
8991
9073
  var MODEL_FORMATS = [
8992
9074
  "onnx",
8993
9075
  "coreml",
@@ -25844,7 +25926,6 @@ var storageCapability = {
25844
25926
  }), _instanceof(Uint8Array)),
25845
25927
  endDownload: method(object({ downloadId: string() }), _void(), { kind: "mutation" }),
25846
25928
  listLocations: method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()),
25847
- getDefaultLocation: method(object({ type: StorageLocationTypeSchema }), StorageLocationSchema.nullable()),
25848
25929
  listLocationDeclarations: method(_void(), array(StorageLocationDeclarationSchema).readonly()),
25849
25930
  upsertLocation: method(StorageLocationSchema.omit({
25850
25931
  createdAt: true,
@@ -25874,6 +25955,12 @@ var storageCapability = {
25874
25955
  kind: "mutation",
25875
25956
  auth: "admin"
25876
25957
  }),
25958
+ /**
25959
+ * How far each draining location has got (D386). A pure READ of what the
25960
+ * last pressure sweep computed — it starts no work, and a location that is
25961
+ * not draining simply does not appear.
25962
+ */
25963
+ listDrainProgress: method(_void(), array(StorageDrainProgressSchema).readonly()),
25877
25964
  testLocation: method(object({ id: string() }), object({
25878
25965
  ok: boolean(),
25879
25966
  error: string().optional()
@@ -26021,6 +26108,71 @@ var storageMigrationCapability = {
26021
26108
  history: method(object({}), array(StorageMigrationJobSchema).readonly(), { auth: "admin" })
26022
26109
  }
26023
26110
  };
26111
+ /**
26112
+ * `storage-occupancy` — how many bytes an addon actually HOLDS on a storage
26113
+ * location (D388).
26114
+ *
26115
+ * ## Why this is not `storage-evictable`
26116
+ *
26117
+ * `storage-evictable.getEvictableUsage` looks like the same question and is
26118
+ * not, in two ways that both matter and both bite hardest on the locations an
26119
+ * operator most wants a figure for:
26120
+ *
26121
+ * - it reports the whole eviction DOMAIN, not the location. `recordings:default`
26122
+ * and `recordingsLow:default` deliberately share one root and evict as one
26123
+ * oldest-first pool, so both answer with the SAME combined total. As an
26124
+ * occupancy figure that double-counts the disk.
26125
+ * - it reports ZERO for a location whose eviction policy is `never` (D385) —
26126
+ * a `readonly` or `disabled` disk. Those are exactly the disks an operator
26127
+ * is retiring and staring at.
26128
+ *
26129
+ * So this is its own contract with its own quantity, and the quantity is
26130
+ * OCCUPIED: every byte the addon holds on that location, whether or not it
26131
+ * would ever be willing to delete it. A provider that can only answer
26132
+ * "evictable" must not register here — a number that silently means different
26133
+ * things per class is worse than no number.
26134
+ *
26135
+ * ## Absence is an answer
26136
+ *
26137
+ * A location nobody reports for is UNKNOWN, never zero (D315). The orchestrator
26138
+ * stamps `StorageLocation.owned` only for locations it has a report for, and
26139
+ * the field is an OBJECT rather than a bare number so that a `?? 0` on the
26140
+ * consuming side has to be written out loud instead of appearing by accident.
26141
+ *
26142
+ * `internal: true` — consumed by the orchestrator's `listLocations` stamp, never
26143
+ * a public client surface. Clients read the stamped `StorageLocation.owned`.
26144
+ */
26145
+ /** One provider's occupancy answer for one location. */
26146
+ var StorageOccupancyReportSchema = object({
26147
+ locationId: string(),
26148
+ /** Bytes this provider holds on THAT location — not its eviction domain, and
26149
+ * not net of what it is willing to delete. */
26150
+ ownedBytes: number().int().nonnegative(),
26151
+ /** When the provider last actually measured this. The orchestrator carries it
26152
+ * through so a UI can say how old the figure is instead of implying "now". */
26153
+ measuredAtMs: number().int().nonnegative()
26154
+ });
26155
+ var storageOccupancyCapability = {
26156
+ name: "storage-occupancy",
26157
+ scope: "system",
26158
+ mode: "collection",
26159
+ internal: true,
26160
+ methods: {
26161
+ /**
26162
+ * Occupancy for the given locations, in ONE round trip.
26163
+ *
26164
+ * A provider answers only for the locations it actually holds bytes on and
26165
+ * OMITS the rest — an omitted location is "I hold nothing measurable here",
26166
+ * which the orchestrator merges as a contribution of nothing rather than as
26167
+ * a claim that the location is empty. Only a location no provider reports
26168
+ * at all stays unknown.
26169
+ *
26170
+ * This must be CHEAP and must never walk a filesystem: it is on the admin
26171
+ * UI's `listLocations` path. The owner keeps its own figure fresh (D224) and
26172
+ * answers from what it already has.
26173
+ */
26174
+ getOccupancy: method(object({ locationIds: array(string()).readonly() }), array(StorageOccupancyReportSchema).readonly(), { auth: "admin" }) }
26175
+ };
26024
26176
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
26025
26177
  providerId: string().min(1),
26026
26178
  displayName: string().min(1),
@@ -28376,88 +28528,6 @@ onStatusChanged: { data: object({
28376
28528
  volatileStateFields: ["lastUpdated"]
28377
28529
  };
28378
28530
  /**
28379
- * Network-link snapshot. Same shape for every provider (a Reolink wifi
28380
- * camera, a Home Assistant device with a signal-strength sensor, a Tapo
28381
- * plug): one slice under `device.runtimeState['network-link']`, one badge,
28382
- * one Home Assistant projection.
28383
- */
28384
- var NetworkLinkStatusSchema = object({
28385
- /** The link the device is on. `'unknown'` = not read yet, not "no link". */
28386
- type: _enum([
28387
- "wifi",
28388
- "ethernet",
28389
- "cellular",
28390
- "unknown"
28391
- ]),
28392
- /**
28393
- * Link quality, 0..100 inclusive, normalised by the provider from whatever
28394
- * the firmware reports (bars, RSSI, a vendor scale). **`null` means NOT
28395
- * KNOWN or NOT APPLICABLE** — a wired link has no signal, and a wireless
28396
- * one whose reading has not landed must not be drawn at 0 %. Consumers
28397
- * SKIP a null rather than coerce it.
28398
- */
28399
- signalPercent: number().min(0).max(100).nullable(),
28400
- /** Raw received signal strength in dBm, when the firmware reports one. */
28401
- rssiDbm: number().optional(),
28402
- /** Network name of a wireless link, when the firmware reports it. */
28403
- ssid: string().optional(),
28404
- /** Ms epoch of the last observation. Lets consumers reason about freshness. */
28405
- lastUpdated: number()
28406
- });
28407
- var networkLinkCapability = {
28408
- name: "network-link",
28409
- scope: "device",
28410
- deviceNative: true,
28411
- mode: "singleton",
28412
- deviceTypes: [
28413
- DeviceType.Camera,
28414
- DeviceType.Sensor,
28415
- DeviceType.Button,
28416
- DeviceType.Switch,
28417
- DeviceType.Light,
28418
- DeviceType.Lock,
28419
- DeviceType.Siren
28420
- ],
28421
- methods: {},
28422
- events: {
28423
- /**
28424
- * Emitted whenever the cached status changes (a link switch, a signal
28425
- * reading that moved). Mirrored on the parent chain by the
28426
- * DeviceEventPropagator like `battery.onStatusChanged`.
28427
- */
28428
- onStatusChanged: { data: object({
28429
- deviceId: number(),
28430
- status: NetworkLinkStatusSchema
28431
- }) } },
28432
- status: {
28433
- schema: NetworkLinkStatusSchema,
28434
- kind: "push",
28435
- empty: {
28436
- type: "unknown",
28437
- signalPercent: null,
28438
- lastUpdated: 0
28439
- }
28440
- },
28441
- /**
28442
- * Runtime-state slice — every provider stores the same shape under
28443
- * `device.runtimeState['network-link']`, read once by the badge and the
28444
- * Home Assistant projector regardless of the driver.
28445
- */
28446
- runtimeState: NetworkLinkStatusSchema,
28447
- /**
28448
- * Runtime-state durability: **restored** — a link reading is slow to
28449
- * change and a sleeping battery camera may not report for hours; the
28450
- * restored slice is what the badge shows until the next read.
28451
- *
28452
- * See `RuntimeStateDurability`. Enforced by
28453
- * `scripts/check-runtime-state-durability.ts`.
28454
- */
28455
- durability: "restored",
28456
- /** Clock fields: written, but excluded from the compare that decides
28457
- * whether persisting is worth a SQLite commit. */
28458
- volatileStateFields: ["lastUpdated"]
28459
- };
28460
- /**
28461
28531
  * Generic boolean sensor — last-resort fallback when no domain-
28462
28532
  * specific binary cap fits (Home Assistant `binary_sensor` without a
28463
28533
  * known `device_class`, or a domain we haven't typed yet). Pure
@@ -32268,6 +32338,369 @@ var nativeObjectDetectionCapability = {
32268
32338
  volatileStateFields: ["lastFetchedAt"]
32269
32339
  };
32270
32340
  /**
32341
+ * `navigation` — a device-scoped capability that natively expresses the FULL
32342
+ * navigation / action surface of a robot that DRIVES ITSELF and carries an
32343
+ * on-board camera (the Dreame robot-vacuum camera is the first provider).
32344
+ *
32345
+ * Why a NEW cap rather than overloading `ptz`:
32346
+ * - `ptz` moves a *gimbal* on a fixed camera (pan/tilt/zoom of the lens). A
32347
+ * robot vacuum has no gimbal — the whole chassis drives, turns and spins.
32348
+ * The two are different physical models: PTZ is absolute-position + presets,
32349
+ * navigation is momentary drive nudges + discrete robot ACTIONS
32350
+ * (dock / spot-clean / follow-pet / go-to-point / …).
32351
+ * - This cap is the SOURCE OF TRUTH. Two consumers adapt from it rather than
32352
+ * the reverse:
32353
+ * 1. a native CamStack navigation panel (data-driven from `listActions`
32354
+ * / `getOptions`), and
32355
+ * 2. the PTZ surface — a thin adapter mimics `ptz` from `navigation` so a
32356
+ * robot camera shows up in the existing PTZ control path without every
32357
+ * PTZ provider learning about robots. The mapping lives in the adapter,
32358
+ * not here (see the addon design note):
32359
+ * ptz.continuousMove({pan,tilt}) → navigation.move({pan,tilt})
32360
+ * ptz.stop() → navigation.stop()
32361
+ * ptz.goHome() → navigation.runAction('goHome')
32362
+ * ptz.getPresets() → navigation.listActions() (id→preset)
32363
+ * ptz.goToPreset(id) → navigation.runAction(id)
32364
+ *
32365
+ * ## Continuous drive
32366
+ *
32367
+ * `move` is MOMENTARY. Fluid navigation comes from the UI (or the PTZ adapter)
32368
+ * sending `move({pan,tilt})` REPEATEDLY at ~1 Hz while a direction is held, and
32369
+ * one `stop()` on release — exactly like the robot app's remote-drive joystick.
32370
+ * The provider forwards EACH `move` to one drive write; it must NOT debounce or
32371
+ * coalesce them. The UI owns the cadence.
32372
+ *
32373
+ * ## The action dictionary
32374
+ *
32375
+ * The discrete controls (dock / locate / spot-clean / follow / flash / sounds)
32376
+ * are a DATA-DRIVEN dictionary the cap exposes via `listActions()`. Each entry
32377
+ * carries `{ id, kind, label, icon }` (plus `soundId` for sound entries) so the
32378
+ * native panel AND the PTZ mimic render buttons WITHOUT hardcoding a
32379
+ * vendor-specific list. `kind: 'action'` entries are triggered with
32380
+ * `runAction({ actionId })`; `kind: 'sound'` entries with `playSound({ soundId })`
32381
+ * (the entry carries the `soundId` to pass). The general primitives — `move`,
32382
+ * `stop`, `goToPoint` — stay as first-class methods, not dictionary entries.
32383
+ *
32384
+ * NOTE (provider wiring): the first provider (Dreame) drives this with the RAW
32385
+ * `callAction(siid,aiid,in)` / `setProperty({siid,piid,value})` MIoT primitives
32386
+ * that the currently-published `@apocaliss92/nodedreame` already exposes on
32387
+ * every device handle. A future nodedreame publish adds a typed
32388
+ * `DreameCameraController` (drive / playPetSound / spotClean / findPet / …); the
32389
+ * provider can then swap the raw calls for the typed methods with no change to
32390
+ * THIS contract.
32391
+ */
32392
+ /**
32393
+ * A momentary drive nudge. The robot MOVES (no gimbal) for as long as the caller
32394
+ * keeps sending nudges (~1 Hz); an explicit `stop` (or letting the nudges lapse)
32395
+ * halts it.
32396
+ *
32397
+ * - `pan` — turn: negative = left, positive = right, 0 = straight.
32398
+ * - `tilt` — throttle: positive = forward, negative = spin / turn-around.
32399
+ * - `speed` — optional intensity hint [0, 1]; the provider may scale the drive
32400
+ * vector by it (drivers without proportional drive ignore it).
32401
+ *
32402
+ * `pan` / `tilt` are normalized [-1, 1]. Both optional so a caller can nudge one
32403
+ * axis alone; an all-undefined nudge is a no-op.
32404
+ */
32405
+ var NavigationMoveCommandSchema = object({
32406
+ pan: number().min(-1).max(1).optional(),
32407
+ tilt: number().min(-1).max(1).optional(),
32408
+ speed: number().min(0).max(1).optional()
32409
+ });
32410
+ /**
32411
+ * The enumerated discrete actions a navigation-capable robot can perform via
32412
+ * `runAction`. This is the CLOSED vocabulary; a given device advertises the
32413
+ * subset it supports through `listActions`. Sounds are NOT here — they go through
32414
+ * `playSound` (see the `sound` dictionary entries).
32415
+ */
32416
+ var NavigationActionIdSchema = _enum([
32417
+ "goHome",
32418
+ "locate",
32419
+ "spotClean",
32420
+ "findPet",
32421
+ "personFollow",
32422
+ "stop",
32423
+ "startClean",
32424
+ "pauseClean",
32425
+ "dockWash",
32426
+ "autoEmpty",
32427
+ "flashOn",
32428
+ "flashOff"
32429
+ ]);
32430
+ /** Whether a dictionary entry is a `runAction` action or a `playSound` sound. */
32431
+ var NavigationEntryKindSchema = _enum(["action", "sound"]);
32432
+ /**
32433
+ * One entry in the navigation action dictionary — the DATA-DRIVEN unit both the
32434
+ * native panel and the PTZ mimic render as a button.
32435
+ *
32436
+ * - `id` — stable id. For `kind:'action'` it is a {@link NavigationActionId}
32437
+ * (pass to `runAction`); for `kind:'sound'` it is a namespaced id
32438
+ * (`sound:meow`) whose `soundId` is passed to `playSound`.
32439
+ * - `icon` — icon HINT (lucide-style name; the UI maps it to its own set).
32440
+ * - `label` — operator-facing English label.
32441
+ * - `soundId` — wire sound id, present only on `kind:'sound'` entries.
32442
+ * - `enabled` — per-device FEATURE FLAG. `listActions` reports it so the UI /
32443
+ * PTZ render ONLY enabled entries. Data-driven: the provider
32444
+ * flips it from config, never by editing code.
32445
+ */
32446
+ var NavigationActionEntrySchema = object({
32447
+ id: string(),
32448
+ kind: NavigationEntryKindSchema,
32449
+ label: string(),
32450
+ icon: string(),
32451
+ /** Present only on `kind:'sound'` entries — the id to pass to `playSound`. */
32452
+ soundId: number().int().optional(),
32453
+ /** Per-device feature flag — render this entry only when true. */
32454
+ enabled: boolean()
32455
+ });
32456
+ /** Coordinates for `goToPoint` — a point on the robot's live map. */
32457
+ var NavigationPointSchema = object({
32458
+ x: number(),
32459
+ y: number()
32460
+ });
32461
+ /**
32462
+ * Per-device FEATURE-FLAG report for the general (non-dictionary) primitives.
32463
+ * The cap reports which are enabled so the UI / PTZ render only the controls
32464
+ * that are turned on for THIS device. Data-driven: the provider derives these
32465
+ * from config + probe, never hardcoded in the UI. The per-DICTIONARY-entry flags
32466
+ * live on {@link NavigationActionEntrySchema.enabled}; these gate the primitives
32467
+ * that are not dictionary entries.
32468
+ *
32469
+ * - `move` / `stop` — the momentary drive joystick.
32470
+ * - `goToPoint` — send-to-map-coordinate. Ships OFF on Dreame until the
32471
+ * map-coordinate plumbing is wired.
32472
+ * - `runAction` — the discrete action buttons (dictionary `kind:'action'`).
32473
+ * - `playSound` — the sound buttons (dictionary `kind:'sound'`).
32474
+ * - `light` — the on/off fill-light toggle (works anytime).
32475
+ * - `lightMode` — the auto/manual selector + manual level slider (a
32476
+ * camera-service control; needs an active stream).
32477
+ */
32478
+ var NavigationFeaturesSchema = object({
32479
+ move: boolean(),
32480
+ stop: boolean(),
32481
+ goToPoint: boolean(),
32482
+ runAction: boolean(),
32483
+ playSound: boolean(),
32484
+ light: boolean(),
32485
+ lightMode: boolean()
32486
+ });
32487
+ /** Light mode: `auto` lets the camera choose brightness; `manual` uses `level`. */
32488
+ var NavigationLightModeSchema = _enum(["auto", "manual"]);
32489
+ /**
32490
+ * Live navigation state so the UI can reflect what the robot is doing:
32491
+ * - `mode` — coarse activity (idle / cleaning / following / …).
32492
+ * - `following` — person/pet follow is currently armed.
32493
+ * - `flash` — the on-camera fill light is on.
32494
+ * - `lightMode` — auto vs manual fill-light mode.
32495
+ * - `lightLevel` — manual fill-light level (40..100); meaningful when
32496
+ * `lightMode === 'manual'`.
32497
+ */
32498
+ var NavigationStatusSchema = object({
32499
+ mode: _enum([
32500
+ "idle",
32501
+ "cleaning",
32502
+ "spot",
32503
+ "following",
32504
+ "goto",
32505
+ "returning",
32506
+ "paused",
32507
+ "unknown"
32508
+ ]),
32509
+ following: boolean(),
32510
+ flash: boolean(),
32511
+ lightMode: NavigationLightModeSchema,
32512
+ lightLevel: number().min(40).max(100),
32513
+ /** Ms epoch when the slice was last updated. */
32514
+ lastChangedAt: number()
32515
+ });
32516
+ /**
32517
+ * Runtime-state slice owned by this cap (kernel-managed: validated, mirrored,
32518
+ * observable). Adds `lastFetchedAt` on top of the status shape per the
32519
+ * convention.
32520
+ */
32521
+ var NavigationRuntimeStateSchema = NavigationStatusSchema.extend({ lastFetchedAt: number() });
32522
+ var navigationCapability = {
32523
+ name: "navigation",
32524
+ scope: "device",
32525
+ deviceNative: true,
32526
+ mode: "singleton",
32527
+ deviceTypes: [DeviceType.Camera],
32528
+ deviceConfig: { ui: {
32529
+ kind: "widget",
32530
+ widgetId: "host/navigation-panel",
32531
+ tab: "navigation",
32532
+ topTab: true,
32533
+ label: "Navigation",
32534
+ order: 0
32535
+ } },
32536
+ methods: {
32537
+ /**
32538
+ * Momentary drive nudge (the robot moves). `protected` — mirrors
32539
+ * `ptz.continuousMove` so the Viewer navigation panel (and the PTZ-mimic
32540
+ * path) works for any authenticated user, not admin-only. The UI sends
32541
+ * these at ~1 Hz while a control is held; the provider forwards each one to
32542
+ * a single drive write WITHOUT debouncing.
32543
+ */
32544
+ move: method(NavigationMoveCommandSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
32545
+ /** Halt all motion immediately (zero drive vector). */
32546
+ stop: method(object({ deviceId: number() }), _void(), { kind: "mutation" }),
32547
+ /** Send the robot to a point on its live map. */
32548
+ goToPoint: method(NavigationPointSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
32549
+ /**
32550
+ * Enumerate the discrete controls THIS device supports (data-driven UI +
32551
+ * PTZ mimic). Camera-probed subset of {@link NAVIGATION_ACTION_CATALOG}.
32552
+ */
32553
+ listActions: method(object({ deviceId: number() }), array(NavigationActionEntrySchema)),
32554
+ /**
32555
+ * Run one discrete action (a `kind:'action'` dictionary entry). Invalid /
32556
+ * unsupported action ids are rejected by the provider.
32557
+ */
32558
+ runAction: method(object({
32559
+ deviceId: number(),
32560
+ actionId: NavigationActionIdSchema
32561
+ }), _void(), { kind: "mutation" }),
32562
+ /** Play a sound by its wire id (the `soundId` of a `kind:'sound'` entry). */
32563
+ playSound: method(object({
32564
+ deviceId: number(),
32565
+ soundId: number().int()
32566
+ }), _void(), { kind: "mutation" }),
32567
+ /**
32568
+ * Turn the on-camera fill light on / off (the `OpenFullLight` control —
32569
+ * works anytime, no active stream required).
32570
+ */
32571
+ setLightOn: method(object({
32572
+ deviceId: number(),
32573
+ on: boolean()
32574
+ }), _void(), { kind: "mutation" }),
32575
+ /**
32576
+ * Set the fill-light mode (auto vs manual). `manual` optionally carries the
32577
+ * initial `level`. The auto/manual + level control is a CAMERA-service
32578
+ * action that generally needs an active camera stream/monitor session — the
32579
+ * UI shows the manual level slider ONLY when `mode === 'manual'`.
32580
+ */
32581
+ setLightMode: method(object({
32582
+ deviceId: number(),
32583
+ mode: NavigationLightModeSchema,
32584
+ level: number().min(40).max(100).optional()
32585
+ }), _void(), { kind: "mutation" }),
32586
+ /** Set the MANUAL fill-light level (40..100). Implies `manual` mode. */
32587
+ setLightLevel: method(object({
32588
+ deviceId: number(),
32589
+ level: number().min(40).max(100)
32590
+ }), _void(), { kind: "mutation" }),
32591
+ /**
32592
+ * Per-device FEATURE-FLAG report for the general primitives — drives which
32593
+ * controls the UI shows (the per-entry flags for the dictionary come back on
32594
+ * `listActions`).
32595
+ */
32596
+ getFeatures: method(object({ deviceId: number() }), NavigationFeaturesSchema)
32597
+ },
32598
+ events: { onStatusChanged: { data: object({
32599
+ deviceId: number(),
32600
+ status: NavigationStatusSchema
32601
+ }) } },
32602
+ status: {
32603
+ schema: NavigationStatusSchema,
32604
+ kind: "push"
32605
+ },
32606
+ /**
32607
+ * Runtime-state slice mirrored by the kernel. The navigation panel watches it
32608
+ * for live mode / follow / flash changes.
32609
+ */
32610
+ runtimeState: NavigationRuntimeStateSchema,
32611
+ /**
32612
+ * Runtime-state durability: **session** — like `vacuum-control`, a restored
32613
+ * `mode: cleaning` / `following: true` is a robot that is not actually doing
32614
+ * that. The live handle re-publishes on connect.
32615
+ *
32616
+ * See `RuntimeStateDurability`. Enforced by
32617
+ * `scripts/check-runtime-state-durability.ts`.
32618
+ */
32619
+ durability: "session"
32620
+ };
32621
+ /**
32622
+ * Network-link snapshot. Same shape for every provider (a Reolink wifi
32623
+ * camera, a Home Assistant device with a signal-strength sensor, a Tapo
32624
+ * plug): one slice under `device.runtimeState['network-link']`, one badge,
32625
+ * one Home Assistant projection.
32626
+ */
32627
+ var NetworkLinkStatusSchema = object({
32628
+ /** The link the device is on. `'unknown'` = not read yet, not "no link". */
32629
+ type: _enum([
32630
+ "wifi",
32631
+ "ethernet",
32632
+ "cellular",
32633
+ "unknown"
32634
+ ]),
32635
+ /**
32636
+ * Link quality, 0..100 inclusive, normalised by the provider from whatever
32637
+ * the firmware reports (bars, RSSI, a vendor scale). **`null` means NOT
32638
+ * KNOWN or NOT APPLICABLE** — a wired link has no signal, and a wireless
32639
+ * one whose reading has not landed must not be drawn at 0 %. Consumers
32640
+ * SKIP a null rather than coerce it.
32641
+ */
32642
+ signalPercent: number().min(0).max(100).nullable(),
32643
+ /** Raw received signal strength in dBm, when the firmware reports one. */
32644
+ rssiDbm: number().optional(),
32645
+ /** Network name of a wireless link, when the firmware reports it. */
32646
+ ssid: string().optional(),
32647
+ /** Ms epoch of the last observation. Lets consumers reason about freshness. */
32648
+ lastUpdated: number()
32649
+ });
32650
+ var networkLinkCapability = {
32651
+ name: "network-link",
32652
+ scope: "device",
32653
+ deviceNative: true,
32654
+ mode: "singleton",
32655
+ deviceTypes: [
32656
+ DeviceType.Camera,
32657
+ DeviceType.Sensor,
32658
+ DeviceType.Button,
32659
+ DeviceType.Switch,
32660
+ DeviceType.Light,
32661
+ DeviceType.Lock,
32662
+ DeviceType.Siren
32663
+ ],
32664
+ methods: {},
32665
+ events: {
32666
+ /**
32667
+ * Emitted whenever the cached status changes (a link switch, a signal
32668
+ * reading that moved). Mirrored on the parent chain by the
32669
+ * DeviceEventPropagator like `battery.onStatusChanged`.
32670
+ */
32671
+ onStatusChanged: { data: object({
32672
+ deviceId: number(),
32673
+ status: NetworkLinkStatusSchema
32674
+ }) } },
32675
+ status: {
32676
+ schema: NetworkLinkStatusSchema,
32677
+ kind: "push",
32678
+ empty: {
32679
+ type: "unknown",
32680
+ signalPercent: null,
32681
+ lastUpdated: 0
32682
+ }
32683
+ },
32684
+ /**
32685
+ * Runtime-state slice — every provider stores the same shape under
32686
+ * `device.runtimeState['network-link']`, read once by the badge and the
32687
+ * Home Assistant projector regardless of the driver.
32688
+ */
32689
+ runtimeState: NetworkLinkStatusSchema,
32690
+ /**
32691
+ * Runtime-state durability: **restored** — a link reading is slow to
32692
+ * change and a sleeping battery camera may not report for hours; the
32693
+ * restored slice is what the badge shows until the next read.
32694
+ *
32695
+ * See `RuntimeStateDurability`. Enforced by
32696
+ * `scripts/check-runtime-state-durability.ts`.
32697
+ */
32698
+ durability: "restored",
32699
+ /** Clock fields: written, but excluded from the compare that decides
32700
+ * whether persisting is worth a SQLite commit. */
32701
+ volatileStateFields: ["lastUpdated"]
32702
+ };
32703
+ /**
32271
32704
  * network-quality — system-scoped singleton capability tracking RTT,
32272
32705
  * jitter, and observed/peak bandwidth per device + per client.
32273
32706
  *
@@ -34081,287 +34514,6 @@ var ptzAutotrackCapability = {
34081
34514
  durability: "session"
34082
34515
  };
34083
34516
  /**
34084
- * `navigation` — a device-scoped capability that natively expresses the FULL
34085
- * navigation / action surface of a robot that DRIVES ITSELF and carries an
34086
- * on-board camera (the Dreame robot-vacuum camera is the first provider).
34087
- *
34088
- * Why a NEW cap rather than overloading `ptz`:
34089
- * - `ptz` moves a *gimbal* on a fixed camera (pan/tilt/zoom of the lens). A
34090
- * robot vacuum has no gimbal — the whole chassis drives, turns and spins.
34091
- * The two are different physical models: PTZ is absolute-position + presets,
34092
- * navigation is momentary drive nudges + discrete robot ACTIONS
34093
- * (dock / spot-clean / follow-pet / go-to-point / …).
34094
- * - This cap is the SOURCE OF TRUTH. Two consumers adapt from it rather than
34095
- * the reverse:
34096
- * 1. a native CamStack navigation panel (data-driven from `listActions`
34097
- * / `getOptions`), and
34098
- * 2. the PTZ surface — a thin adapter mimics `ptz` from `navigation` so a
34099
- * robot camera shows up in the existing PTZ control path without every
34100
- * PTZ provider learning about robots. The mapping lives in the adapter,
34101
- * not here (see the addon design note):
34102
- * ptz.continuousMove({pan,tilt}) → navigation.move({pan,tilt})
34103
- * ptz.stop() → navigation.stop()
34104
- * ptz.goHome() → navigation.runAction('goHome')
34105
- * ptz.getPresets() → navigation.listActions() (id→preset)
34106
- * ptz.goToPreset(id) → navigation.runAction(id)
34107
- *
34108
- * ## Continuous drive
34109
- *
34110
- * `move` is MOMENTARY. Fluid navigation comes from the UI (or the PTZ adapter)
34111
- * sending `move({pan,tilt})` REPEATEDLY at ~1 Hz while a direction is held, and
34112
- * one `stop()` on release — exactly like the robot app's remote-drive joystick.
34113
- * The provider forwards EACH `move` to one drive write; it must NOT debounce or
34114
- * coalesce them. The UI owns the cadence.
34115
- *
34116
- * ## The action dictionary
34117
- *
34118
- * The discrete controls (dock / locate / spot-clean / follow / flash / sounds)
34119
- * are a DATA-DRIVEN dictionary the cap exposes via `listActions()`. Each entry
34120
- * carries `{ id, kind, label, icon }` (plus `soundId` for sound entries) so the
34121
- * native panel AND the PTZ mimic render buttons WITHOUT hardcoding a
34122
- * vendor-specific list. `kind: 'action'` entries are triggered with
34123
- * `runAction({ actionId })`; `kind: 'sound'` entries with `playSound({ soundId })`
34124
- * (the entry carries the `soundId` to pass). The general primitives — `move`,
34125
- * `stop`, `goToPoint` — stay as first-class methods, not dictionary entries.
34126
- *
34127
- * NOTE (provider wiring): the first provider (Dreame) drives this with the RAW
34128
- * `callAction(siid,aiid,in)` / `setProperty({siid,piid,value})` MIoT primitives
34129
- * that the currently-published `@apocaliss92/nodedreame` already exposes on
34130
- * every device handle. A future nodedreame publish adds a typed
34131
- * `DreameCameraController` (drive / playPetSound / spotClean / findPet / …); the
34132
- * provider can then swap the raw calls for the typed methods with no change to
34133
- * THIS contract.
34134
- */
34135
- /**
34136
- * A momentary drive nudge. The robot MOVES (no gimbal) for as long as the caller
34137
- * keeps sending nudges (~1 Hz); an explicit `stop` (or letting the nudges lapse)
34138
- * halts it.
34139
- *
34140
- * - `pan` — turn: negative = left, positive = right, 0 = straight.
34141
- * - `tilt` — throttle: positive = forward, negative = spin / turn-around.
34142
- * - `speed` — optional intensity hint [0, 1]; the provider may scale the drive
34143
- * vector by it (drivers without proportional drive ignore it).
34144
- *
34145
- * `pan` / `tilt` are normalized [-1, 1]. Both optional so a caller can nudge one
34146
- * axis alone; an all-undefined nudge is a no-op.
34147
- */
34148
- var NavigationMoveCommandSchema = object({
34149
- pan: number().min(-1).max(1).optional(),
34150
- tilt: number().min(-1).max(1).optional(),
34151
- speed: number().min(0).max(1).optional()
34152
- });
34153
- /**
34154
- * The enumerated discrete actions a navigation-capable robot can perform via
34155
- * `runAction`. This is the CLOSED vocabulary; a given device advertises the
34156
- * subset it supports through `listActions`. Sounds are NOT here — they go through
34157
- * `playSound` (see the `sound` dictionary entries).
34158
- */
34159
- var NavigationActionIdSchema = _enum([
34160
- "goHome",
34161
- "locate",
34162
- "spotClean",
34163
- "findPet",
34164
- "personFollow",
34165
- "stop",
34166
- "startClean",
34167
- "pauseClean",
34168
- "dockWash",
34169
- "autoEmpty",
34170
- "flashOn",
34171
- "flashOff"
34172
- ]);
34173
- /** Whether a dictionary entry is a `runAction` action or a `playSound` sound. */
34174
- var NavigationEntryKindSchema = _enum(["action", "sound"]);
34175
- /**
34176
- * One entry in the navigation action dictionary — the DATA-DRIVEN unit both the
34177
- * native panel and the PTZ mimic render as a button.
34178
- *
34179
- * - `id` — stable id. For `kind:'action'` it is a {@link NavigationActionId}
34180
- * (pass to `runAction`); for `kind:'sound'` it is a namespaced id
34181
- * (`sound:meow`) whose `soundId` is passed to `playSound`.
34182
- * - `icon` — icon HINT (lucide-style name; the UI maps it to its own set).
34183
- * - `label` — operator-facing English label.
34184
- * - `soundId` — wire sound id, present only on `kind:'sound'` entries.
34185
- * - `enabled` — per-device FEATURE FLAG. `listActions` reports it so the UI /
34186
- * PTZ render ONLY enabled entries. Data-driven: the provider
34187
- * flips it from config, never by editing code.
34188
- */
34189
- var NavigationActionEntrySchema = object({
34190
- id: string(),
34191
- kind: NavigationEntryKindSchema,
34192
- label: string(),
34193
- icon: string(),
34194
- /** Present only on `kind:'sound'` entries — the id to pass to `playSound`. */
34195
- soundId: number().int().optional(),
34196
- /** Per-device feature flag — render this entry only when true. */
34197
- enabled: boolean()
34198
- });
34199
- /** Coordinates for `goToPoint` — a point on the robot's live map. */
34200
- var NavigationPointSchema = object({
34201
- x: number(),
34202
- y: number()
34203
- });
34204
- /**
34205
- * Per-device FEATURE-FLAG report for the general (non-dictionary) primitives.
34206
- * The cap reports which are enabled so the UI / PTZ render only the controls
34207
- * that are turned on for THIS device. Data-driven: the provider derives these
34208
- * from config + probe, never hardcoded in the UI. The per-DICTIONARY-entry flags
34209
- * live on {@link NavigationActionEntrySchema.enabled}; these gate the primitives
34210
- * that are not dictionary entries.
34211
- *
34212
- * - `move` / `stop` — the momentary drive joystick.
34213
- * - `goToPoint` — send-to-map-coordinate. Ships OFF on Dreame until the
34214
- * map-coordinate plumbing is wired.
34215
- * - `runAction` — the discrete action buttons (dictionary `kind:'action'`).
34216
- * - `playSound` — the sound buttons (dictionary `kind:'sound'`).
34217
- * - `light` — the on/off fill-light toggle (works anytime).
34218
- * - `lightMode` — the auto/manual selector + manual level slider (a
34219
- * camera-service control; needs an active stream).
34220
- */
34221
- var NavigationFeaturesSchema = object({
34222
- move: boolean(),
34223
- stop: boolean(),
34224
- goToPoint: boolean(),
34225
- runAction: boolean(),
34226
- playSound: boolean(),
34227
- light: boolean(),
34228
- lightMode: boolean()
34229
- });
34230
- /** Light mode: `auto` lets the camera choose brightness; `manual` uses `level`. */
34231
- var NavigationLightModeSchema = _enum(["auto", "manual"]);
34232
- /**
34233
- * Live navigation state so the UI can reflect what the robot is doing:
34234
- * - `mode` — coarse activity (idle / cleaning / following / …).
34235
- * - `following` — person/pet follow is currently armed.
34236
- * - `flash` — the on-camera fill light is on.
34237
- * - `lightMode` — auto vs manual fill-light mode.
34238
- * - `lightLevel` — manual fill-light level (40..100); meaningful when
34239
- * `lightMode === 'manual'`.
34240
- */
34241
- var NavigationStatusSchema = object({
34242
- mode: _enum([
34243
- "idle",
34244
- "cleaning",
34245
- "spot",
34246
- "following",
34247
- "goto",
34248
- "returning",
34249
- "paused",
34250
- "unknown"
34251
- ]),
34252
- following: boolean(),
34253
- flash: boolean(),
34254
- lightMode: NavigationLightModeSchema,
34255
- lightLevel: number().min(40).max(100),
34256
- /** Ms epoch when the slice was last updated. */
34257
- lastChangedAt: number()
34258
- });
34259
- /**
34260
- * Runtime-state slice owned by this cap (kernel-managed: validated, mirrored,
34261
- * observable). Adds `lastFetchedAt` on top of the status shape per the
34262
- * convention.
34263
- */
34264
- var NavigationRuntimeStateSchema = NavigationStatusSchema.extend({ lastFetchedAt: number() });
34265
- var navigationCapability = {
34266
- name: "navigation",
34267
- scope: "device",
34268
- deviceNative: true,
34269
- mode: "singleton",
34270
- deviceTypes: [DeviceType.Camera],
34271
- deviceConfig: { ui: {
34272
- kind: "widget",
34273
- widgetId: "host/navigation-panel",
34274
- tab: "navigation",
34275
- topTab: true,
34276
- label: "Navigation",
34277
- order: 0
34278
- } },
34279
- methods: {
34280
- /**
34281
- * Momentary drive nudge (the robot moves). `protected` — mirrors
34282
- * `ptz.continuousMove` so the Viewer navigation panel (and the PTZ-mimic
34283
- * path) works for any authenticated user, not admin-only. The UI sends
34284
- * these at ~1 Hz while a control is held; the provider forwards each one to
34285
- * a single drive write WITHOUT debouncing.
34286
- */
34287
- move: method(NavigationMoveCommandSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
34288
- /** Halt all motion immediately (zero drive vector). */
34289
- stop: method(object({ deviceId: number() }), _void(), { kind: "mutation" }),
34290
- /** Send the robot to a point on its live map. */
34291
- goToPoint: method(NavigationPointSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
34292
- /**
34293
- * Enumerate the discrete controls THIS device supports (data-driven UI +
34294
- * PTZ mimic). Camera-probed subset of {@link NAVIGATION_ACTION_CATALOG}.
34295
- */
34296
- listActions: method(object({ deviceId: number() }), array(NavigationActionEntrySchema)),
34297
- /**
34298
- * Run one discrete action (a `kind:'action'` dictionary entry). Invalid /
34299
- * unsupported action ids are rejected by the provider.
34300
- */
34301
- runAction: method(object({
34302
- deviceId: number(),
34303
- actionId: NavigationActionIdSchema
34304
- }), _void(), { kind: "mutation" }),
34305
- /** Play a sound by its wire id (the `soundId` of a `kind:'sound'` entry). */
34306
- playSound: method(object({
34307
- deviceId: number(),
34308
- soundId: number().int()
34309
- }), _void(), { kind: "mutation" }),
34310
- /**
34311
- * Turn the on-camera fill light on / off (the `OpenFullLight` control —
34312
- * works anytime, no active stream required).
34313
- */
34314
- setLightOn: method(object({
34315
- deviceId: number(),
34316
- on: boolean()
34317
- }), _void(), { kind: "mutation" }),
34318
- /**
34319
- * Set the fill-light mode (auto vs manual). `manual` optionally carries the
34320
- * initial `level`. The auto/manual + level control is a CAMERA-service
34321
- * action that generally needs an active camera stream/monitor session — the
34322
- * UI shows the manual level slider ONLY when `mode === 'manual'`.
34323
- */
34324
- setLightMode: method(object({
34325
- deviceId: number(),
34326
- mode: NavigationLightModeSchema,
34327
- level: number().min(40).max(100).optional()
34328
- }), _void(), { kind: "mutation" }),
34329
- /** Set the MANUAL fill-light level (40..100). Implies `manual` mode. */
34330
- setLightLevel: method(object({
34331
- deviceId: number(),
34332
- level: number().min(40).max(100)
34333
- }), _void(), { kind: "mutation" }),
34334
- /**
34335
- * Per-device FEATURE-FLAG report for the general primitives — drives which
34336
- * controls the UI shows (the per-entry flags for the dictionary come back on
34337
- * `listActions`).
34338
- */
34339
- getFeatures: method(object({ deviceId: number() }), NavigationFeaturesSchema)
34340
- },
34341
- events: { onStatusChanged: { data: object({
34342
- deviceId: number(),
34343
- status: NavigationStatusSchema
34344
- }) } },
34345
- status: {
34346
- schema: NavigationStatusSchema,
34347
- kind: "push"
34348
- },
34349
- /**
34350
- * Runtime-state slice mirrored by the kernel. The navigation panel watches it
34351
- * for live mode / follow / flash changes.
34352
- */
34353
- runtimeState: NavigationRuntimeStateSchema,
34354
- /**
34355
- * Runtime-state durability: **session** — like `vacuum-control`, a restored
34356
- * `mode: cleaning` / `following: true` is a robot that is not actually doing
34357
- * that. The live handle re-publishes on connect.
34358
- *
34359
- * See `RuntimeStateDurability`. Enforced by
34360
- * `scripts/check-runtime-state-durability.ts`.
34361
- */
34362
- durability: "session"
34363
- };
34364
- /**
34365
34517
  * reboot — device-scoped capability for "soft" device reboots (firmware
34366
34518
  * reboot via vendor protocol; cameras, NVRs, doorbells). Surfaces a
34367
34519
  * single mutation so the UI can offer a confirm-and-reboot button for
@@ -37997,6 +38149,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
37997
38149
  storageCapability,
37998
38150
  storageEvictableCapability,
37999
38151
  storageMigrationCapability,
38152
+ storageOccupancyCapability,
38000
38153
  storageProviderCapability,
38001
38154
  streamBrokerCapability,
38002
38155
  streamCatalogCapability,
@@ -42963,13 +43116,13 @@ Object.freeze({
42963
43116
  addonId: null,
42964
43117
  access: "view"
42965
43118
  },
42966
- "storage.getDefaultLocation": {
43119
+ "storage.list": {
42967
43120
  capName: "storage",
42968
43121
  capScope: "system",
42969
43122
  addonId: null,
42970
43123
  access: "view"
42971
43124
  },
42972
- "storage.list": {
43125
+ "storage.listDrainProgress": {
42973
43126
  capName: "storage",
42974
43127
  capScope: "system",
42975
43128
  addonId: null,
@@ -43119,6 +43272,12 @@ Object.freeze({
43119
43272
  addonId: null,
43120
43273
  access: "view"
43121
43274
  },
43275
+ "storageOccupancy.getOccupancy": {
43276
+ capName: "storage-occupancy",
43277
+ capScope: "system",
43278
+ addonId: null,
43279
+ access: "view"
43280
+ },
43122
43281
  "storageProvider.abortUpload": {
43123
43282
  capName: "storage-provider",
43124
43283
  capScope: "system",