@camstack/system 1.2.201 → 1.2.203

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +12 -0
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +37 -3
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +37 -3
  10. package/dist/builtins/console-logging/index.js +1 -1
  11. package/dist/builtins/console-logging/index.mjs +1 -1
  12. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  13. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  14. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  15. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  16. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  17. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  18. package/dist/builtins/hub-forwarder/index.js +1 -1
  19. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  20. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  21. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  22. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  23. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  24. package/dist/builtins/local-network/local-network.addon.js +1 -1
  25. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  26. package/dist/builtins/loki-logging/index.js +1 -1
  27. package/dist/builtins/loki-logging/index.mjs +1 -1
  28. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  29. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  30. package/dist/builtins/platform-probe/index.js +1 -1
  31. package/dist/builtins/platform-probe/index.mjs +1 -1
  32. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  33. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  34. package/dist/builtins/snapshot/index.js +1 -1
  35. package/dist/builtins/snapshot/index.mjs +1 -1
  36. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  37. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  38. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  39. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  40. package/dist/builtins/storage-orchestrator/drain-ratchet.d.ts +104 -0
  41. package/dist/builtins/storage-orchestrator/drain-state-store.d.ts +13 -0
  42. package/dist/builtins/storage-orchestrator/location-occupancy-slice.d.ts +47 -0
  43. package/dist/builtins/storage-orchestrator/location-protection.d.ts +20 -0
  44. package/dist/builtins/storage-orchestrator/location-root-collision.d.ts +44 -0
  45. package/dist/builtins/storage-orchestrator/location-store.d.ts +12 -3
  46. package/dist/builtins/storage-orchestrator/storage-migration-coordinator.d.ts +9 -9
  47. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +47 -0
  48. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +865 -157
  49. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +865 -157
  50. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +75 -31
  51. package/dist/builtins/storage-orchestrator/storage-pressure-manager.d.ts +45 -0
  52. package/dist/builtins/system-config/system-config.addon.js +1 -1
  53. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  54. package/dist/builtins/winston-logging/index.js +1 -1
  55. package/dist/builtins/winston-logging/index.mjs +1 -1
  56. package/dist/{dist-CcV3k-hD.js → dist-BFvWv_bz.js} +933 -546
  57. package/dist/{dist-BmJNnb_s.mjs → dist-DcMUcdeq.mjs} +862 -547
  58. package/dist/index.js +1 -1
  59. package/dist/index.mjs +1 -1
  60. package/dist/{retired-settings-keys-nLVKlhe-.mjs → retired-settings-keys-CJfgnUSb.mjs} +1 -1
  61. package/dist/{retired-settings-keys-Bd8fuwO1.js → retired-settings-keys-Dd7K3K3r.js} +1 -1
  62. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  let zod = require("zod");
2
- //#region ../types/dist/event-category-zAv7pMUz.mjs
2
+ //#region ../types/dist/event-category-CnLqLOKs.mjs
3
3
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4
4
  EventCategory["SystemBoot"] = "system.boot";
5
5
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -194,6 +194,19 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
194
194
  EventCategory["ProcessCrashed"] = "process.crashed";
195
195
  EventCategory["ProcessRestartScheduled"] = "process.restart_scheduled";
196
196
  EventCategory["ProcessRestarted"] = "process.restarted";
197
+ /**
198
+ * The SET of storage locations changed — one was created, edited, enabled,
199
+ * disabled or deleted through `storage.upsertLocation` / `deleteLocation`.
200
+ *
201
+ * Telemetry, not a transaction (D8/D11): every consumer that re-resolves on
202
+ * it must also converge on its own periodic path, because a dropped event
203
+ * must not leave a node writing to yesterday's disk set forever. It exists
204
+ * because there was NO signal at all — an operator who added a second
205
+ * recordings disk in the admin UI got nothing, and the recorder kept its
206
+ * resolved locations until something else happened to re-resolve them
207
+ * (D387). Payload `StorageLocationsChangedPayload`.
208
+ */
209
+ EventCategory["StorageLocationsChanged"] = "storage.locations-changed";
197
210
  EventCategory["RecordingStarted"] = "recording.started";
198
211
  EventCategory["RecordingStopped"] = "recording.stopped";
199
212
  EventCategory["RecordingError"] = "recording.error";
@@ -3815,111 +3828,6 @@ function logLevelAtMost(level, threshold) {
3815
3828
  return LOG_LEVEL_RANK[level] <= LOG_LEVEL_RANK[threshold];
3816
3829
  }
3817
3830
  /**
3818
- * Per-component log CHANNELS — the gate a hot path consults, and the registry
3819
- * an addon declares its channels in.
3820
- *
3821
- * ## Two axes, deliberately separated
3822
- *
3823
- * - **DECLARATION** — which channels exist. Only the addon knows:
3824
- * `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
3825
- * baichuan/handshake. A hand-wired central list rots at the first addition,
3826
- * and rots silently. So a channel is declared where it is consulted, and the
3827
- * `log-channels` capability enumerates the declarations.
3828
- * - **VALUE** — at which level, for which scope, until when. That stays ONE
3829
- * thing: the logging settings document on the `system` cap. Two authorities
3830
- * over the values is the exact defect
3831
- * `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
3832
- * remove; re-introducing it from the cure side would be grotesque.
3833
- *
3834
- * Nothing in this file reads a clock, an env var or a store. The registry is
3835
- * a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
3836
- * the hot path with a value somebody actually read, and by
3837
- * {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
3838
- * never reaches here, so it can neither disarm an armed channel nor arm a
3839
- * disarmed one (D49).
3840
- *
3841
- * ## The canonical call shape
3842
- *
3843
- * ```ts
3844
- * if (CH_RTP.on && CH_RTP.wants(deviceId)) {
3845
- * CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
3846
- * }
3847
- * ```
3848
- *
3849
- * `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
3850
- * read. Disarmed, a call site costs one load and one branch, and the `extras`
3851
- * object literal is never constructed because it lives inside the branch. It
3852
- * is the same shape already proven in production at `stream-broker.ts:1650`,
3853
- * and the same discipline `LoggingGate.allowsDestination` uses for the
3854
- * destination floor (measured at 1.93 ns/call when off).
3855
- *
3856
- * ## Why a channel emits at `info`
3857
- *
3858
- * `loki-logging.addon.ts` pins the destination default at `info` and
3859
- * `loki-destination.ts` drops everything below it, so a line emitted at
3860
- * `debug` never reaches Loki and the hub's in-memory ring only holds ~35
3861
- * minutes. A diagnostic that cannot be read an hour later is worse than no
3862
- * diagnostic, because it looks done. {@link LogChannelGate.log} therefore
3863
- * emits at the channel's declared level, whose schema floor is `info`.
3864
- */
3865
- /**
3866
- * The level a channel writes at once armed.
3867
- *
3868
- * `debug` is absent ON PURPOSE and not by omission: below `info` the line does
3869
- * not leave the process for Loki, and the whole point of arming a channel is
3870
- * to read it later.
3871
- */
3872
- var LogChannelLevelSchema = zod.z.enum([
3873
- "info",
3874
- "warn",
3875
- "error"
3876
- ]);
3877
- /**
3878
- * What an addon declares about one channel. No value, no state — a
3879
- * declaration is inert.
3880
- */
3881
- var LogChannelDescriptorSchema = zod.z.object({
3882
- /**
3883
- * Dotted `area.thing`, unique across the workspace. `area` is conventionally
3884
- * the addon's short name so an operator reading a channel list can tell who
3885
- * owns it without a second lookup.
3886
- */
3887
- name: zod.z.string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
3888
- /** One sentence: what the operator will SEE after arming it. */
3889
- description: zod.z.string().min(1),
3890
- /** The level its lines are emitted at. Never below `info`. */
3891
- defaultLevel: LogChannelLevelSchema,
3892
- /**
3893
- * Whether this channel can be narrowed to a camera.
3894
- *
3895
- * `true` is a PROMISE with two halves, and both must hold: the gate is
3896
- * consulted with the numeric device id, AND every line the channel admits
3897
- * carries `tags: { deviceId }` with that same numeric id. The second half is
3898
- * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
3899
- * keeps `deviceId` out of the stream labels for cardinality, so the tag in
3900
- * the body is the only way to filter.
3901
- *
3902
- * A channel whose lines carry the device only in `meta` (or not at all) is
3903
- * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
3904
- * the operator narrows to one camera, sees nothing, and concludes the code
3905
- * path was never taken.
3906
- */
3907
- perDevice: zod.z.boolean()
3908
- });
3909
- /**
3910
- * An armed window over one channel, as the document hands it to a mirror.
3911
- *
3912
- * A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
3913
- * expires by itself, which is the one failure a boolean cannot avoid.
3914
- */
3915
- var LogChannelWindowSchema = zod.z.object({
3916
- channel: zod.z.string().min(1),
3917
- /** Epoch ms the window closes at. */
3918
- armedUntilMs: zod.z.number(),
3919
- /** `null` = every camera. A non-empty list narrows to those numeric ids. */
3920
- deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable()
3921
- });
3922
- /**
3923
3831
  * Ops-log — the durable, append-only operations audit shared by the
3924
3832
  * recordings and events management surfaces.
3925
3833
  *
@@ -4873,6 +4781,144 @@ var StorageCleanupJobSchema = zod.z.object({
4873
4781
  });
4874
4782
  var StorageCleanupStatusInputSchema = zod.z.object({ jobId: zod.z.string().optional() });
4875
4783
  /**
4784
+ * The storage-location STATE MODEL (D385) — one typed state, one policy module.
4785
+ *
4786
+ * A location's state used to be split across two authorities: the typed
4787
+ * `enabled` field (THE write switch since D383) and an untyped `config.readOnly`
4788
+ * key. They did not mean the same thing — `enabled: false` was still evicted
4789
+ * under disk pressure while `config.readOnly` was deliberately excluded — and
4790
+ * neither name said which. Every consumer re-derived the difference, and the
4791
+ * three questions that actually matter were answered in six places.
4792
+ *
4793
+ * This module is the ONLY place in the repo allowed to interpret the state. It
4794
+ * answers three questions and nothing else:
4795
+ *
4796
+ * - may this location be WRITTEN to? {@link modeMayWrite}
4797
+ * - may this location be READ? {@link modeMayRead}
4798
+ * - what is its eviction policy? {@link evictionPolicyForMode}
4799
+ *
4800
+ * | mode | write | read | eviction |
4801
+ * | ---------- | ----- | ---- | ------------------------------ |
4802
+ * | `active` | yes | yes | `normal` (pressure + usage cap) |
4803
+ * | `readonly` | no | yes | `never` |
4804
+ * | `drain` | no | yes | `drain` (paced, until empty) |
4805
+ * | `disabled` | no | no | `never` |
4806
+ *
4807
+ * `scripts/check-storage-location-mode-single-owner.ts` fails the build when
4808
+ * anything outside this module reads `config['readOnly']` or compares `enabled`
4809
+ * directly. A rule nothing checks has already been broken somewhere.
4810
+ */
4811
+ var STORAGE_LOCATION_MODES = [
4812
+ "active",
4813
+ "readonly",
4814
+ "drain",
4815
+ "disabled"
4816
+ ];
4817
+ /**
4818
+ * The one typed state of a storage location. Authoritative Zod schema — the TS
4819
+ * alias below is `z.infer<>` of it, never a second spelling.
4820
+ */
4821
+ var StorageLocationModeSchema = zod.z.enum(STORAGE_LOCATION_MODES);
4822
+ zod.z.enum([
4823
+ "normal",
4824
+ "never",
4825
+ "drain"
4826
+ ]);
4827
+ /**
4828
+ * The legacy untyped drain key. Named ONCE, here, so the guard has exactly one
4829
+ * sanctioned reader and the string never appears anywhere else.
4830
+ */
4831
+ var LEGACY_READ_ONLY_CONFIG_KEY = "readOnly";
4832
+ /** Is this mode a write target? Only `active` is. */
4833
+ function modeMayWrite(mode) {
4834
+ return mode === "active";
4835
+ }
4836
+ /** May this mode be read (playback, timeline, scrub, relocate source)? */
4837
+ function modeMayRead(mode) {
4838
+ return mode !== "disabled";
4839
+ }
4840
+ /** What eviction may do here. See {@link StorageEvictionPolicy}. */
4841
+ function evictionPolicyForMode(mode) {
4842
+ switch (mode) {
4843
+ case "active": return "normal";
4844
+ case "drain": return "drain";
4845
+ case "readonly":
4846
+ case "disabled": return "never";
4847
+ }
4848
+ }
4849
+ /**
4850
+ * The mode a LEGACY row implies, or `null` when it implies nothing — the row is
4851
+ * already stamped, or it carried neither flag.
4852
+ *
4853
+ * Both legacy flags fold to `readonly`, which is the CONSERVATIVE direction: a
4854
+ * state change must never start deleting footage on its own, and it must never
4855
+ * make footage that was still being served disappear. `enabled: false` used to
4856
+ * leave the location evictable under pressure; folding it to `readonly` stops
4857
+ * that, which is a strictly safer answer than the one it replaces.
4858
+ */
4859
+ function legacyModeOf(location) {
4860
+ if (location.mode !== void 0) return null;
4861
+ if (location.config["readOnly"] === true) return "readonly";
4862
+ if (location.enabled === false) return "readonly";
4863
+ return null;
4864
+ }
4865
+ /**
4866
+ * The state of a location, stamped or folded. THE one interpretation: a row
4867
+ * that predates D385 is never ambiguous, and a stamped `mode` always wins over
4868
+ * whatever the legacy pair still says.
4869
+ */
4870
+ function resolveLocationMode(location) {
4871
+ return (isStorageLocationMode(location.mode) ? location.mode : void 0) ?? legacyModeOf(location) ?? "active";
4872
+ }
4873
+ /** Is this one of the four states? The stamped value crosses a wire, and a
4874
+ * value nobody defined must not be rendered as if it were a state. */
4875
+ function isStorageLocationMode(value) {
4876
+ return STORAGE_LOCATION_MODES.some((mode) => mode === value);
4877
+ }
4878
+ /** May this location be written to? */
4879
+ function mayWriteToLocation(location) {
4880
+ return modeMayWrite(resolveLocationMode(location));
4881
+ }
4882
+ /** May this location be read? A `disabled` one may not — and that is an
4883
+ * operator CHOICE, which callers must report as unavailable rather than as an
4884
+ * unknown-location fault. */
4885
+ function mayReadLocation(location) {
4886
+ return modeMayRead(resolveLocationMode(location));
4887
+ }
4888
+ /** What eviction may do to this location. */
4889
+ function evictionPolicyOfLocation(location) {
4890
+ return evictionPolicyForMode(resolveLocationMode(location));
4891
+ }
4892
+ /**
4893
+ * The config blob with the retired drain key removed. Returns the SAME object
4894
+ * when there was nothing to strip, so a caller can tell "changed" from
4895
+ * "unchanged" by identity and skip a pointless persist.
4896
+ */
4897
+ function strippedOfLegacyReadOnly(config) {
4898
+ if (!("readOnly" in config)) return config;
4899
+ const next = { ...config };
4900
+ delete next[LEGACY_READ_ONLY_CONFIG_KEY];
4901
+ return next;
4902
+ }
4903
+ /**
4904
+ * Set a location's mode, and with it everything that must agree with it.
4905
+ *
4906
+ * `enabled` stays readable for one release as a DERIVED value (`mode ===
4907
+ * 'active'`) so consumers that have not moved yet keep working, and the legacy
4908
+ * `config.readOnly` key is DELETED. This function is the only way to write
4909
+ * either, which is what makes it impossible for the two to disagree.
4910
+ *
4911
+ * Returns a new object — the input is never mutated.
4912
+ */
4913
+ function withLocationMode(location, mode) {
4914
+ return {
4915
+ ...location,
4916
+ mode,
4917
+ enabled: modeMayWrite(mode),
4918
+ config: strippedOfLegacyReadOnly(location.config)
4919
+ };
4920
+ }
4921
+ /**
4876
4922
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
4877
4923
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
4878
4924
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -4897,8 +4943,11 @@ var StorageLocationTypeSchema = zod.z.string().regex(/^[a-z][a-zA-Z0-9-]*$/);
4897
4943
  * `STORAGE_LOCATION_CARDINALITY` map has been removed.
4898
4944
  *
4899
4945
  * `id` is a stable namespaced string of the form `<type>:<slug>`.
4900
- * The default location for a type uses `id === <type>:default` by
4901
- * convention (the bare type ref like `'backups'` resolves to it).
4946
+ * The seed names its first instance `<type>:default` — a NAME, not a flag.
4947
+ * There is no default location any more (D383): `enabled` is the whole write
4948
+ * model, and a bare type ref resolves to the sole location of the type, or —
4949
+ * transitionally, only while legacy NULL-stamped rows exist — to the row whose
4950
+ * slug is `default`.
4902
4951
  *
4903
4952
  * `isSystem` is a legacy persisted flag. Seed still creates the initial
4904
4953
  * `<type>:default` locations; the flag is no longer a lock, a badge, or a
@@ -4919,23 +4968,37 @@ var StorageLocationSchema = zod.z.object({
4919
4968
  * flag at upsert time, not here (the schema is provider-agnostic).
4920
4969
  */
4921
4970
  nodeId: zod.z.string().optional(),
4922
- isDefault: zod.z.boolean().default(false),
4923
4971
  isSystem: zod.z.boolean().default(false),
4924
4972
  /**
4925
- * Operator opt-in: whether consumers that BALANCE across several locations
4926
- * of a type may write here. Recordings reads it today; event media and
4927
- * backups are the next consumers, which is why the flag lives on the
4928
- * location rather than in any one addon's store — nothing has to be
4929
- * extended to add the next consumer.
4973
+ * THE write switch, and the only one (D383). `enabled: true` means every
4974
+ * consumer that chooses a write target for this type may write here, and all
4975
+ * enabled locations of a type are used TOGETHER; `false` means read-only —
4976
+ * still read, still played back, still age-swept, still drained, never
4977
+ * written.
4930
4978
  *
4931
- * OPTIONAL, and ABSENT MEANS ACTIVE. Every location persisted before the
4932
- * flag existed reads back with no flag and keeps working exactly as before;
4933
- * that is the whole compat story, and it is why no migration ships with it.
4934
- * A newly CREATED sibling is stamped `false` by the orchestrator (creating a
4935
- * disk must not silently start writing to it); the default of a type is
4936
- * always stamped `true`.
4979
+ * OPTIONAL only for the wire: an upsert that omits it means "leave what is
4980
+ * stored" on an update and "born inert unless it is the first location of its
4981
+ * type" on a create. On a PERSISTED row absence is legacy and it means
4982
+ * enabled — {@link isLocationEnabled} is the one place that says so, and the
4983
+ * orchestrator stamps every flagless row `true` once at hydrate so absence
4984
+ * stops existing rather than being re-derived on every read.
4937
4985
  */
4938
4986
  enabled: zod.z.boolean().optional(),
4987
+ /**
4988
+ * THE state of this location (D385), and the only authority on what may be
4989
+ * written, read or evicted here. Interpreted in exactly one place —
4990
+ * `storage-location-mode.ts` — which also folds the legacy
4991
+ * `enabled` / `config.readOnly` pair into a mode so an old row is never
4992
+ * ambiguous.
4993
+ *
4994
+ * OPTIONAL only for the wire and for rows written before D385: absence is
4995
+ * resolved by `resolveLocationMode`, and the orchestrator stamps every
4996
+ * unstamped row ONCE at hydrate so absence stops existing rather than being
4997
+ * re-derived on every read. `enabled` survives one release as a DERIVED
4998
+ * mirror (`mode === 'active'`); `withLocationMode` is the only writer of
4999
+ * either, so the two cannot disagree.
5000
+ */
5001
+ mode: StorageLocationModeSchema.optional(),
4939
5002
  /** COMPUTED at read time by the orchestrator (statfs of the backing volume
4940
5003
  * for node-local locations it can reach) — never persisted, absent when the
4941
5004
  * volume is remote/unreachable. The single capacity truth every UI reads. */
@@ -4943,13 +5006,83 @@ var StorageLocationSchema = zod.z.object({
4943
5006
  totalBytes: zod.z.number(),
4944
5007
  availableBytes: zod.z.number()
4945
5008
  }).nullable().optional(),
5009
+ /**
5010
+ * How much of that volume CamStack ITSELF holds on this location (D388) —
5011
+ * COMPUTED at read time from the `storage-occupancy` providers' own figures,
5012
+ * never persisted, never a filesystem walk.
5013
+ *
5014
+ * **ABSENT MEANS UNKNOWN, never zero.** No provider has reported for this
5015
+ * location yet — nobody stores here, the owning addon is down, or the first
5016
+ * refresh has not completed. A UI must omit the segment rather than draw it
5017
+ * at zero, which would claim we occupy nothing (D315). It is an OBJECT and
5018
+ * not a bare number precisely so that a `?? 0` on the consuming side has to
5019
+ * be spelled out loud instead of appearing by accident.
5020
+ *
5021
+ * `measuredAtMs` is the OLDEST contributing measurement, so it is honest
5022
+ * about the whole figure rather than about its freshest part.
5023
+ */
5024
+ owned: zod.z.object({
5025
+ bytes: zod.z.number().int().nonnegative(),
5026
+ measuredAtMs: zod.z.number().int().nonnegative()
5027
+ }).optional(),
4946
5028
  createdAt: zod.z.number(),
4947
5029
  updatedAt: zod.z.number()
4948
5030
  });
4949
5031
  /**
5032
+ * The retired `isDefault` key, DECLARED rather than dropped.
5033
+ *
5034
+ * A key removed from a non-strict `z.object` is stripped in silence (D380,
5035
+ * D381): a reader that still needs the old value gets `undefined` and cannot
5036
+ * tell "absent" from "never sent". The one reader that legitimately needs it —
5037
+ * the location store's boot migration, which turns the old default flag into
5038
+ * the `enabled` write set — parses THIS schema against the raw row instead, so
5039
+ * the migration is explicit and the live schema stays clean. Nothing else in
5040
+ * the repo may read it; `scripts/check-no-storage-default.ts` enforces that.
5041
+ */
5042
+ var LegacyStorageLocationDefaultSchema = zod.z.object({ isDefault: zod.z.boolean().optional() });
5043
+ function isLocationEnabled(location) {
5044
+ return mayWriteToLocation({
5045
+ ...location,
5046
+ config: location.config ?? {}
5047
+ });
5048
+ }
5049
+ /**
5050
+ * Bytes in one GB, for every storage figure an operator types.
5051
+ *
5052
+ * BINARY (1024³), everywhere. The repo had both: the orchestrator's
5053
+ * `maxUsedGb` → bytes conversion used 1024³ while the recorder's placement
5054
+ * headroom used 10⁹ for the SAME stored key, so a location with a cap set was
5055
+ * silently 7.4% off depending on which side of the pipe asked. The persisted
5056
+ * values were entered against the binary unit and the admin UI reads it, so
5057
+ * that is the one that stays. Every GB knob — `maxUsedGb`, `minFreeGb` —
5058
+ * converts through {@link gbToBytes} and nowhere else.
5059
+ */
5060
+ var STORAGE_BYTES_PER_GB = 1024 ** 3;
5061
+ /** GB → bytes, binary. Fractional GB is admitted and floored. */
5062
+ function gbToBytes(gb) {
5063
+ return Math.floor(gb * STORAGE_BYTES_PER_GB);
5064
+ }
5065
+ /**
5066
+ * How far a `drain` has got (D386) — the read a UI renders, and nothing more.
5067
+ *
5068
+ * `estimatedEmptyAtMs` is derived from the growth the ratchet has actually
5069
+ * OBSERVED and is `null` when it has observed none. Never a fabricated date: a
5070
+ * drain with no observed growth has no honest ETA, and inventing one is how an
5071
+ * operator learns not to believe the screen.
5072
+ */
5073
+ var StorageDrainProgressSchema = zod.z.object({
5074
+ locationId: zod.z.string(),
5075
+ startedAtMs: zod.z.number(),
5076
+ startBytes: zod.z.number(),
5077
+ bytesRemaining: zod.z.number(),
5078
+ drained: zod.z.boolean(),
5079
+ estimatedEmptyAtMs: zod.z.number().nullable()
5080
+ });
5081
+ /**
4950
5082
  * Reference accepted by consumer-facing `api.storage.*` calls.
4951
5083
  * Either:
4952
- * - a `StorageLocationType` (e.g. `'backups'`) → orchestrator resolves to the default of that type
5084
+ * - a `StorageLocationType` (e.g. `'backups'`) → the sole location of that type
5085
+ * (transitionally, the `<type>:default`-slugged row when several exist)
4953
5086
  * - a fully-qualified id (e.g. `'backups:nas-01'`) → addresses a specific instance
4954
5087
  *
4955
5088
  * The orchestrator's `resolveRef(ref)` handles both cases.
@@ -5131,6 +5264,111 @@ var DecoderSessionConfigSchema = zod.z.object({
5131
5264
  */
5132
5265
  debug: zod.z.boolean().optional()
5133
5266
  });
5267
+ /**
5268
+ * Per-component log CHANNELS — the gate a hot path consults, and the registry
5269
+ * an addon declares its channels in.
5270
+ *
5271
+ * ## Two axes, deliberately separated
5272
+ *
5273
+ * - **DECLARATION** — which channels exist. Only the addon knows:
5274
+ * `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
5275
+ * baichuan/handshake. A hand-wired central list rots at the first addition,
5276
+ * and rots silently. So a channel is declared where it is consulted, and the
5277
+ * `log-channels` capability enumerates the declarations.
5278
+ * - **VALUE** — at which level, for which scope, until when. That stays ONE
5279
+ * thing: the logging settings document on the `system` cap. Two authorities
5280
+ * over the values is the exact defect
5281
+ * `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
5282
+ * remove; re-introducing it from the cure side would be grotesque.
5283
+ *
5284
+ * Nothing in this file reads a clock, an env var or a store. The registry is
5285
+ * a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
5286
+ * the hot path with a value somebody actually read, and by
5287
+ * {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
5288
+ * never reaches here, so it can neither disarm an armed channel nor arm a
5289
+ * disarmed one (D49).
5290
+ *
5291
+ * ## The canonical call shape
5292
+ *
5293
+ * ```ts
5294
+ * if (CH_RTP.on && CH_RTP.wants(deviceId)) {
5295
+ * CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
5296
+ * }
5297
+ * ```
5298
+ *
5299
+ * `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
5300
+ * read. Disarmed, a call site costs one load and one branch, and the `extras`
5301
+ * object literal is never constructed because it lives inside the branch. It
5302
+ * is the same shape already proven in production at `stream-broker.ts:1650`,
5303
+ * and the same discipline `LoggingGate.allowsDestination` uses for the
5304
+ * destination floor (measured at 1.93 ns/call when off).
5305
+ *
5306
+ * ## Why a channel emits at `info`
5307
+ *
5308
+ * `loki-logging.addon.ts` pins the destination default at `info` and
5309
+ * `loki-destination.ts` drops everything below it, so a line emitted at
5310
+ * `debug` never reaches Loki and the hub's in-memory ring only holds ~35
5311
+ * minutes. A diagnostic that cannot be read an hour later is worse than no
5312
+ * diagnostic, because it looks done. {@link LogChannelGate.log} therefore
5313
+ * emits at the channel's declared level, whose schema floor is `info`.
5314
+ */
5315
+ /**
5316
+ * The level a channel writes at once armed.
5317
+ *
5318
+ * `debug` is absent ON PURPOSE and not by omission: below `info` the line does
5319
+ * not leave the process for Loki, and the whole point of arming a channel is
5320
+ * to read it later.
5321
+ */
5322
+ var LogChannelLevelSchema = zod.z.enum([
5323
+ "info",
5324
+ "warn",
5325
+ "error"
5326
+ ]);
5327
+ /**
5328
+ * What an addon declares about one channel. No value, no state — a
5329
+ * declaration is inert.
5330
+ */
5331
+ var LogChannelDescriptorSchema = zod.z.object({
5332
+ /**
5333
+ * Dotted `area.thing`, unique across the workspace. `area` is conventionally
5334
+ * the addon's short name so an operator reading a channel list can tell who
5335
+ * owns it without a second lookup.
5336
+ */
5337
+ name: zod.z.string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
5338
+ /** One sentence: what the operator will SEE after arming it. */
5339
+ description: zod.z.string().min(1),
5340
+ /** The level its lines are emitted at. Never below `info`. */
5341
+ defaultLevel: LogChannelLevelSchema,
5342
+ /**
5343
+ * Whether this channel can be narrowed to a camera.
5344
+ *
5345
+ * `true` is a PROMISE with two halves, and both must hold: the gate is
5346
+ * consulted with the numeric device id, AND every line the channel admits
5347
+ * carries `tags: { deviceId }` with that same numeric id. The second half is
5348
+ * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
5349
+ * keeps `deviceId` out of the stream labels for cardinality, so the tag in
5350
+ * the body is the only way to filter.
5351
+ *
5352
+ * A channel whose lines carry the device only in `meta` (or not at all) is
5353
+ * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
5354
+ * the operator narrows to one camera, sees nothing, and concludes the code
5355
+ * path was never taken.
5356
+ */
5357
+ perDevice: zod.z.boolean()
5358
+ });
5359
+ /**
5360
+ * An armed window over one channel, as the document hands it to a mirror.
5361
+ *
5362
+ * A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
5363
+ * expires by itself, which is the one failure a boolean cannot avoid.
5364
+ */
5365
+ var LogChannelWindowSchema = zod.z.object({
5366
+ channel: zod.z.string().min(1),
5367
+ /** Epoch ms the window closes at. */
5368
+ armedUntilMs: zod.z.number(),
5369
+ /** `null` = every camera. A non-empty list narrows to those numeric ids. */
5370
+ deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable()
5371
+ });
5134
5372
  /** Bucket key for rows no addon owns. Stable, so its series is continuous. */
5135
5373
  var UNATTRIBUTED_BUCKET_KEY = "__unattributed__";
5136
5374
  var ROOT_BUCKET_KEY = "__root__";
@@ -22090,7 +22328,6 @@ var storageCapability = {
22090
22328
  }), zod.z.instanceof(Uint8Array)),
22091
22329
  endDownload: method(zod.z.object({ downloadId: zod.z.string() }), zod.z.void(), { kind: "mutation" }),
22092
22330
  listLocations: method(zod.z.object({ type: StorageLocationTypeSchema.optional() }), zod.z.array(StorageLocationSchema).readonly()),
22093
- getDefaultLocation: method(zod.z.object({ type: StorageLocationTypeSchema }), StorageLocationSchema.nullable()),
22094
22331
  listLocationDeclarations: method(zod.z.void(), zod.z.array(StorageLocationDeclarationSchema).readonly()),
22095
22332
  upsertLocation: method(StorageLocationSchema.omit({
22096
22333
  createdAt: true,
@@ -22120,6 +22357,12 @@ var storageCapability = {
22120
22357
  kind: "mutation",
22121
22358
  auth: "admin"
22122
22359
  }),
22360
+ /**
22361
+ * How far each draining location has got (D386). A pure READ of what the
22362
+ * last pressure sweep computed — it starts no work, and a location that is
22363
+ * not draining simply does not appear.
22364
+ */
22365
+ listDrainProgress: method(zod.z.void(), zod.z.array(StorageDrainProgressSchema).readonly()),
22123
22366
  testLocation: method(zod.z.object({ id: zod.z.string() }), zod.z.object({
22124
22367
  ok: zod.z.boolean(),
22125
22368
  error: zod.z.string().optional()
@@ -22267,6 +22510,71 @@ var storageMigrationCapability = {
22267
22510
  history: method(zod.z.object({}), zod.z.array(StorageMigrationJobSchema).readonly(), { auth: "admin" })
22268
22511
  }
22269
22512
  };
22513
+ /**
22514
+ * `storage-occupancy` — how many bytes an addon actually HOLDS on a storage
22515
+ * location (D388).
22516
+ *
22517
+ * ## Why this is not `storage-evictable`
22518
+ *
22519
+ * `storage-evictable.getEvictableUsage` looks like the same question and is
22520
+ * not, in two ways that both matter and both bite hardest on the locations an
22521
+ * operator most wants a figure for:
22522
+ *
22523
+ * - it reports the whole eviction DOMAIN, not the location. `recordings:default`
22524
+ * and `recordingsLow:default` deliberately share one root and evict as one
22525
+ * oldest-first pool, so both answer with the SAME combined total. As an
22526
+ * occupancy figure that double-counts the disk.
22527
+ * - it reports ZERO for a location whose eviction policy is `never` (D385) —
22528
+ * a `readonly` or `disabled` disk. Those are exactly the disks an operator
22529
+ * is retiring and staring at.
22530
+ *
22531
+ * So this is its own contract with its own quantity, and the quantity is
22532
+ * OCCUPIED: every byte the addon holds on that location, whether or not it
22533
+ * would ever be willing to delete it. A provider that can only answer
22534
+ * "evictable" must not register here — a number that silently means different
22535
+ * things per class is worse than no number.
22536
+ *
22537
+ * ## Absence is an answer
22538
+ *
22539
+ * A location nobody reports for is UNKNOWN, never zero (D315). The orchestrator
22540
+ * stamps `StorageLocation.owned` only for locations it has a report for, and
22541
+ * the field is an OBJECT rather than a bare number so that a `?? 0` on the
22542
+ * consuming side has to be written out loud instead of appearing by accident.
22543
+ *
22544
+ * `internal: true` — consumed by the orchestrator's `listLocations` stamp, never
22545
+ * a public client surface. Clients read the stamped `StorageLocation.owned`.
22546
+ */
22547
+ /** One provider's occupancy answer for one location. */
22548
+ var StorageOccupancyReportSchema = zod.z.object({
22549
+ locationId: zod.z.string(),
22550
+ /** Bytes this provider holds on THAT location — not its eviction domain, and
22551
+ * not net of what it is willing to delete. */
22552
+ ownedBytes: zod.z.number().int().nonnegative(),
22553
+ /** When the provider last actually measured this. The orchestrator carries it
22554
+ * through so a UI can say how old the figure is instead of implying "now". */
22555
+ measuredAtMs: zod.z.number().int().nonnegative()
22556
+ });
22557
+ var storageOccupancyCapability = {
22558
+ name: "storage-occupancy",
22559
+ scope: "system",
22560
+ mode: "collection",
22561
+ internal: true,
22562
+ methods: {
22563
+ /**
22564
+ * Occupancy for the given locations, in ONE round trip.
22565
+ *
22566
+ * A provider answers only for the locations it actually holds bytes on and
22567
+ * OMITS the rest — an omitted location is "I hold nothing measurable here",
22568
+ * which the orchestrator merges as a contribution of nothing rather than as
22569
+ * a claim that the location is empty. Only a location no provider reports
22570
+ * at all stays unknown.
22571
+ *
22572
+ * This must be CHEAP and must never walk a filesystem: it is on the admin
22573
+ * UI's `listLocations` path. The owner keeps its own figure fresh (D224) and
22574
+ * answers from what it already has.
22575
+ */
22576
+ getOccupancy: method(zod.z.object({ locationIds: zod.z.array(zod.z.string()).readonly() }), zod.z.array(StorageOccupancyReportSchema).readonly(), { auth: "admin" }) }
22577
+ };
22270
22578
  var ProviderInfoSchema = zod.z.discriminatedUnion("shouldSaveDiskSpace", [zod.z.object({
22271
22579
  providerId: zod.z.string().min(1),
22272
22580
  displayName: zod.z.string().min(1),
@@ -24631,88 +24939,6 @@ onStatusChanged: { data: zod.z.object({
24631
24939
  volatileStateFields: ["lastUpdated"]
24632
24940
  };
24633
24941
  /**
24634
- * Network-link snapshot. Same shape for every provider (a Reolink wifi
24635
- * camera, a Home Assistant device with a signal-strength sensor, a Tapo
24636
- * plug): one slice under `device.runtimeState['network-link']`, one badge,
24637
- * one Home Assistant projection.
24638
- */
24639
- var NetworkLinkStatusSchema = zod.z.object({
24640
- /** The link the device is on. `'unknown'` = not read yet, not "no link". */
24641
- type: zod.z.enum([
24642
- "wifi",
24643
- "ethernet",
24644
- "cellular",
24645
- "unknown"
24646
- ]),
24647
- /**
24648
- * Link quality, 0..100 inclusive, normalised by the provider from whatever
24649
- * the firmware reports (bars, RSSI, a vendor scale). **`null` means NOT
24650
- * KNOWN or NOT APPLICABLE** — a wired link has no signal, and a wireless
24651
- * one whose reading has not landed must not be drawn at 0 %. Consumers
24652
- * SKIP a null rather than coerce it.
24653
- */
24654
- signalPercent: zod.z.number().min(0).max(100).nullable(),
24655
- /** Raw received signal strength in dBm, when the firmware reports one. */
24656
- rssiDbm: zod.z.number().optional(),
24657
- /** Network name of a wireless link, when the firmware reports it. */
24658
- ssid: zod.z.string().optional(),
24659
- /** Ms epoch of the last observation. Lets consumers reason about freshness. */
24660
- lastUpdated: zod.z.number()
24661
- });
24662
- var networkLinkCapability = {
24663
- name: "network-link",
24664
- scope: "device",
24665
- deviceNative: true,
24666
- mode: "singleton",
24667
- deviceTypes: [
24668
- DeviceType.Camera,
24669
- DeviceType.Sensor,
24670
- DeviceType.Button,
24671
- DeviceType.Switch,
24672
- DeviceType.Light,
24673
- DeviceType.Lock,
24674
- DeviceType.Siren
24675
- ],
24676
- methods: {},
24677
- events: {
24678
- /**
24679
- * Emitted whenever the cached status changes (a link switch, a signal
24680
- * reading that moved). Mirrored on the parent chain by the
24681
- * DeviceEventPropagator like `battery.onStatusChanged`.
24682
- */
24683
- onStatusChanged: { data: zod.z.object({
24684
- deviceId: zod.z.number(),
24685
- status: NetworkLinkStatusSchema
24686
- }) } },
24687
- status: {
24688
- schema: NetworkLinkStatusSchema,
24689
- kind: "push",
24690
- empty: {
24691
- type: "unknown",
24692
- signalPercent: null,
24693
- lastUpdated: 0
24694
- }
24695
- },
24696
- /**
24697
- * Runtime-state slice — every provider stores the same shape under
24698
- * `device.runtimeState['network-link']`, read once by the badge and the
24699
- * Home Assistant projector regardless of the driver.
24700
- */
24701
- runtimeState: NetworkLinkStatusSchema,
24702
- /**
24703
- * Runtime-state durability: **restored** — a link reading is slow to
24704
- * change and a sleeping battery camera may not report for hours; the
24705
- * restored slice is what the badge shows until the next read.
24706
- *
24707
- * See `RuntimeStateDurability`. Enforced by
24708
- * `scripts/check-runtime-state-durability.ts`.
24709
- */
24710
- durability: "restored",
24711
- /** Clock fields: written, but excluded from the compare that decides
24712
- * whether persisting is worth a SQLite commit. */
24713
- volatileStateFields: ["lastUpdated"]
24714
- };
24715
- /**
24716
24942
  * Generic boolean sensor — last-resort fallback when no domain-
24717
24943
  * specific binary cap fits (Home Assistant `binary_sensor` without a
24718
24944
  * known `device_class`, or a domain we haven't typed yet). Pure
@@ -28590,6 +28816,371 @@ var nativeObjectDetectionCapability = {
28590
28816
  volatileStateFields: ["lastFetchedAt"]
28591
28817
  };
28592
28818
  /**
28819
+ * `navigation` — a device-scoped capability that natively expresses the FULL
28820
+ * navigation / action surface of a robot that DRIVES ITSELF and carries an
28821
+ * on-board camera (the Dreame robot-vacuum camera is the first provider).
28822
+ *
28823
+ * Why a NEW cap rather than overloading `ptz`:
28824
+ * - `ptz` moves a *gimbal* on a fixed camera (pan/tilt/zoom of the lens). A
28825
+ * robot vacuum has no gimbal — the whole chassis drives, turns and spins.
28826
+ * The two are different physical models: PTZ is absolute-position + presets,
28827
+ * navigation is momentary drive nudges + discrete robot ACTIONS
28828
+ * (dock / spot-clean / follow-pet / go-to-point / …).
28829
+ * - This cap is the SOURCE OF TRUTH. Two consumers adapt from it rather than
28830
+ * the reverse:
28831
+ * 1. a native CamStack navigation panel (data-driven from `listActions`
28832
+ * / `getOptions`), and
28833
+ * 2. the PTZ surface — a thin adapter mimics `ptz` from `navigation` so a
28834
+ * robot camera shows up in the existing PTZ control path without every
28835
+ * PTZ provider learning about robots. The mapping lives in the adapter,
28836
+ * not here (see the addon design note):
28837
+ * ptz.continuousMove({pan,tilt}) → navigation.move({pan,tilt})
28838
+ * ptz.stop() → navigation.stop()
28839
+ * ptz.goHome() → navigation.runAction('goHome')
28840
+ * ptz.getPresets() → navigation.listActions() (id→preset)
28841
+ * ptz.goToPreset(id) → navigation.runAction(id)
28842
+ *
28843
+ * ## Continuous drive
28844
+ *
28845
+ * `move` is MOMENTARY. Fluid navigation comes from the UI (or the PTZ adapter)
28846
+ * sending `move({pan,tilt})` REPEATEDLY at ~1 Hz while a direction is held, and
28847
+ * one `stop()` on release — exactly like the robot app's remote-drive joystick.
28848
+ * The provider forwards EACH `move` to one drive write; it must NOT debounce or
28849
+ * coalesce them. The UI owns the cadence.
28850
+ *
28851
+ * ## The action dictionary
28852
+ *
28853
+ * The discrete controls (dock / locate / spot-clean / follow / flash / sounds)
28854
+ * are a DATA-DRIVEN dictionary the cap exposes via `listActions()`. Each entry
28855
+ * carries `{ id, kind, label, icon }` (plus `soundId` for sound entries) so the
28856
+ * native panel AND the PTZ mimic render buttons WITHOUT hardcoding a
28857
+ * vendor-specific list. `kind: 'action'` entries are triggered with
28858
+ * `runAction({ actionId })`; `kind: 'sound'` entries with `playSound({ soundId })`
28859
+ * (the entry carries the `soundId` to pass). The general primitives — `move`,
28860
+ * `stop`, `goToPoint` — stay as first-class methods, not dictionary entries.
28861
+ *
28862
+ * NOTE (provider wiring): the first provider (Dreame) drives this with the RAW
28863
+ * `callAction(siid,aiid,in)` / `setProperty({siid,piid,value})` MIoT primitives
28864
+ * that the currently-published `@apocaliss92/nodedreame` already exposes on
28865
+ * every device handle. A future nodedreame publish adds a typed
28866
+ * `DreameCameraController` (drive / playPetSound / spotClean / findPet / …); the
28867
+ * provider can then swap the raw calls for the typed methods with no change to
28868
+ * THIS contract.
28869
+ */
28870
+ /**
28871
+ * A momentary drive nudge. The robot MOVES (no gimbal) for as long as the caller
28872
+ * keeps sending nudges (~1 Hz); an explicit `stop` (or letting the nudges lapse)
28873
+ * halts it.
28874
+ *
28875
+ * - `pan` — turn: negative = left, positive = right, 0 = straight.
28876
+ * - `tilt` — throttle: positive = forward, negative = spin / turn-around.
28877
+ * - `speed` — optional intensity hint [0, 1]; the provider may scale the drive
28878
+ * vector by it (drivers without proportional drive ignore it).
28879
+ *
28880
+ * `pan` / `tilt` are normalized [-1, 1]. Both optional so a caller can nudge one
28881
+ * axis alone; an all-undefined nudge is a no-op.
28882
+ */
28883
+ var NavigationMoveCommandSchema = zod.z.object({
28884
+ pan: zod.z.number().min(-1).max(1).optional(),
28885
+ tilt: zod.z.number().min(-1).max(1).optional(),
28886
+ speed: zod.z.number().min(0).max(1).optional()
28887
+ });
28888
+ /**
28889
+ * The enumerated discrete actions a navigation-capable robot can perform via
28890
+ * `runAction`. This is the CLOSED vocabulary; a given device advertises the
28891
+ * subset it supports through `listActions`. Sounds are NOT here — they go through
28892
+ * `playSound` (see the `sound` dictionary entries).
28893
+ */
28894
+ var NavigationActionIdSchema = zod.z.enum([
28895
+ "goHome",
28896
+ "locate",
28897
+ "spotClean",
28898
+ "findPet",
28899
+ "personFollow",
28900
+ "stop",
28901
+ "startClean",
28902
+ "pauseClean",
28903
+ "dockWash",
28904
+ "autoEmpty",
28905
+ "flashOn",
28906
+ "flashOff"
28907
+ ]);
28908
+ /** Whether a dictionary entry is a `runAction` action or a `playSound` sound. */
28909
+ var NavigationEntryKindSchema = zod.z.enum(["action", "sound"]);
28910
+ /**
28911
+ * One entry in the navigation action dictionary — the DATA-DRIVEN unit both the
28912
+ * native panel and the PTZ mimic render as a button.
28913
+ *
28914
+ * - `id` — stable id. For `kind:'action'` it is a {@link NavigationActionId}
28915
+ * (pass to `runAction`); for `kind:'sound'` it is a namespaced id
28916
+ * (`sound:meow`) whose `soundId` is passed to `playSound`.
28917
+ * - `icon` — icon HINT (lucide-style name; the UI maps it to its own set).
28918
+ * - `label` — operator-facing English label.
28919
+ * - `soundId` — wire sound id, present only on `kind:'sound'` entries.
28920
+ * - `enabled` — per-device FEATURE FLAG. `listActions` reports it so the UI /
28921
+ * PTZ render ONLY enabled entries. Data-driven: the provider
28922
+ * flips it from config, never by editing code.
28923
+ */
28924
+ var NavigationActionEntrySchema = zod.z.object({
28925
+ id: zod.z.string(),
28926
+ kind: NavigationEntryKindSchema,
28927
+ label: zod.z.string(),
28928
+ icon: zod.z.string(),
28929
+ /** Present only on `kind:'sound'` entries — the id to pass to `playSound`. */
28930
+ soundId: zod.z.number().int().optional(),
28931
+ /** Per-device feature flag — render this entry only when true. */
28932
+ enabled: zod.z.boolean()
28933
+ });
28934
+ /** Coordinates for `goToPoint` — a point on the robot's live map. */
28935
+ var NavigationPointSchema = zod.z.object({
28936
+ x: zod.z.number(),
28937
+ y: zod.z.number()
28938
+ });
28939
+ /**
28940
+ * Per-device FEATURE-FLAG report for the general (non-dictionary) primitives.
28941
+ * The cap reports which are enabled so the UI / PTZ render only the controls
28942
+ * that are turned on for THIS device. Data-driven: the provider derives these
28943
+ * from config + probe, never hardcoded in the UI. The per-DICTIONARY-entry flags
28944
+ * live on {@link NavigationActionEntrySchema.enabled}; these gate the primitives
28945
+ * that are not dictionary entries.
28946
+ *
28947
+ * - `move` / `stop` — the momentary drive joystick.
28948
+ * - `goToPoint` — send-to-map-coordinate. Ships OFF on Dreame until the
28949
+ * map-coordinate plumbing is wired.
28950
+ * - `runAction` — the discrete action buttons (dictionary `kind:'action'`).
28951
+ * - `playSound` — the sound buttons (dictionary `kind:'sound'`).
28952
+ * - `light` — the on/off fill-light toggle (works anytime).
28953
+ * - `lightMode` — the auto/manual selector + manual level slider (a
28954
+ * camera-service control; needs an active stream).
28955
+ */
28956
+ var NavigationFeaturesSchema = zod.z.object({
28957
+ move: zod.z.boolean(),
28958
+ stop: zod.z.boolean(),
28959
+ goToPoint: zod.z.boolean(),
28960
+ runAction: zod.z.boolean(),
28961
+ playSound: zod.z.boolean(),
28962
+ light: zod.z.boolean(),
28963
+ lightMode: zod.z.boolean()
28964
+ });
28965
+ /** Light mode: `auto` lets the camera choose brightness; `manual` uses `level`. */
28966
+ var NavigationLightModeSchema = zod.z.enum(["auto", "manual"]);
28967
+ /** Coarse work mode the robot reports — drives the UI's active-state chips. */
28968
+ var NavigationModeSchema = zod.z.enum([
28969
+ "idle",
28970
+ "cleaning",
28971
+ "spot",
28972
+ "following",
28973
+ "goto",
28974
+ "returning",
28975
+ "paused",
28976
+ "unknown"
28977
+ ]);
28978
+ /**
28979
+ * Live navigation state so the UI can reflect what the robot is doing:
28980
+ * - `mode` — coarse activity (idle / cleaning / following / …).
28981
+ * - `following` — person/pet follow is currently armed.
28982
+ * - `flash` — the on-camera fill light is on.
28983
+ * - `lightMode` — auto vs manual fill-light mode.
28984
+ * - `lightLevel` — manual fill-light level (40..100); meaningful when
28985
+ * `lightMode === 'manual'`.
28986
+ */
28987
+ var NavigationStatusSchema = zod.z.object({
28988
+ mode: NavigationModeSchema,
28989
+ following: zod.z.boolean(),
28990
+ flash: zod.z.boolean(),
28991
+ lightMode: NavigationLightModeSchema,
28992
+ lightLevel: zod.z.number().min(40).max(100),
28993
+ /** Ms epoch when the slice was last updated. */
28994
+ lastChangedAt: zod.z.number()
28995
+ });
28996
+ /**
28997
+ * Runtime-state slice owned by this cap (kernel-managed: validated, mirrored,
28998
+ * observable). Adds `lastFetchedAt` on top of the status shape per the
28999
+ * convention.
29000
+ */
29001
+ var NavigationRuntimeStateSchema = NavigationStatusSchema.extend({ lastFetchedAt: zod.z.number() });
29002
+ var navigationCapability = {
29003
+ name: "navigation",
29004
+ scope: "device",
29005
+ deviceNative: true,
29006
+ mode: "singleton",
29007
+ deviceTypes: [DeviceType.Camera],
29008
+ deviceConfig: { ui: {
29009
+ kind: "widget",
29010
+ widgetId: "host/navigation-panel",
29011
+ tab: "navigation",
29012
+ topTab: true,
29013
+ label: "Navigation",
29014
+ order: 0
29015
+ } },
29016
+ methods: {
29017
+ /**
29018
+ * Momentary drive nudge (the robot moves). `protected` — mirrors
29019
+ * `ptz.continuousMove` so the Viewer navigation panel (and the PTZ-mimic
29020
+ * path) works for any authenticated user, not admin-only. The UI sends
29021
+ * these at ~1 Hz while a control is held; the provider forwards each one to
29022
+ * a single drive write WITHOUT debouncing.
29023
+ */
29024
+ move: method(NavigationMoveCommandSchema.extend({ deviceId: zod.z.number() }), zod.z.void(), { kind: "mutation" }),
29025
+ /** Halt all motion immediately (zero drive vector). */
29026
+ stop: method(zod.z.object({ deviceId: zod.z.number() }), zod.z.void(), { kind: "mutation" }),
29027
+ /** Send the robot to a point on its live map. */
29028
+ goToPoint: method(NavigationPointSchema.extend({ deviceId: zod.z.number() }), zod.z.void(), { kind: "mutation" }),
29029
+ /**
29030
+ * Enumerate the discrete controls THIS device supports (data-driven UI +
29031
+ * PTZ mimic). Camera-probed subset of {@link NAVIGATION_ACTION_CATALOG}.
29032
+ */
29033
+ listActions: method(zod.z.object({ deviceId: zod.z.number() }), zod.z.array(NavigationActionEntrySchema)),
29034
+ /**
29035
+ * Run one discrete action (a `kind:'action'` dictionary entry). Invalid /
29036
+ * unsupported action ids are rejected by the provider.
29037
+ */
29038
+ runAction: method(zod.z.object({
29039
+ deviceId: zod.z.number(),
29040
+ actionId: NavigationActionIdSchema
29041
+ }), zod.z.void(), { kind: "mutation" }),
29042
+ /** Play a sound by its wire id (the `soundId` of a `kind:'sound'` entry). */
29043
+ playSound: method(zod.z.object({
29044
+ deviceId: zod.z.number(),
29045
+ soundId: zod.z.number().int()
29046
+ }), zod.z.void(), { kind: "mutation" }),
29047
+ /**
29048
+ * Turn the on-camera fill light on / off (the `OpenFullLight` control —
29049
+ * works anytime, no active stream required).
29050
+ */
29051
+ setLightOn: method(zod.z.object({
29052
+ deviceId: zod.z.number(),
29053
+ on: zod.z.boolean()
29054
+ }), zod.z.void(), { kind: "mutation" }),
29055
+ /**
29056
+ * Set the fill-light mode (auto vs manual). `manual` optionally carries the
29057
+ * initial `level`. The auto/manual + level control is a CAMERA-service
29058
+ * action that generally needs an active camera stream/monitor session — the
29059
+ * UI shows the manual level slider ONLY when `mode === 'manual'`.
29060
+ */
29061
+ setLightMode: method(zod.z.object({
29062
+ deviceId: zod.z.number(),
29063
+ mode: NavigationLightModeSchema,
29064
+ level: zod.z.number().min(40).max(100).optional()
29065
+ }), zod.z.void(), { kind: "mutation" }),
29066
+ /** Set the MANUAL fill-light level (40..100). Implies `manual` mode. */
29067
+ setLightLevel: method(zod.z.object({
29068
+ deviceId: zod.z.number(),
29069
+ level: zod.z.number().min(40).max(100)
29070
+ }), zod.z.void(), { kind: "mutation" }),
29071
+ /**
29072
+ * Per-device FEATURE-FLAG report for the general primitives — drives which
29073
+ * controls the UI shows (the per-entry flags for the dictionary come back on
29074
+ * `listActions`).
29075
+ */
29076
+ getFeatures: method(zod.z.object({ deviceId: zod.z.number() }), NavigationFeaturesSchema)
29077
+ },
29078
+ events: { onStatusChanged: { data: zod.z.object({
29079
+ deviceId: zod.z.number(),
29080
+ status: NavigationStatusSchema
29081
+ }) } },
29082
+ status: {
29083
+ schema: NavigationStatusSchema,
29084
+ kind: "push"
29085
+ },
29086
+ /**
29087
+ * Runtime-state slice mirrored by the kernel. The navigation panel watches it
29088
+ * for live mode / follow / flash changes.
29089
+ */
29090
+ runtimeState: NavigationRuntimeStateSchema,
29091
+ /**
29092
+ * Runtime-state durability: **session** — like `vacuum-control`, a restored
29093
+ * `mode: cleaning` / `following: true` is a robot that is not actually doing
29094
+ * that. The live handle re-publishes on connect.
29095
+ *
29096
+ * See `RuntimeStateDurability`. Enforced by
29097
+ * `scripts/check-runtime-state-durability.ts`.
29098
+ */
29099
+ durability: "session"
29100
+ };
29101
+ /**
29102
+ * Network-link snapshot. Same shape for every provider (a Reolink wifi
29103
+ * camera, a Home Assistant device with a signal-strength sensor, a Tapo
29104
+ * plug): one slice under `device.runtimeState['network-link']`, one badge,
29105
+ * one Home Assistant projection.
29106
+ */
29107
+ var NetworkLinkStatusSchema = zod.z.object({
29108
+ /** The link the device is on. `'unknown'` = not read yet, not "no link". */
29109
+ type: zod.z.enum([
29110
+ "wifi",
29111
+ "ethernet",
29112
+ "cellular",
29113
+ "unknown"
29114
+ ]),
29115
+ /**
29116
+ * Link quality, 0..100 inclusive, normalised by the provider from whatever
29117
+ * the firmware reports (bars, RSSI, a vendor scale). **`null` means NOT
29118
+ * KNOWN or NOT APPLICABLE** — a wired link has no signal, and a wireless
29119
+ * one whose reading has not landed must not be drawn at 0 %. Consumers
29120
+ * SKIP a null rather than coerce it.
29121
+ */
29122
+ signalPercent: zod.z.number().min(0).max(100).nullable(),
29123
+ /** Raw received signal strength in dBm, when the firmware reports one. */
29124
+ rssiDbm: zod.z.number().optional(),
29125
+ /** Network name of a wireless link, when the firmware reports it. */
29126
+ ssid: zod.z.string().optional(),
29127
+ /** Ms epoch of the last observation. Lets consumers reason about freshness. */
29128
+ lastUpdated: zod.z.number()
29129
+ });
29130
+ var networkLinkCapability = {
29131
+ name: "network-link",
29132
+ scope: "device",
29133
+ deviceNative: true,
29134
+ mode: "singleton",
29135
+ deviceTypes: [
29136
+ DeviceType.Camera,
29137
+ DeviceType.Sensor,
29138
+ DeviceType.Button,
29139
+ DeviceType.Switch,
29140
+ DeviceType.Light,
29141
+ DeviceType.Lock,
29142
+ DeviceType.Siren
29143
+ ],
29144
+ methods: {},
29145
+ events: {
29146
+ /**
29147
+ * Emitted whenever the cached status changes (a link switch, a signal
29148
+ * reading that moved). Mirrored on the parent chain by the
29149
+ * DeviceEventPropagator like `battery.onStatusChanged`.
29150
+ */
29151
+ onStatusChanged: { data: zod.z.object({
29152
+ deviceId: zod.z.number(),
29153
+ status: NetworkLinkStatusSchema
29154
+ }) } },
29155
+ status: {
29156
+ schema: NetworkLinkStatusSchema,
29157
+ kind: "push",
29158
+ empty: {
29159
+ type: "unknown",
29160
+ signalPercent: null,
29161
+ lastUpdated: 0
29162
+ }
29163
+ },
29164
+ /**
29165
+ * Runtime-state slice — every provider stores the same shape under
29166
+ * `device.runtimeState['network-link']`, read once by the badge and the
29167
+ * Home Assistant projector regardless of the driver.
29168
+ */
29169
+ runtimeState: NetworkLinkStatusSchema,
29170
+ /**
29171
+ * Runtime-state durability: **restored** — a link reading is slow to
29172
+ * change and a sleeping battery camera may not report for hours; the
29173
+ * restored slice is what the badge shows until the next read.
29174
+ *
29175
+ * See `RuntimeStateDurability`. Enforced by
29176
+ * `scripts/check-runtime-state-durability.ts`.
29177
+ */
29178
+ durability: "restored",
29179
+ /** Clock fields: written, but excluded from the compare that decides
29180
+ * whether persisting is worth a SQLite commit. */
29181
+ volatileStateFields: ["lastUpdated"]
29182
+ };
29183
+ /**
28593
29184
  * network-quality — system-scoped singleton capability tracking RTT,
28594
29185
  * jitter, and observed/peak bandwidth per device + per client.
28595
29186
  *
@@ -30440,289 +31031,6 @@ var ptzAutotrackCapability = {
30440
31031
  durability: "session"
30441
31032
  };
30442
31033
  /**
30443
- * `navigation` — a device-scoped capability that natively expresses the FULL
30444
- * navigation / action surface of a robot that DRIVES ITSELF and carries an
30445
- * on-board camera (the Dreame robot-vacuum camera is the first provider).
30446
- *
30447
- * Why a NEW cap rather than overloading `ptz`:
30448
- * - `ptz` moves a *gimbal* on a fixed camera (pan/tilt/zoom of the lens). A
30449
- * robot vacuum has no gimbal — the whole chassis drives, turns and spins.
30450
- * The two are different physical models: PTZ is absolute-position + presets,
30451
- * navigation is momentary drive nudges + discrete robot ACTIONS
30452
- * (dock / spot-clean / follow-pet / go-to-point / …).
30453
- * - This cap is the SOURCE OF TRUTH. Two consumers adapt from it rather than
30454
- * the reverse:
30455
- * 1. a native CamStack navigation panel (data-driven from `listActions`
30456
- * / `getOptions`), and
30457
- * 2. the PTZ surface — a thin adapter mimics `ptz` from `navigation` so a
30458
- * robot camera shows up in the existing PTZ control path without every
30459
- * PTZ provider learning about robots. The mapping lives in the adapter,
30460
- * not here (see the addon design note):
30461
- * ptz.continuousMove({pan,tilt}) → navigation.move({pan,tilt})
30462
- * ptz.stop() → navigation.stop()
30463
- * ptz.goHome() → navigation.runAction('goHome')
30464
- * ptz.getPresets() → navigation.listActions() (id→preset)
30465
- * ptz.goToPreset(id) → navigation.runAction(id)
30466
- *
30467
- * ## Continuous drive
30468
- *
30469
- * `move` is MOMENTARY. Fluid navigation comes from the UI (or the PTZ adapter)
30470
- * sending `move({pan,tilt})` REPEATEDLY at ~1 Hz while a direction is held, and
30471
- * one `stop()` on release — exactly like the robot app's remote-drive joystick.
30472
- * The provider forwards EACH `move` to one drive write; it must NOT debounce or
30473
- * coalesce them. The UI owns the cadence.
30474
- *
30475
- * ## The action dictionary
30476
- *
30477
- * The discrete controls (dock / locate / spot-clean / follow / flash / sounds)
30478
- * are a DATA-DRIVEN dictionary the cap exposes via `listActions()`. Each entry
30479
- * carries `{ id, kind, label, icon }` (plus `soundId` for sound entries) so the
30480
- * native panel AND the PTZ mimic render buttons WITHOUT hardcoding a
30481
- * vendor-specific list. `kind: 'action'` entries are triggered with
30482
- * `runAction({ actionId })`; `kind: 'sound'` entries with `playSound({ soundId })`
30483
- * (the entry carries the `soundId` to pass). The general primitives — `move`,
30484
- * `stop`, `goToPoint` — stay as first-class methods, not dictionary entries.
30485
- *
30486
- * NOTE (provider wiring): the first provider (Dreame) drives this with the RAW
30487
- * `callAction(siid,aiid,in)` / `setProperty({siid,piid,value})` MIoT primitives
30488
- * that the currently-published `@apocaliss92/nodedreame` already exposes on
30489
- * every device handle. A future nodedreame publish adds a typed
30490
- * `DreameCameraController` (drive / playPetSound / spotClean / findPet / …); the
30491
- * provider can then swap the raw calls for the typed methods with no change to
30492
- * THIS contract.
30493
- */
30494
- /**
30495
- * A momentary drive nudge. The robot MOVES (no gimbal) for as long as the caller
30496
- * keeps sending nudges (~1 Hz); an explicit `stop` (or letting the nudges lapse)
30497
- * halts it.
30498
- *
30499
- * - `pan` — turn: negative = left, positive = right, 0 = straight.
30500
- * - `tilt` — throttle: positive = forward, negative = spin / turn-around.
30501
- * - `speed` — optional intensity hint [0, 1]; the provider may scale the drive
30502
- * vector by it (drivers without proportional drive ignore it).
30503
- *
30504
- * `pan` / `tilt` are normalized [-1, 1]. Both optional so a caller can nudge one
30505
- * axis alone; an all-undefined nudge is a no-op.
30506
- */
30507
- var NavigationMoveCommandSchema = zod.z.object({
30508
- pan: zod.z.number().min(-1).max(1).optional(),
30509
- tilt: zod.z.number().min(-1).max(1).optional(),
30510
- speed: zod.z.number().min(0).max(1).optional()
30511
- });
30512
- /**
30513
- * The enumerated discrete actions a navigation-capable robot can perform via
30514
- * `runAction`. This is the CLOSED vocabulary; a given device advertises the
30515
- * subset it supports through `listActions`. Sounds are NOT here — they go through
30516
- * `playSound` (see the `sound` dictionary entries).
30517
- */
30518
- var NavigationActionIdSchema = zod.z.enum([
30519
- "goHome",
30520
- "locate",
30521
- "spotClean",
30522
- "findPet",
30523
- "personFollow",
30524
- "stop",
30525
- "startClean",
30526
- "pauseClean",
30527
- "dockWash",
30528
- "autoEmpty",
30529
- "flashOn",
30530
- "flashOff"
30531
- ]);
30532
- /** Whether a dictionary entry is a `runAction` action or a `playSound` sound. */
30533
- var NavigationEntryKindSchema = zod.z.enum(["action", "sound"]);
30534
- /**
30535
- * One entry in the navigation action dictionary — the DATA-DRIVEN unit both the
30536
- * native panel and the PTZ mimic render as a button.
30537
- *
30538
- * - `id` — stable id. For `kind:'action'` it is a {@link NavigationActionId}
30539
- * (pass to `runAction`); for `kind:'sound'` it is a namespaced id
30540
- * (`sound:meow`) whose `soundId` is passed to `playSound`.
30541
- * - `icon` — icon HINT (lucide-style name; the UI maps it to its own set).
30542
- * - `label` — operator-facing English label.
30543
- * - `soundId` — wire sound id, present only on `kind:'sound'` entries.
30544
- * - `enabled` — per-device FEATURE FLAG. `listActions` reports it so the UI /
30545
- * PTZ render ONLY enabled entries. Data-driven: the provider
30546
- * flips it from config, never by editing code.
30547
- */
30548
- var NavigationActionEntrySchema = zod.z.object({
30549
- id: zod.z.string(),
30550
- kind: NavigationEntryKindSchema,
30551
- label: zod.z.string(),
30552
- icon: zod.z.string(),
30553
- /** Present only on `kind:'sound'` entries — the id to pass to `playSound`. */
30554
- soundId: zod.z.number().int().optional(),
30555
- /** Per-device feature flag — render this entry only when true. */
30556
- enabled: zod.z.boolean()
30557
- });
30558
- /** Coordinates for `goToPoint` — a point on the robot's live map. */
30559
- var NavigationPointSchema = zod.z.object({
30560
- x: zod.z.number(),
30561
- y: zod.z.number()
30562
- });
30563
- /**
30564
- * Per-device FEATURE-FLAG report for the general (non-dictionary) primitives.
30565
- * The cap reports which are enabled so the UI / PTZ render only the controls
30566
- * that are turned on for THIS device. Data-driven: the provider derives these
30567
- * from config + probe, never hardcoded in the UI. The per-DICTIONARY-entry flags
30568
- * live on {@link NavigationActionEntrySchema.enabled}; these gate the primitives
30569
- * that are not dictionary entries.
30570
- *
30571
- * - `move` / `stop` — the momentary drive joystick.
30572
- * - `goToPoint` — send-to-map-coordinate. Ships OFF on Dreame until the
30573
- * map-coordinate plumbing is wired.
30574
- * - `runAction` — the discrete action buttons (dictionary `kind:'action'`).
30575
- * - `playSound` — the sound buttons (dictionary `kind:'sound'`).
30576
- * - `light` — the on/off fill-light toggle (works anytime).
30577
- * - `lightMode` — the auto/manual selector + manual level slider (a
30578
- * camera-service control; needs an active stream).
30579
- */
30580
- var NavigationFeaturesSchema = zod.z.object({
30581
- move: zod.z.boolean(),
30582
- stop: zod.z.boolean(),
30583
- goToPoint: zod.z.boolean(),
30584
- runAction: zod.z.boolean(),
30585
- playSound: zod.z.boolean(),
30586
- light: zod.z.boolean(),
30587
- lightMode: zod.z.boolean()
30588
- });
30589
- /** Light mode: `auto` lets the camera choose brightness; `manual` uses `level`. */
30590
- var NavigationLightModeSchema = zod.z.enum(["auto", "manual"]);
30591
- /** Coarse work mode the robot reports — drives the UI's active-state chips. */
30592
- var NavigationModeSchema = zod.z.enum([
30593
- "idle",
30594
- "cleaning",
30595
- "spot",
30596
- "following",
30597
- "goto",
30598
- "returning",
30599
- "paused",
30600
- "unknown"
30601
- ]);
30602
- /**
30603
- * Live navigation state so the UI can reflect what the robot is doing:
30604
- * - `mode` — coarse activity (idle / cleaning / following / …).
30605
- * - `following` — person/pet follow is currently armed.
30606
- * - `flash` — the on-camera fill light is on.
30607
- * - `lightMode` — auto vs manual fill-light mode.
30608
- * - `lightLevel` — manual fill-light level (40..100); meaningful when
30609
- * `lightMode === 'manual'`.
30610
- */
30611
- var NavigationStatusSchema = zod.z.object({
30612
- mode: NavigationModeSchema,
30613
- following: zod.z.boolean(),
30614
- flash: zod.z.boolean(),
30615
- lightMode: NavigationLightModeSchema,
30616
- lightLevel: zod.z.number().min(40).max(100),
30617
- /** Ms epoch when the slice was last updated. */
30618
- lastChangedAt: zod.z.number()
30619
- });
30620
- /**
30621
- * Runtime-state slice owned by this cap (kernel-managed: validated, mirrored,
30622
- * observable). Adds `lastFetchedAt` on top of the status shape per the
30623
- * convention.
30624
- */
30625
- var NavigationRuntimeStateSchema = NavigationStatusSchema.extend({ lastFetchedAt: zod.z.number() });
30626
- var navigationCapability = {
30627
- name: "navigation",
30628
- scope: "device",
30629
- deviceNative: true,
30630
- mode: "singleton",
30631
- deviceTypes: [DeviceType.Camera],
30632
- deviceConfig: { ui: {
30633
- kind: "widget",
30634
- widgetId: "host/navigation-panel",
30635
- tab: "navigation",
30636
- topTab: true,
30637
- label: "Navigation",
30638
- order: 0
30639
- } },
30640
- methods: {
30641
- /**
30642
- * Momentary drive nudge (the robot moves). `protected` — mirrors
30643
- * `ptz.continuousMove` so the Viewer navigation panel (and the PTZ-mimic
30644
- * path) works for any authenticated user, not admin-only. The UI sends
30645
- * these at ~1 Hz while a control is held; the provider forwards each one to
30646
- * a single drive write WITHOUT debouncing.
30647
- */
30648
- move: method(NavigationMoveCommandSchema.extend({ deviceId: zod.z.number() }), zod.z.void(), { kind: "mutation" }),
30649
- /** Halt all motion immediately (zero drive vector). */
30650
- stop: method(zod.z.object({ deviceId: zod.z.number() }), zod.z.void(), { kind: "mutation" }),
30651
- /** Send the robot to a point on its live map. */
30652
- goToPoint: method(NavigationPointSchema.extend({ deviceId: zod.z.number() }), zod.z.void(), { kind: "mutation" }),
30653
- /**
30654
- * Enumerate the discrete controls THIS device supports (data-driven UI +
30655
- * PTZ mimic). Camera-probed subset of {@link NAVIGATION_ACTION_CATALOG}.
30656
- */
30657
- listActions: method(zod.z.object({ deviceId: zod.z.number() }), zod.z.array(NavigationActionEntrySchema)),
30658
- /**
30659
- * Run one discrete action (a `kind:'action'` dictionary entry). Invalid /
30660
- * unsupported action ids are rejected by the provider.
30661
- */
30662
- runAction: method(zod.z.object({
30663
- deviceId: zod.z.number(),
30664
- actionId: NavigationActionIdSchema
30665
- }), zod.z.void(), { kind: "mutation" }),
30666
- /** Play a sound by its wire id (the `soundId` of a `kind:'sound'` entry). */
30667
- playSound: method(zod.z.object({
30668
- deviceId: zod.z.number(),
30669
- soundId: zod.z.number().int()
30670
- }), zod.z.void(), { kind: "mutation" }),
30671
- /**
30672
- * Turn the on-camera fill light on / off (the `OpenFullLight` control —
30673
- * works anytime, no active stream required).
30674
- */
30675
- setLightOn: method(zod.z.object({
30676
- deviceId: zod.z.number(),
30677
- on: zod.z.boolean()
30678
- }), zod.z.void(), { kind: "mutation" }),
30679
- /**
30680
- * Set the fill-light mode (auto vs manual). `manual` optionally carries the
30681
- * initial `level`. The auto/manual + level control is a CAMERA-service
30682
- * action that generally needs an active camera stream/monitor session — the
30683
- * UI shows the manual level slider ONLY when `mode === 'manual'`.
30684
- */
30685
- setLightMode: method(zod.z.object({
30686
- deviceId: zod.z.number(),
30687
- mode: NavigationLightModeSchema,
30688
- level: zod.z.number().min(40).max(100).optional()
30689
- }), zod.z.void(), { kind: "mutation" }),
30690
- /** Set the MANUAL fill-light level (40..100). Implies `manual` mode. */
30691
- setLightLevel: method(zod.z.object({
30692
- deviceId: zod.z.number(),
30693
- level: zod.z.number().min(40).max(100)
30694
- }), zod.z.void(), { kind: "mutation" }),
30695
- /**
30696
- * Per-device FEATURE-FLAG report for the general primitives — drives which
30697
- * controls the UI shows (the per-entry flags for the dictionary come back on
30698
- * `listActions`).
30699
- */
30700
- getFeatures: method(zod.z.object({ deviceId: zod.z.number() }), NavigationFeaturesSchema)
30701
- },
30702
- events: { onStatusChanged: { data: zod.z.object({
30703
- deviceId: zod.z.number(),
30704
- status: NavigationStatusSchema
30705
- }) } },
30706
- status: {
30707
- schema: NavigationStatusSchema,
30708
- kind: "push"
30709
- },
30710
- /**
30711
- * Runtime-state slice mirrored by the kernel. The navigation panel watches it
30712
- * for live mode / follow / flash changes.
30713
- */
30714
- runtimeState: NavigationRuntimeStateSchema,
30715
- /**
30716
- * Runtime-state durability: **session** — like `vacuum-control`, a restored
30717
- * `mode: cleaning` / `following: true` is a robot that is not actually doing
30718
- * that. The live handle re-publishes on connect.
30719
- *
30720
- * See `RuntimeStateDurability`. Enforced by
30721
- * `scripts/check-runtime-state-durability.ts`.
30722
- */
30723
- durability: "session"
30724
- };
30725
- /**
30726
31034
  * reboot — device-scoped capability for "soft" device reboots (firmware
30727
31035
  * reboot via vendor protocol; cameras, NVRs, doorbells). Surfaces a
30728
31036
  * single mutation so the UI can offer a confirm-and-reboot button for
@@ -34609,6 +34917,63 @@ var RUNTIME_DEFAULTS = {
34609
34917
  "auth.tokenExpiry": "30d"
34610
34918
  };
34611
34919
  /**
34920
+ * Accessory device helpers — shared across drivers.
34921
+ *
34922
+ * Many vendor-specific drivers register accessory child devices on
34923
+ * top of a parent (Reolink: siren / floodlight / PIR / autotrack /
34924
+ * chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
34925
+ * driver picks the right `DeviceType` + `DeviceRole` explicitly when
34926
+ * spawning, builds a name derived from the parent, and produces a
34927
+ * stableId tied to the parent so boot-restore can reconstruct the
34928
+ * relationship.
34929
+ *
34930
+ * Centralised `(kind → DeviceType)` mapping was dropped on purpose:
34931
+ * drivers may reasonably disagree on the right type for an accessory
34932
+ * (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
34933
+ * read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
34934
+ * one canonical mapping was over-prescriptive and added a layer of
34935
+ * indirection without saving meaningful code at call sites — the
34936
+ * driver knows its own hardware best.
34937
+ */
34938
+ /**
34939
+ * Subset of `DeviceRole` values that drivers register as child
34940
+ * accessories of a parent device. Sourced verbatim from `DeviceRole`
34941
+ * — `AccessoryKind` is the alias drivers use when building accessory
34942
+ * children, so the call site reads as
34943
+ * `accessoryStableId(parent, AccessoryKind.Siren)` rather than
34944
+ * `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
34945
+ * any role works, including non-accessory ones like Doorbell).
34946
+ */
34947
+ var AccessoryKind = {
34948
+ Siren: DeviceRole.Siren,
34949
+ Floodlight: DeviceRole.Floodlight,
34950
+ Spotlight: DeviceRole.Spotlight,
34951
+ PirSensor: DeviceRole.PirSensor,
34952
+ Chime: DeviceRole.Chime,
34953
+ Autotrack: DeviceRole.Autotrack,
34954
+ Nightvision: DeviceRole.Nightvision,
34955
+ PrivacyMask: DeviceRole.PrivacyMask
34956
+ };
34957
+ AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
34958
+ /**
34959
+ * THE derivation. One crop rectangle, one presence verdict — see D52 for why
34960
+ * this repo insists a derived value has exactly one implementation.
34961
+ *
34962
+ * `undefined` status ⇒ `sleeping`. A device with no slice has made no claim,
34963
+ * and the caller decides separately whether it is even battery-operated
34964
+ * (`DeviceFeature.BatteryOperated`); this function never answers that
34965
+ * question, only what a battery device is doing.
34966
+ */
34967
+ function deriveBatteryPresence(input) {
34968
+ const status = input.status;
34969
+ if (!status) return "sleeping";
34970
+ if (status.sleeping !== true) return "awake";
34971
+ const lastContactAt = status.lastContactAt;
34972
+ if (typeof lastContactAt !== "number" || lastContactAt <= 0) return "sleeping";
34973
+ const budget = input.unreachableAfterMs ?? 216e5;
34974
+ return input.nowMs - lastContactAt > budget ? "unreachable" : "sleeping";
34975
+ }
34976
+ /**
34612
34977
  * WHICH child a container stands for — one definition, for every consumer.
34613
34978
  *
34614
34979
  * A CONTAINER device has no controllable surface of its own: it groups entity
@@ -34734,63 +35099,6 @@ function resolveContainerPrimaryChild(children, overrideEntityId, containerName)
34734
35099
  const namesake = (c) => containerName !== void 0 && containerName.length > 0 && c.name !== void 0 && c.name.toLowerCase() === containerName.toLowerCase() ? 0 : 1;
34735
35100
  return [...children].toSorted((a, b) => roleRank(a.role) - roleRank(b.role) || namesake(a) - namesake(b) || rank(a.type) - rank(b.type))[0] ?? null;
34736
35101
  }
34737
- /**
34738
- * Accessory device helpers — shared across drivers.
34739
- *
34740
- * Many vendor-specific drivers register accessory child devices on
34741
- * top of a parent (Reolink: siren / floodlight / PIR / autotrack /
34742
- * chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
34743
- * driver picks the right `DeviceType` + `DeviceRole` explicitly when
34744
- * spawning, builds a name derived from the parent, and produces a
34745
- * stableId tied to the parent so boot-restore can reconstruct the
34746
- * relationship.
34747
- *
34748
- * Centralised `(kind → DeviceType)` mapping was dropped on purpose:
34749
- * drivers may reasonably disagree on the right type for an accessory
34750
- * (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
34751
- * read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
34752
- * one canonical mapping was over-prescriptive and added a layer of
34753
- * indirection without saving meaningful code at call sites — the
34754
- * driver knows its own hardware best.
34755
- */
34756
- /**
34757
- * Subset of `DeviceRole` values that drivers register as child
34758
- * accessories of a parent device. Sourced verbatim from `DeviceRole`
34759
- * — `AccessoryKind` is the alias drivers use when building accessory
34760
- * children, so the call site reads as
34761
- * `accessoryStableId(parent, AccessoryKind.Siren)` rather than
34762
- * `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
34763
- * any role works, including non-accessory ones like Doorbell).
34764
- */
34765
- var AccessoryKind = {
34766
- Siren: DeviceRole.Siren,
34767
- Floodlight: DeviceRole.Floodlight,
34768
- Spotlight: DeviceRole.Spotlight,
34769
- PirSensor: DeviceRole.PirSensor,
34770
- Chime: DeviceRole.Chime,
34771
- Autotrack: DeviceRole.Autotrack,
34772
- Nightvision: DeviceRole.Nightvision,
34773
- PrivacyMask: DeviceRole.PrivacyMask
34774
- };
34775
- AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
34776
- /**
34777
- * THE derivation. One crop rectangle, one presence verdict — see D52 for why
34778
- * this repo insists a derived value has exactly one implementation.
34779
- *
34780
- * `undefined` status ⇒ `sleeping`. A device with no slice has made no claim,
34781
- * and the caller decides separately whether it is even battery-operated
34782
- * (`DeviceFeature.BatteryOperated`); this function never answers that
34783
- * question, only what a battery device is doing.
34784
- */
34785
- function deriveBatteryPresence(input) {
34786
- const status = input.status;
34787
- if (!status) return "sleeping";
34788
- if (status.sleeping !== true) return "awake";
34789
- const lastContactAt = status.lastContactAt;
34790
- if (typeof lastContactAt !== "number" || lastContactAt <= 0) return "sleeping";
34791
- const budget = input.unreachableAfterMs ?? 216e5;
34792
- return input.nowMs - lastContactAt > budget ? "unreachable" : "sleeping";
34793
- }
34794
35102
  /** Marker written to a declared integration's `info`. */
34795
35103
  var DECLARED_INTEGRATION_FIXED_KEY = "fixed";
34796
35104
  /**
@@ -35303,6 +35611,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
35303
35611
  storageCapability,
35304
35612
  storageEvictableCapability,
35305
35613
  storageMigrationCapability,
35614
+ storageOccupancyCapability,
35306
35615
  storageProviderCapability,
35307
35616
  streamBrokerCapability,
35308
35617
  streamCatalogCapability,
@@ -40269,13 +40578,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
40269
40578
  addonId: null,
40270
40579
  access: "view"
40271
40580
  },
40272
- "storage.getDefaultLocation": {
40581
+ "storage.list": {
40273
40582
  capName: "storage",
40274
40583
  capScope: "system",
40275
40584
  addonId: null,
40276
40585
  access: "view"
40277
40586
  },
40278
- "storage.list": {
40587
+ "storage.listDrainProgress": {
40279
40588
  capName: "storage",
40280
40589
  capScope: "system",
40281
40590
  addonId: null,
@@ -40425,6 +40734,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
40425
40734
  addonId: null,
40426
40735
  access: "view"
40427
40736
  },
40737
+ "storageOccupancy.getOccupancy": {
40738
+ capName: "storage-occupancy",
40739
+ capScope: "system",
40740
+ addonId: null,
40741
+ access: "view"
40742
+ },
40428
40743
  "storageProvider.abortUpload": {
40429
40744
  capName: "storage-provider",
40430
40745
  capScope: "system",
@@ -44391,6 +44706,12 @@ Object.defineProperty(exports, "LOG_LEVEL_RANK", {
44391
44706
  return LOG_LEVEL_RANK;
44392
44707
  }
44393
44708
  });
44709
+ Object.defineProperty(exports, "LegacyStorageLocationDefaultSchema", {
44710
+ enumerable: true,
44711
+ get: function() {
44712
+ return LegacyStorageLocationDefaultSchema;
44713
+ }
44714
+ });
44394
44715
  Object.defineProperty(exports, "METHOD_ACCESS_MAP", {
44395
44716
  enumerable: true,
44396
44717
  get: function() {
@@ -44463,6 +44784,12 @@ Object.defineProperty(exports, "StorageCleanupJobSchema", {
44463
44784
  return StorageCleanupJobSchema;
44464
44785
  }
44465
44786
  });
44787
+ Object.defineProperty(exports, "StorageLocationModeSchema", {
44788
+ enumerable: true,
44789
+ get: function() {
44790
+ return StorageLocationModeSchema;
44791
+ }
44792
+ });
44466
44793
  Object.defineProperty(exports, "StorageLocationTypeSchema", {
44467
44794
  enumerable: true,
44468
44795
  get: function() {
@@ -44673,6 +45000,12 @@ Object.defineProperty(exports, "evaluateSensorEdge", {
44673
45000
  return evaluateSensorEdge;
44674
45001
  }
44675
45002
  });
45003
+ Object.defineProperty(exports, "evictionPolicyOfLocation", {
45004
+ enumerable: true,
45005
+ get: function() {
45006
+ return evictionPolicyOfLocation;
45007
+ }
45008
+ });
44676
45009
  Object.defineProperty(exports, "expandCapMethods", {
44677
45010
  enumerable: true,
44678
45011
  get: function() {
@@ -44697,6 +45030,12 @@ Object.defineProperty(exports, "foldSnapshotByFunction", {
44697
45030
  return foldSnapshotByFunction;
44698
45031
  }
44699
45032
  });
45033
+ Object.defineProperty(exports, "gbToBytes", {
45034
+ enumerable: true,
45035
+ get: function() {
45036
+ return gbToBytes;
45037
+ }
45038
+ });
44700
45039
  Object.defineProperty(exports, "hydrateSchema", {
44701
45040
  enumerable: true,
44702
45041
  get: function() {
@@ -44721,6 +45060,12 @@ Object.defineProperty(exports, "isDeviceConfigCap", {
44721
45060
  return isDeviceConfigCap;
44722
45061
  }
44723
45062
  });
45063
+ Object.defineProperty(exports, "isLocationEnabled", {
45064
+ enumerable: true,
45065
+ get: function() {
45066
+ return isLocationEnabled;
45067
+ }
45068
+ });
44724
45069
  Object.defineProperty(exports, "isObjectInput", {
44725
45070
  enumerable: true,
44726
45071
  get: function() {
@@ -44751,6 +45096,12 @@ Object.defineProperty(exports, "kebabToCamel", {
44751
45096
  return kebabToCamel;
44752
45097
  }
44753
45098
  });
45099
+ Object.defineProperty(exports, "legacyModeOf", {
45100
+ enumerable: true,
45101
+ get: function() {
45102
+ return legacyModeOf;
45103
+ }
45104
+ });
44754
45105
  Object.defineProperty(exports, "lifecycleJobSchema", {
44755
45106
  enumerable: true,
44756
45107
  get: function() {
@@ -44781,12 +45132,30 @@ Object.defineProperty(exports, "looseSchema", {
44781
45132
  return looseSchema;
44782
45133
  }
44783
45134
  });
45135
+ Object.defineProperty(exports, "mayReadLocation", {
45136
+ enumerable: true,
45137
+ get: function() {
45138
+ return mayReadLocation;
45139
+ }
45140
+ });
45141
+ Object.defineProperty(exports, "mayWriteToLocation", {
45142
+ enumerable: true,
45143
+ get: function() {
45144
+ return mayWriteToLocation;
45145
+ }
45146
+ });
44784
45147
  Object.defineProperty(exports, "metricsProviderCapability", {
44785
45148
  enumerable: true,
44786
45149
  get: function() {
44787
45150
  return metricsProviderCapability;
44788
45151
  }
44789
45152
  });
45153
+ Object.defineProperty(exports, "modeMayWrite", {
45154
+ enumerable: true,
45155
+ get: function() {
45156
+ return modeMayWrite;
45157
+ }
45158
+ });
44790
45159
  Object.defineProperty(exports, "nodePin", {
44791
45160
  enumerable: true,
44792
45161
  get: function() {
@@ -44865,6 +45234,12 @@ Object.defineProperty(exports, "resolveContainerPrimaryChild", {
44865
45234
  return resolveContainerPrimaryChild;
44866
45235
  }
44867
45236
  });
45237
+ Object.defineProperty(exports, "resolveLocationMode", {
45238
+ enumerable: true,
45239
+ get: function() {
45240
+ return resolveLocationMode;
45241
+ }
45242
+ });
44868
45243
  Object.defineProperty(exports, "resolveMethodAuth", {
44869
45244
  enumerable: true,
44870
45245
  get: function() {
@@ -44931,6 +45306,12 @@ Object.defineProperty(exports, "storageMigrationCapability", {
44931
45306
  return storageMigrationCapability;
44932
45307
  }
44933
45308
  });
45309
+ Object.defineProperty(exports, "storageOccupancyCapability", {
45310
+ enumerable: true,
45311
+ get: function() {
45312
+ return storageOccupancyCapability;
45313
+ }
45314
+ });
44934
45315
  Object.defineProperty(exports, "storageProviderCapability", {
44935
45316
  enumerable: true,
44936
45317
  get: function() {
@@ -44961,3 +45342,9 @@ Object.defineProperty(exports, "vectorStoreCapability", {
44961
45342
  return vectorStoreCapability;
44962
45343
  }
44963
45344
  });
45345
+ Object.defineProperty(exports, "withLocationMode", {
45346
+ enumerable: true,
45347
+ get: function() {
45348
+ return withLocationMode;
45349
+ }
45350
+ });