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