@camstack/addon-pipeline 1.2.198 → 1.2.200

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 (43) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/{default-detection-model-B5v8qEI0.js → default-detection-model-BsmxtyZx.js} +1 -1
  4. package/dist/{default-detection-model-CASpAESz.mjs → default-detection-model-c1LY6thP.mjs} +1 -1
  5. package/dist/detection-pipeline/index.js +5 -5
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-BxP0SvPz.js → dist-D2YTTkRf.js} +436 -419
  8. package/dist/{dist-oFIHnD88.mjs → dist-mEsrAAc3.mjs} +436 -419
  9. package/dist/{lazy-sharp-VKOG3WW4.js → lazy-sharp-B5bW34uC.js} +1 -1
  10. package/dist/motion-wasm/index.js +2 -2
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-B4zETsJ9.mjs → node-CUxep5tc.mjs} +1 -1
  13. package/dist/{node-BIKrNEtS.js → node-x3YokJkD.js} +1 -1
  14. package/dist/pipeline-runner/index.js +8 -7
  15. package/dist/pipeline-runner/index.mjs +7 -6
  16. package/dist/{process-memory-D9xYwm6L.mjs → process-memory-BCqpTruN.mjs} +1 -1
  17. package/dist/{process-memory-CMHOmj75.js → process-memory-CFc4D9A2.js} +1 -1
  18. package/dist/recorder/index.js +169 -334
  19. package/dist/recorder/index.mjs +164 -329
  20. package/dist/restream-intent-B1Difcnq.js +313 -0
  21. package/dist/restream-intent-DWe2kInY.mjs +248 -0
  22. package/dist/{prebuffer-DmBYRQWg.mjs → retire-root-keys-Csb8z2f3.js} +12 -64
  23. package/dist/{prebuffer-DqdeLSsk.js → retire-root-keys-DZvlso2l.mjs} +1 -99
  24. package/dist/{segment-demux-js-DLwL1vmq.js → segment-demux-js-DUZVURQd.js} +1 -1
  25. package/dist/{segment-demux-js-DZysrgdC.mjs → segment-demux-js-xHtB450F.mjs} +1 -1
  26. package/dist/session-decode/decode-worker-child.js +25 -5
  27. package/dist/session-decode/decode-worker-child.mjs +24 -4
  28. package/dist/stream-broker/_stub.js +2 -2
  29. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C2eTdxvd.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CLXImQ3R.mjs} +3 -3
  30. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-NqqfpkYF.mjs +26 -0
  31. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dj_qoc1e.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D62O5bRa.mjs} +1 -1
  32. package/dist/stream-broker/demux-worker-child.js +1 -1
  33. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  34. package/dist/stream-broker/{hostInit-C8tu7NS4.mjs → hostInit-D_cZQhBA.mjs} +3 -3
  35. package/dist/stream-broker/index.js +181 -48
  36. package/dist/stream-broker/index.mjs +172 -39
  37. package/dist/stream-broker/remoteEntry.js +1 -1
  38. package/dist/{worker-protocol-D2mMjUXv.js → worker-protocol-CRDaBlJl.js} +1 -1
  39. package/dist/{worker-protocol-DFlZm_wr.mjs → worker-protocol-oUamwZSn.mjs} +1 -1
  40. package/package.json +1 -1
  41. package/dist/restream-intent-B4BXZra7.mjs +0 -72
  42. package/dist/restream-intent-Cv9x3jmu.js +0 -89
  43. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BcjLrxhF.mjs +0 -26
@@ -8617,362 +8617,6 @@ var CameraSwitchGroupSchema = object({
8617
8617
  fetchedAt: number()
8618
8618
  });
8619
8619
  /**
8620
- * Per-component log CHANNELS — the gate a hot path consults, and the registry
8621
- * an addon declares its channels in.
8622
- *
8623
- * ## Two axes, deliberately separated
8624
- *
8625
- * - **DECLARATION** — which channels exist. Only the addon knows:
8626
- * `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
8627
- * baichuan/handshake. A hand-wired central list rots at the first addition,
8628
- * and rots silently. So a channel is declared where it is consulted, and the
8629
- * `log-channels` capability enumerates the declarations.
8630
- * - **VALUE** — at which level, for which scope, until when. That stays ONE
8631
- * thing: the logging settings document on the `system` cap. Two authorities
8632
- * over the values is the exact defect
8633
- * `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
8634
- * remove; re-introducing it from the cure side would be grotesque.
8635
- *
8636
- * Nothing in this file reads a clock, an env var or a store. The registry is
8637
- * a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
8638
- * the hot path with a value somebody actually read, and by
8639
- * {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
8640
- * never reaches here, so it can neither disarm an armed channel nor arm a
8641
- * disarmed one (D49).
8642
- *
8643
- * ## The canonical call shape
8644
- *
8645
- * ```ts
8646
- * if (CH_RTP.on && CH_RTP.wants(deviceId)) {
8647
- * CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
8648
- * }
8649
- * ```
8650
- *
8651
- * `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
8652
- * read. Disarmed, a call site costs one load and one branch, and the `extras`
8653
- * object literal is never constructed because it lives inside the branch. It
8654
- * is the same shape already proven in production at `stream-broker.ts:1650`,
8655
- * and the same discipline `LoggingGate.allowsDestination` uses for the
8656
- * destination floor (measured at 1.93 ns/call when off).
8657
- *
8658
- * ## Why a channel emits at `info`
8659
- *
8660
- * `loki-logging.addon.ts` pins the destination default at `info` and
8661
- * `loki-destination.ts` drops everything below it, so a line emitted at
8662
- * `debug` never reaches Loki and the hub's in-memory ring only holds ~35
8663
- * minutes. A diagnostic that cannot be read an hour later is worse than no
8664
- * diagnostic, because it looks done. {@link LogChannelGate.log} therefore
8665
- * emits at the channel's declared level, whose schema floor is `info`.
8666
- */
8667
- /**
8668
- * The level a channel writes at once armed.
8669
- *
8670
- * `debug` is absent ON PURPOSE and not by omission: below `info` the line does
8671
- * not leave the process for Loki, and the whole point of arming a channel is
8672
- * to read it later.
8673
- */
8674
- var LogChannelLevelSchema = _enum([
8675
- "info",
8676
- "warn",
8677
- "error"
8678
- ]);
8679
- /**
8680
- * What an addon declares about one channel. No value, no state — a
8681
- * declaration is inert.
8682
- */
8683
- var LogChannelDescriptorSchema = object({
8684
- /**
8685
- * Dotted `area.thing`, unique across the workspace. `area` is conventionally
8686
- * the addon's short name so an operator reading a channel list can tell who
8687
- * owns it without a second lookup.
8688
- */
8689
- name: string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
8690
- /** One sentence: what the operator will SEE after arming it. */
8691
- description: string().min(1),
8692
- /** The level its lines are emitted at. Never below `info`. */
8693
- defaultLevel: LogChannelLevelSchema,
8694
- /**
8695
- * Whether this channel can be narrowed to a camera.
8696
- *
8697
- * `true` is a PROMISE with two halves, and both must hold: the gate is
8698
- * consulted with the numeric device id, AND every line the channel admits
8699
- * carries `tags: { deviceId }` with that same numeric id. The second half is
8700
- * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
8701
- * keeps `deviceId` out of the stream labels for cardinality, so the tag in
8702
- * the body is the only way to filter.
8703
- *
8704
- * A channel whose lines carry the device only in `meta` (or not at all) is
8705
- * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
8706
- * the operator narrows to one camera, sees nothing, and concludes the code
8707
- * path was never taken.
8708
- */
8709
- perDevice: boolean()
8710
- });
8711
- /**
8712
- * An armed window over one channel, as the document hands it to a mirror.
8713
- *
8714
- * A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
8715
- * expires by itself, which is the one failure a boolean cannot avoid.
8716
- */
8717
- var LogChannelWindowSchema = object({
8718
- channel: string().min(1),
8719
- /** Epoch ms the window closes at. */
8720
- armedUntilMs: number(),
8721
- /** `null` = every camera. A non-empty list narrows to those numeric ids. */
8722
- deviceIds: array(number().int()).readonly().nullable()
8723
- });
8724
- /**
8725
- * The gate a hot path holds.
8726
- *
8727
- * Obtain it ONCE — at module scope or in a constructor — and keep the
8728
- * reference. Looking a channel up by name per line would put a Map lookup on
8729
- * the path this class exists to keep free.
8730
- */
8731
- var LogChannelGate = class {
8732
- descriptor;
8733
- /**
8734
- * HOT PATH GUARD. A plain data FIELD, and it must stay one.
8735
- *
8736
- * `log-channel.spec.ts` asserts the property descriptor has no getter and
8737
- * booby-traps the device set, so turning this into an accessor — or reading
8738
- * anything before it — fails the spec instead of taxing every line the
8739
- * process emits.
8740
- */
8741
- on = false;
8742
- /** `null` while armed for every camera. Never read while `on` is false. */
8743
- devices = null;
8744
- level;
8745
- closesAtMs = 0;
8746
- constructor(descriptor) {
8747
- this.descriptor = descriptor;
8748
- this.level = descriptor.defaultLevel;
8749
- }
8750
- /** Epoch ms this channel disarms itself at. 0 when disarmed. */
8751
- get armedUntilMs() {
8752
- return this.on ? this.closesAtMs : 0;
8753
- }
8754
- /**
8755
- * Does this channel want a line about `deviceId`?
8756
- *
8757
- * Call it only behind `gate.on &&`. On its own it is still correct — the
8758
- * guard is repeated inside — but the point of the prefix is that a disarmed
8759
- * channel must not pay the call at all.
8760
- */
8761
- wants(deviceId) {
8762
- if (!this.on) return false;
8763
- return this.devices === null || this.devices.has(deviceId);
8764
- }
8765
- /**
8766
- * Emit one line on this channel, at the channel's declared level.
8767
- *
8768
- * The channel name is added as `tags.logChannel` so LogQL can select the
8769
- * channel without matching on the message text, and whatever `tags` the
8770
- * caller passed — `deviceId` above all — is preserved.
8771
- */
8772
- log(logger, message, extras) {
8773
- if (!this.on) return;
8774
- const tags = {
8775
- ...extras.tags,
8776
- logChannel: this.descriptor.name
8777
- };
8778
- const line = {
8779
- ...extras,
8780
- tags
8781
- };
8782
- if (this.level === "error") logger.error(message, line);
8783
- else if (this.level === "warn") logger.warn(message, line);
8784
- else logger.info(message, line);
8785
- }
8786
- /**
8787
- * Arm (or RE-arm, restarting) this channel. Off the hot path only.
8788
- *
8789
- * An empty `deviceIds` list is treated as "every camera" rather than "no
8790
- * camera": a window that matches nothing is indistinguishable from a
8791
- * disarmed one, and the operator who asked for it would wait for lines that
8792
- * can never come.
8793
- */
8794
- arm(window) {
8795
- const ids = window.deviceIds;
8796
- this.devices = ids === null || ids.length === 0 ? null : new Set(ids);
8797
- this.closesAtMs = window.armedUntilMs;
8798
- this.on = true;
8799
- }
8800
- /** Disarm. Off the hot path only. */
8801
- disarm() {
8802
- this.on = false;
8803
- this.devices = null;
8804
- this.closesAtMs = 0;
8805
- }
8806
- };
8807
- /**
8808
- * Every channel this PROCESS declares, and the mirror of what is armed on it.
8809
- *
8810
- * One per process. A forked runner has its own, and it is refreshed through
8811
- * the `log-channels` capability by the hub that owns the document — the
8812
- * registry never reaches for a value itself.
8813
- */
8814
- var LogChannelRegistry = class {
8815
- gates = /* @__PURE__ */ new Map();
8816
- /**
8817
- * Declare a channel and get its gate.
8818
- *
8819
- * A duplicate name throws. Two declarations of one name is a programming
8820
- * error, not a merge: the operator would arm one and the other would stay
8821
- * dark, which is the dead-knob shape (D62) with an extra step.
8822
- */
8823
- declare(descriptor) {
8824
- const parsed = LogChannelDescriptorSchema.parse(descriptor);
8825
- if (this.gates.get(parsed.name) !== void 0) throw new Error(`log channel "${parsed.name}" is already declared in this process — two declarations of one name is a programming error, not a merge`);
8826
- const gate = new LogChannelGate(parsed);
8827
- this.gates.set(parsed.name, gate);
8828
- return gate;
8829
- }
8830
- /** The declarations, sorted by name so a list is stable to read and diff. */
8831
- list() {
8832
- return [...this.gates.values()].map((gate) => gate.descriptor).sort((a, b) => a.name.localeCompare(b.name));
8833
- }
8834
- /** The gate for a declared channel, or `undefined`. */
8835
- gate(name) {
8836
- return this.gates.get(name);
8837
- }
8838
- /**
8839
- * Apply the FULL set of armed windows. Off the hot path.
8840
- *
8841
- * Full, not incremental, and that is the whole design: the document is the
8842
- * authority, so a channel the document does not name is disarmed here. An
8843
- * incremental apply would let a disarm get lost in transit and leave a
8844
- * channel running that nobody can see is running.
8845
- *
8846
- * A window already past its deadline is ignored rather than armed — a
8847
- * restore that re-armed an expired window would make a forgotten diagnostic
8848
- * immortal across restarts.
8849
- *
8850
- * Returns the names it could not place, so the caller can log them: a
8851
- * channel named in the document that this process does not declare is
8852
- * either a typo or an addon that has not booted yet, and both deserve a
8853
- * line rather than silence.
8854
- */
8855
- apply(windows, nowMs) {
8856
- const wanted = /* @__PURE__ */ new Map();
8857
- const unknown = [];
8858
- for (const window of windows) {
8859
- if (window.armedUntilMs <= nowMs) continue;
8860
- if (!this.gates.has(window.channel)) {
8861
- unknown.push(window.channel);
8862
- continue;
8863
- }
8864
- wanted.set(window.channel, window);
8865
- }
8866
- for (const [name, gate] of this.gates) {
8867
- const window = wanted.get(name);
8868
- if (window === void 0) gate.disarm();
8869
- else gate.arm(window);
8870
- }
8871
- return unknown;
8872
- }
8873
- /**
8874
- * Disarm whatever has run out. Called on a timer, NEVER from a log path — a
8875
- * diagnostic that adds a `Date.now()` to the path it is measuring measures
8876
- * itself.
8877
- *
8878
- * Returns the names it closed, so the caller can write the one line that
8879
- * says a window ended and stops "it went quiet" from reading as "the branch
8880
- * was not taken".
8881
- */
8882
- tick(nowMs) {
8883
- const closed = [];
8884
- for (const [name, gate] of this.gates) if (gate.on && gate.armedUntilMs <= nowMs) {
8885
- gate.disarm();
8886
- closed.push(name);
8887
- }
8888
- return closed;
8889
- }
8890
- /** The channels armed right now, as the document would describe them. */
8891
- armed() {
8892
- const out = [];
8893
- for (const [name, gate] of this.gates) if (gate.on) out.push({
8894
- channel: name,
8895
- armedUntilMs: gate.armedUntilMs,
8896
- deviceIds: null
8897
- });
8898
- return out;
8899
- }
8900
- };
8901
- /**
8902
- * Process-wide holder for the {@link LogChannelRegistry}.
8903
- *
8904
- * Three call sites that never meet need the SAME instance: the hot paths that
8905
- * declare a gate at module scope, the `log-channels` provider that enumerates
8906
- * the declarations for the hub, and the same provider applying the windows the
8907
- * document hands down. A registry built inside any one of them would be
8908
- * refreshed and collected — the shape of a knob that never does anything.
8909
- *
8910
- * Same idiom as `logging-gate.singleton.ts` and
8911
- * `http-request-census.singleton.ts`.
8912
- */
8913
- var instance = null;
8914
- /** The process-wide log channel registry. Created empty on first use. */
8915
- function getLogChannelRegistry() {
8916
- instance ??= new LogChannelRegistry();
8917
- return instance;
8918
- }
8919
- /**
8920
- * Declare a channel on the process-wide registry and get its gate.
8921
- *
8922
- * The one call an addon makes. Keep the returned gate in a module-scope
8923
- * `const`: looking a channel up by name per line would put a Map lookup on
8924
- * exactly the path this mechanism exists to keep free.
8925
- *
8926
- * `scripts/check-log-channel-gated.ts` reads these call sites. It pairs the
8927
- * declared name with the binding it is assigned to and refuses to let a
8928
- * channel ship that no `<binding>.on` anywhere consults — a declared channel
8929
- * nobody reads is a knob the operator turns with nothing happening, forever,
8930
- * and without a line. That is D62, and this repo has now shipped it three
8931
- * times (`audioThresholdDbfs`, the HA entities with no source, the second
8932
- * per-camera switch that wrote a store nobody read).
8933
- */
8934
- function declareLogChannel(descriptor) {
8935
- return getLogChannelRegistry().declare(descriptor);
8936
- }
8937
- /**
8938
- * Build the `log-channels` provider for this process.
8939
- *
8940
- * `logger` is used ONLY off the hot path — for the arm/expiry lines — so a
8941
- * channel that is never armed costs this module nothing but a timer.
8942
- */
8943
- function createLogChannelsProvider(logger, options = {}) {
8944
- const registry = getLogChannelRegistry();
8945
- const now = options.now ?? Date.now;
8946
- const tickMs = options.tickMs ?? 5e3;
8947
- const timer = setInterval(() => {
8948
- const closed = registry.tick(now());
8949
- for (const name of closed) logger.info("log channel window closed", {
8950
- tags: { logChannel: name },
8951
- meta: { channel: name }
8952
- });
8953
- }, tickMs);
8954
- timer.unref?.();
8955
- return {
8956
- list: () => registry.list(),
8957
- apply: (input) => {
8958
- const unknown = registry.apply(input.windows, now());
8959
- const armed = registry.armed();
8960
- logger.info("log channels applied", { meta: {
8961
- armed: armed.map((window) => window.channel),
8962
- unknown,
8963
- declared: registry.list().length
8964
- } });
8965
- return {
8966
- armed: armed.length,
8967
- unknown
8968
- };
8969
- },
8970
- stop: () => {
8971
- clearInterval(timer);
8972
- }
8973
- };
8974
- }
8975
- /**
8976
8620
  * Ops-log — the durable, append-only operations audit shared by the
8977
8621
  * recordings and events management surfaces.
8978
8622
  *
@@ -10111,8 +9755,11 @@ var StorageLocationTypeSchema = string().regex(/^[a-z][a-zA-Z0-9-]*$/);
10111
9755
  * `STORAGE_LOCATION_CARDINALITY` map has been removed.
10112
9756
  *
10113
9757
  * `id` is a stable namespaced string of the form `<type>:<slug>`.
10114
- * The default location for a type uses `id === <type>:default` by
10115
- * convention (the bare type ref like `'backups'` resolves to it).
9758
+ * The seed names its first instance `<type>:default` — a NAME, not a flag.
9759
+ * There is no default location any more (D383): `enabled` is the whole write
9760
+ * model, and a bare type ref resolves to the sole location of the type, or —
9761
+ * transitionally, only while legacy NULL-stamped rows exist — to the row whose
9762
+ * slug is `default`.
10116
9763
  *
10117
9764
  * `isSystem` is a legacy persisted flag. Seed still creates the initial
10118
9765
  * `<type>:default` locations; the flag is no longer a lock, a badge, or a
@@ -10133,21 +9780,20 @@ var StorageLocationSchema = object({
10133
9780
  * flag at upsert time, not here (the schema is provider-agnostic).
10134
9781
  */
10135
9782
  nodeId: string().optional(),
10136
- isDefault: boolean().default(false),
10137
9783
  isSystem: boolean().default(false),
10138
9784
  /**
10139
- * Operator opt-in: whether consumers that BALANCE across several locations
10140
- * of a type may write here. Recordings reads it today; event media and
10141
- * backups are the next consumers, which is why the flag lives on the
10142
- * location rather than in any one addon's store — nothing has to be
10143
- * extended to add the next consumer.
9785
+ * THE write switch, and the only one (D383). `enabled: true` means every
9786
+ * consumer that chooses a write target for this type may write here, and all
9787
+ * enabled locations of a type are used TOGETHER; `false` means read-only —
9788
+ * still read, still played back, still age-swept, still drained, never
9789
+ * written.
10144
9790
  *
10145
- * OPTIONAL, and ABSENT MEANS ACTIVE. Every location persisted before the
10146
- * flag existed reads back with no flag and keeps working exactly as before;
10147
- * that is the whole compat story, and it is why no migration ships with it.
10148
- * A newly CREATED sibling is stamped `false` by the orchestrator (creating a
10149
- * disk must not silently start writing to it); the default of a type is
10150
- * always stamped `true`.
9791
+ * OPTIONAL only for the wire: an upsert that omits it means "leave what is
9792
+ * stored" on an update and "born inert unless it is the first location of its
9793
+ * type" on a create. On a PERSISTED row absence is legacy and it means
9794
+ * enabled — {@link isLocationEnabled} is the one place that says so, and the
9795
+ * orchestrator stamps every flagless row `true` once at hydrate so absence
9796
+ * stops existing rather than being re-derived on every read.
10151
9797
  */
10152
9798
  enabled: boolean().optional(),
10153
9799
  /** COMPUTED at read time by the orchestrator (statfs of the backing volume
@@ -10160,10 +9806,12 @@ var StorageLocationSchema = object({
10160
9806
  createdAt: number(),
10161
9807
  updatedAt: number()
10162
9808
  });
9809
+ object({ isDefault: boolean().optional() });
10163
9810
  /**
10164
9811
  * Reference accepted by consumer-facing `api.storage.*` calls.
10165
9812
  * Either:
10166
- * - a `StorageLocationType` (e.g. `'backups'`) → orchestrator resolves to the default of that type
9813
+ * - a `StorageLocationType` (e.g. `'backups'`) → the sole location of that type
9814
+ * (transitionally, the `<type>:default`-slugged row when several exist)
10167
9815
  * - a fully-qualified id (e.g. `'backups:nas-01'`) → addresses a specific instance
10168
9816
  *
10169
9817
  * The orchestrator's `resolveRef(ref)` handles both cases.
@@ -10304,41 +9952,397 @@ var DecoderSessionConfigSchema = object({
10304
9952
  width: number().optional(),
10305
9953
  height: number().optional(),
10306
9954
  /**
10307
- * Identifier of the camera this decoder session serves. Optional
10308
- * because the cap is generic (any caller could request decode), but
10309
- * stream-broker passes it so decoder logs include `deviceId` for
10310
- * per-camera filtering when diagnosing failures (e.g. node-av
10311
- * sendPacket errors on a single hung camera).
9955
+ * Identifier of the camera this decoder session serves. Optional
9956
+ * because the cap is generic (any caller could request decode), but
9957
+ * stream-broker passes it so decoder logs include `deviceId` for
9958
+ * per-camera filtering when diagnosing failures (e.g. node-av
9959
+ * sendPacket errors on a single hung camera).
9960
+ */
9961
+ deviceId: number().int().nonnegative().optional(),
9962
+ /**
9963
+ * Free-form tag for log scoping. Stream-broker uses
9964
+ * `broker:<deviceId>/<profile>`. Decoder session logger surfaces it
9965
+ * on every line so `grep tag=broker:5/high` filters one camera
9966
+ * profile cleanly.
9967
+ */
9968
+ tag: string().optional(),
9969
+ /**
9970
+ * Where the session delivers decoded frames (Phase 5 / D9):
9971
+ *
9972
+ * - `'callback'` (default) — the legacy pixel path: decoded frames are
9973
+ * buffered as `DecodedFrame`s and drained via `pullFrames`.
9974
+ * - `'shm'` — the shared-memory frame plane: decoded frames are written
9975
+ * into an OS shared-memory ring and drained as zero-pixel
9976
+ * `FrameHandle`s via `pullHandles`. A session is one mode or the
9977
+ * other — `pullFrames` returns nothing for an `'shm'` session and
9978
+ * `pullHandles` returns nothing for a `'callback'` session.
9979
+ */
9980
+ frameSink: _enum(["callback", "shm"]).default("callback"),
9981
+ /**
9982
+ * Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
9983
+ * a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
9984
+ * real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
9985
+ * stream-broker's `streamingDebug` gate — off by default so production logs
9986
+ * stay quiet and the emit path pays zero per-frame cost when disabled.
9987
+ */
9988
+ debug: boolean().optional()
9989
+ });
9990
+ /**
9991
+ * Per-component log CHANNELS — the gate a hot path consults, and the registry
9992
+ * an addon declares its channels in.
9993
+ *
9994
+ * ## Two axes, deliberately separated
9995
+ *
9996
+ * - **DECLARATION** — which channels exist. Only the addon knows:
9997
+ * `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
9998
+ * baichuan/handshake. A hand-wired central list rots at the first addition,
9999
+ * and rots silently. So a channel is declared where it is consulted, and the
10000
+ * `log-channels` capability enumerates the declarations.
10001
+ * - **VALUE** — at which level, for which scope, until when. That stays ONE
10002
+ * thing: the logging settings document on the `system` cap. Two authorities
10003
+ * over the values is the exact defect
10004
+ * `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
10005
+ * remove; re-introducing it from the cure side would be grotesque.
10006
+ *
10007
+ * Nothing in this file reads a clock, an env var or a store. The registry is
10008
+ * a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
10009
+ * the hot path with a value somebody actually read, and by
10010
+ * {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
10011
+ * never reaches here, so it can neither disarm an armed channel nor arm a
10012
+ * disarmed one (D49).
10013
+ *
10014
+ * ## The canonical call shape
10015
+ *
10016
+ * ```ts
10017
+ * if (CH_RTP.on && CH_RTP.wants(deviceId)) {
10018
+ * CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
10019
+ * }
10020
+ * ```
10021
+ *
10022
+ * `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
10023
+ * read. Disarmed, a call site costs one load and one branch, and the `extras`
10024
+ * object literal is never constructed because it lives inside the branch. It
10025
+ * is the same shape already proven in production at `stream-broker.ts:1650`,
10026
+ * and the same discipline `LoggingGate.allowsDestination` uses for the
10027
+ * destination floor (measured at 1.93 ns/call when off).
10028
+ *
10029
+ * ## Why a channel emits at `info`
10030
+ *
10031
+ * `loki-logging.addon.ts` pins the destination default at `info` and
10032
+ * `loki-destination.ts` drops everything below it, so a line emitted at
10033
+ * `debug` never reaches Loki and the hub's in-memory ring only holds ~35
10034
+ * minutes. A diagnostic that cannot be read an hour later is worse than no
10035
+ * diagnostic, because it looks done. {@link LogChannelGate.log} therefore
10036
+ * emits at the channel's declared level, whose schema floor is `info`.
10037
+ */
10038
+ /**
10039
+ * The level a channel writes at once armed.
10040
+ *
10041
+ * `debug` is absent ON PURPOSE and not by omission: below `info` the line does
10042
+ * not leave the process for Loki, and the whole point of arming a channel is
10043
+ * to read it later.
10044
+ */
10045
+ var LogChannelLevelSchema = _enum([
10046
+ "info",
10047
+ "warn",
10048
+ "error"
10049
+ ]);
10050
+ /**
10051
+ * What an addon declares about one channel. No value, no state — a
10052
+ * declaration is inert.
10053
+ */
10054
+ var LogChannelDescriptorSchema = object({
10055
+ /**
10056
+ * Dotted `area.thing`, unique across the workspace. `area` is conventionally
10057
+ * the addon's short name so an operator reading a channel list can tell who
10058
+ * owns it without a second lookup.
10059
+ */
10060
+ name: string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
10061
+ /** One sentence: what the operator will SEE after arming it. */
10062
+ description: string().min(1),
10063
+ /** The level its lines are emitted at. Never below `info`. */
10064
+ defaultLevel: LogChannelLevelSchema,
10065
+ /**
10066
+ * Whether this channel can be narrowed to a camera.
10067
+ *
10068
+ * `true` is a PROMISE with two halves, and both must hold: the gate is
10069
+ * consulted with the numeric device id, AND every line the channel admits
10070
+ * carries `tags: { deviceId }` with that same numeric id. The second half is
10071
+ * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
10072
+ * keeps `deviceId` out of the stream labels for cardinality, so the tag in
10073
+ * the body is the only way to filter.
10074
+ *
10075
+ * A channel whose lines carry the device only in `meta` (or not at all) is
10076
+ * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
10077
+ * the operator narrows to one camera, sees nothing, and concludes the code
10078
+ * path was never taken.
10079
+ */
10080
+ perDevice: boolean()
10081
+ });
10082
+ /**
10083
+ * An armed window over one channel, as the document hands it to a mirror.
10084
+ *
10085
+ * A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
10086
+ * expires by itself, which is the one failure a boolean cannot avoid.
10087
+ */
10088
+ var LogChannelWindowSchema = object({
10089
+ channel: string().min(1),
10090
+ /** Epoch ms the window closes at. */
10091
+ armedUntilMs: number(),
10092
+ /** `null` = every camera. A non-empty list narrows to those numeric ids. */
10093
+ deviceIds: array(number().int()).readonly().nullable()
10094
+ });
10095
+ /**
10096
+ * The gate a hot path holds.
10097
+ *
10098
+ * Obtain it ONCE — at module scope or in a constructor — and keep the
10099
+ * reference. Looking a channel up by name per line would put a Map lookup on
10100
+ * the path this class exists to keep free.
10101
+ */
10102
+ var LogChannelGate = class {
10103
+ descriptor;
10104
+ /**
10105
+ * HOT PATH GUARD. A plain data FIELD, and it must stay one.
10106
+ *
10107
+ * `log-channel.spec.ts` asserts the property descriptor has no getter and
10108
+ * booby-traps the device set, so turning this into an accessor — or reading
10109
+ * anything before it — fails the spec instead of taxing every line the
10110
+ * process emits.
10111
+ */
10112
+ on = false;
10113
+ /** `null` while armed for every camera. Never read while `on` is false. */
10114
+ devices = null;
10115
+ level;
10116
+ closesAtMs = 0;
10117
+ constructor(descriptor) {
10118
+ this.descriptor = descriptor;
10119
+ this.level = descriptor.defaultLevel;
10120
+ }
10121
+ /** Epoch ms this channel disarms itself at. 0 when disarmed. */
10122
+ get armedUntilMs() {
10123
+ return this.on ? this.closesAtMs : 0;
10124
+ }
10125
+ /**
10126
+ * Does this channel want a line about `deviceId`?
10127
+ *
10128
+ * Call it only behind `gate.on &&`. On its own it is still correct — the
10129
+ * guard is repeated inside — but the point of the prefix is that a disarmed
10130
+ * channel must not pay the call at all.
10131
+ */
10132
+ wants(deviceId) {
10133
+ if (!this.on) return false;
10134
+ return this.devices === null || this.devices.has(deviceId);
10135
+ }
10136
+ /**
10137
+ * Emit one line on this channel, at the channel's declared level.
10138
+ *
10139
+ * The channel name is added as `tags.logChannel` so LogQL can select the
10140
+ * channel without matching on the message text, and whatever `tags` the
10141
+ * caller passed — `deviceId` above all — is preserved.
10312
10142
  */
10313
- deviceId: number().int().nonnegative().optional(),
10143
+ log(logger, message, extras) {
10144
+ if (!this.on) return;
10145
+ const tags = {
10146
+ ...extras.tags,
10147
+ logChannel: this.descriptor.name
10148
+ };
10149
+ const line = {
10150
+ ...extras,
10151
+ tags
10152
+ };
10153
+ if (this.level === "error") logger.error(message, line);
10154
+ else if (this.level === "warn") logger.warn(message, line);
10155
+ else logger.info(message, line);
10156
+ }
10314
10157
  /**
10315
- * Free-form tag for log scoping. Stream-broker uses
10316
- * `broker:<deviceId>/<profile>`. Decoder session logger surfaces it
10317
- * on every line so `grep tag=broker:5/high` filters one camera
10318
- * profile cleanly.
10158
+ * Arm (or RE-arm, restarting) this channel. Off the hot path only.
10159
+ *
10160
+ * An empty `deviceIds` list is treated as "every camera" rather than "no
10161
+ * camera": a window that matches nothing is indistinguishable from a
10162
+ * disarmed one, and the operator who asked for it would wait for lines that
10163
+ * can never come.
10319
10164
  */
10320
- tag: string().optional(),
10165
+ arm(window) {
10166
+ const ids = window.deviceIds;
10167
+ this.devices = ids === null || ids.length === 0 ? null : new Set(ids);
10168
+ this.closesAtMs = window.armedUntilMs;
10169
+ this.on = true;
10170
+ }
10171
+ /** Disarm. Off the hot path only. */
10172
+ disarm() {
10173
+ this.on = false;
10174
+ this.devices = null;
10175
+ this.closesAtMs = 0;
10176
+ }
10177
+ };
10178
+ /**
10179
+ * Every channel this PROCESS declares, and the mirror of what is armed on it.
10180
+ *
10181
+ * One per process. A forked runner has its own, and it is refreshed through
10182
+ * the `log-channels` capability by the hub that owns the document — the
10183
+ * registry never reaches for a value itself.
10184
+ */
10185
+ var LogChannelRegistry = class {
10186
+ gates = /* @__PURE__ */ new Map();
10321
10187
  /**
10322
- * Where the session delivers decoded frames (Phase 5 / D9):
10188
+ * Declare a channel and get its gate.
10323
10189
  *
10324
- * - `'callback'` (default) — the legacy pixel path: decoded frames are
10325
- * buffered as `DecodedFrame`s and drained via `pullFrames`.
10326
- * - `'shm'` — the shared-memory frame plane: decoded frames are written
10327
- * into an OS shared-memory ring and drained as zero-pixel
10328
- * `FrameHandle`s via `pullHandles`. A session is one mode or the
10329
- * other — `pullFrames` returns nothing for an `'shm'` session and
10330
- * `pullHandles` returns nothing for a `'callback'` session.
10190
+ * A duplicate name throws. Two declarations of one name is a programming
10191
+ * error, not a merge: the operator would arm one and the other would stay
10192
+ * dark, which is the dead-knob shape (D62) with an extra step.
10331
10193
  */
10332
- frameSink: _enum(["callback", "shm"]).default("callback"),
10194
+ declare(descriptor) {
10195
+ const parsed = LogChannelDescriptorSchema.parse(descriptor);
10196
+ if (this.gates.get(parsed.name) !== void 0) throw new Error(`log channel "${parsed.name}" is already declared in this process — two declarations of one name is a programming error, not a merge`);
10197
+ const gate = new LogChannelGate(parsed);
10198
+ this.gates.set(parsed.name, gate);
10199
+ return gate;
10200
+ }
10201
+ /** The declarations, sorted by name so a list is stable to read and diff. */
10202
+ list() {
10203
+ return [...this.gates.values()].map((gate) => gate.descriptor).sort((a, b) => a.name.localeCompare(b.name));
10204
+ }
10205
+ /** The gate for a declared channel, or `undefined`. */
10206
+ gate(name) {
10207
+ return this.gates.get(name);
10208
+ }
10333
10209
  /**
10334
- * Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
10335
- * a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
10336
- * real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
10337
- * stream-broker's `streamingDebug` gate — off by default so production logs
10338
- * stay quiet and the emit path pays zero per-frame cost when disabled.
10210
+ * Apply the FULL set of armed windows. Off the hot path.
10211
+ *
10212
+ * Full, not incremental, and that is the whole design: the document is the
10213
+ * authority, so a channel the document does not name is disarmed here. An
10214
+ * incremental apply would let a disarm get lost in transit and leave a
10215
+ * channel running that nobody can see is running.
10216
+ *
10217
+ * A window already past its deadline is ignored rather than armed — a
10218
+ * restore that re-armed an expired window would make a forgotten diagnostic
10219
+ * immortal across restarts.
10220
+ *
10221
+ * Returns the names it could not place, so the caller can log them: a
10222
+ * channel named in the document that this process does not declare is
10223
+ * either a typo or an addon that has not booted yet, and both deserve a
10224
+ * line rather than silence.
10339
10225
  */
10340
- debug: boolean().optional()
10341
- });
10226
+ apply(windows, nowMs) {
10227
+ const wanted = /* @__PURE__ */ new Map();
10228
+ const unknown = [];
10229
+ for (const window of windows) {
10230
+ if (window.armedUntilMs <= nowMs) continue;
10231
+ if (!this.gates.has(window.channel)) {
10232
+ unknown.push(window.channel);
10233
+ continue;
10234
+ }
10235
+ wanted.set(window.channel, window);
10236
+ }
10237
+ for (const [name, gate] of this.gates) {
10238
+ const window = wanted.get(name);
10239
+ if (window === void 0) gate.disarm();
10240
+ else gate.arm(window);
10241
+ }
10242
+ return unknown;
10243
+ }
10244
+ /**
10245
+ * Disarm whatever has run out. Called on a timer, NEVER from a log path — a
10246
+ * diagnostic that adds a `Date.now()` to the path it is measuring measures
10247
+ * itself.
10248
+ *
10249
+ * Returns the names it closed, so the caller can write the one line that
10250
+ * says a window ended and stops "it went quiet" from reading as "the branch
10251
+ * was not taken".
10252
+ */
10253
+ tick(nowMs) {
10254
+ const closed = [];
10255
+ for (const [name, gate] of this.gates) if (gate.on && gate.armedUntilMs <= nowMs) {
10256
+ gate.disarm();
10257
+ closed.push(name);
10258
+ }
10259
+ return closed;
10260
+ }
10261
+ /** The channels armed right now, as the document would describe them. */
10262
+ armed() {
10263
+ const out = [];
10264
+ for (const [name, gate] of this.gates) if (gate.on) out.push({
10265
+ channel: name,
10266
+ armedUntilMs: gate.armedUntilMs,
10267
+ deviceIds: null
10268
+ });
10269
+ return out;
10270
+ }
10271
+ };
10272
+ /**
10273
+ * Process-wide holder for the {@link LogChannelRegistry}.
10274
+ *
10275
+ * Three call sites that never meet need the SAME instance: the hot paths that
10276
+ * declare a gate at module scope, the `log-channels` provider that enumerates
10277
+ * the declarations for the hub, and the same provider applying the windows the
10278
+ * document hands down. A registry built inside any one of them would be
10279
+ * refreshed and collected — the shape of a knob that never does anything.
10280
+ *
10281
+ * Same idiom as `logging-gate.singleton.ts` and
10282
+ * `http-request-census.singleton.ts`.
10283
+ */
10284
+ var instance = null;
10285
+ /** The process-wide log channel registry. Created empty on first use. */
10286
+ function getLogChannelRegistry() {
10287
+ instance ??= new LogChannelRegistry();
10288
+ return instance;
10289
+ }
10290
+ /**
10291
+ * Declare a channel on the process-wide registry and get its gate.
10292
+ *
10293
+ * The one call an addon makes. Keep the returned gate in a module-scope
10294
+ * `const`: looking a channel up by name per line would put a Map lookup on
10295
+ * exactly the path this mechanism exists to keep free.
10296
+ *
10297
+ * `scripts/check-log-channel-gated.ts` reads these call sites. It pairs the
10298
+ * declared name with the binding it is assigned to and refuses to let a
10299
+ * channel ship that no `<binding>.on` anywhere consults — a declared channel
10300
+ * nobody reads is a knob the operator turns with nothing happening, forever,
10301
+ * and without a line. That is D62, and this repo has now shipped it three
10302
+ * times (`audioThresholdDbfs`, the HA entities with no source, the second
10303
+ * per-camera switch that wrote a store nobody read).
10304
+ */
10305
+ function declareLogChannel(descriptor) {
10306
+ return getLogChannelRegistry().declare(descriptor);
10307
+ }
10308
+ /**
10309
+ * Build the `log-channels` provider for this process.
10310
+ *
10311
+ * `logger` is used ONLY off the hot path — for the arm/expiry lines — so a
10312
+ * channel that is never armed costs this module nothing but a timer.
10313
+ */
10314
+ function createLogChannelsProvider(logger, options = {}) {
10315
+ const registry = getLogChannelRegistry();
10316
+ const now = options.now ?? Date.now;
10317
+ const tickMs = options.tickMs ?? 5e3;
10318
+ const timer = setInterval(() => {
10319
+ const closed = registry.tick(now());
10320
+ for (const name of closed) logger.info("log channel window closed", {
10321
+ tags: { logChannel: name },
10322
+ meta: { channel: name }
10323
+ });
10324
+ }, tickMs);
10325
+ timer.unref?.();
10326
+ return {
10327
+ list: () => registry.list(),
10328
+ apply: (input) => {
10329
+ const unknown = registry.apply(input.windows, now());
10330
+ const armed = registry.armed();
10331
+ logger.info("log channels applied", { meta: {
10332
+ armed: armed.map((window) => window.channel),
10333
+ unknown,
10334
+ declared: registry.list().length
10335
+ } });
10336
+ return {
10337
+ armed: armed.length,
10338
+ unknown
10339
+ };
10340
+ },
10341
+ stop: () => {
10342
+ clearInterval(timer);
10343
+ }
10344
+ };
10345
+ }
10342
10346
  /**
10343
10347
  * Distinct (device, family, variant) counters one instance will hold.
10344
10348
  *
@@ -12217,12 +12221,30 @@ var BackupDestinationInfoSchema = object({
12217
12221
  lastSuccessAt: number().optional(),
12218
12222
  /** Newest-archive size from `manifests.json`, or undefined. */
12219
12223
  lastSuccessSizeBytes: number().optional(),
12220
- /** Per-destination cron expression. Empty = manual-only (no schedule). */
12224
+ /**
12225
+ * Cron cadence(s) of the ENABLED schedules that fan out to this
12226
+ * destination, comma-joined. Absent when no enabled schedule targets it
12227
+ * — a destination nothing is scheduled to write to must not advertise a
12228
+ * cadence (D384). This is never the `backup_destination_policies.cron`
12229
+ * column: that per-location cron has scheduled nothing since 2026-07-28
12230
+ * and reading it made a destination with a DISABLED schedule claim a
12231
+ * nightly run.
12232
+ */
12221
12233
  cron: string().optional(),
12222
- /** ms-epoch of next computed firing for this destination's cron, if any. */
12234
+ /** ms-epoch of the next firing across those schedules (earliest), if any. */
12223
12235
  nextRunAt: number().optional(),
12224
- /** ms-epoch of last successful scheduled run (mirrors policy.lastRunAt). */
12225
- lastRunAt: number().optional()
12236
+ /**
12237
+ * ms-epoch of the last time a run ATTEMPTED to write here — success or
12238
+ * failure. Never a success stamp: pair it with `lastSuccessAt` (the
12239
+ * newest archive that actually landed) and `lastError`.
12240
+ */
12241
+ lastAttemptAt: number().optional(),
12242
+ /**
12243
+ * Why the last attempt failed, verbatim. Absent when the last attempt
12244
+ * landed the archive. A destination that has never been written to has
12245
+ * neither this nor `lastAttemptAt`.
12246
+ */
12247
+ lastError: string().optional()
12226
12248
  });
12227
12249
  /**
12228
12250
  * Per-archive entry returned by `backup.listArchives({ destinationId })`.
@@ -12385,8 +12407,16 @@ var BackupScheduleSchema = object({
12385
12407
  retentionCount: number().int().min(1).max(1e3),
12386
12408
  /** Optional subset of source locations to include; omitted = all. */
12387
12409
  dataSources: array(string()).readonly().optional(),
12388
- /** ms-epoch of last successful run. */
12389
- lastRunAt: number().optional(),
12410
+ /**
12411
+ * ms-epoch of the last tick that FIRED this schedule. Stamped before the
12412
+ * archive runs (it is the dedupe anchor), so it says "attempted", never
12413
+ * "succeeded" — a run refused by every destination stamps it too.
12414
+ */
12415
+ lastAttemptAt: number().optional(),
12416
+ /** ms-epoch of the last run of this schedule that landed at ≥1 destination. */
12417
+ lastSuccessAt: number().optional(),
12418
+ /** Why the last fired run failed, verbatim. Absent when it succeeded. */
12419
+ lastError: string().optional(),
12390
12420
  /** ms-epoch of next computed firing (read-only, filled on list). */
12391
12421
  nextRunAt: number().optional()
12392
12422
  });
@@ -12435,14 +12465,7 @@ method(_void(), array(BackupDestinationInfoSchema).readonly(), { auth: "admin" }
12435
12465
  locationId: string(),
12436
12466
  enabled: boolean(),
12437
12467
  retentionCount: number().int().min(1).max(1e3),
12438
- label: string().optional(),
12439
- /**
12440
- * Per-destination cron expression. Empty string clears the
12441
- * schedule (manual-only). Validated server-side via croner;
12442
- * malformed expressions reject the upsert with an actionable
12443
- * message.
12444
- */
12445
- cron: string().optional()
12468
+ label: string().optional()
12446
12469
  }), _void(), {
12447
12470
  kind: "mutation",
12448
12471
  auth: "admin"
@@ -24312,7 +24335,7 @@ method(object({
24312
24335
  downloadId: string(),
24313
24336
  offset: number(),
24314
24337
  length: number()
24315
- }), _instanceof(Uint8Array)), method(object({ downloadId: string() }), _void(), { kind: "mutation" }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(object({ type: StorageLocationTypeSchema }), StorageLocationSchema.nullable()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
24338
+ }), _instanceof(Uint8Array)), method(object({ downloadId: string() }), _void(), { kind: "mutation" }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
24316
24339
  createdAt: true,
24317
24340
  updatedAt: true
24318
24341
  }), StorageLocationSchema, {
@@ -37744,12 +37767,6 @@ Object.freeze({
37744
37767
  addonId: null,
37745
37768
  access: "view"
37746
37769
  },
37747
- "storage.getDefaultLocation": {
37748
- capName: "storage",
37749
- capScope: "system",
37750
- addonId: null,
37751
- access: "view"
37752
- },
37753
37770
  "storage.list": {
37754
37771
  capName: "storage",
37755
37772
  capScope: "system",