@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.
- package/dist/audio-analyzer/index.js +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/{default-detection-model-B5v8qEI0.js → default-detection-model-BsmxtyZx.js} +1 -1
- package/dist/{default-detection-model-CASpAESz.mjs → default-detection-model-c1LY6thP.mjs} +1 -1
- package/dist/detection-pipeline/index.js +5 -5
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-BxP0SvPz.js → dist-D2YTTkRf.js} +436 -419
- package/dist/{dist-oFIHnD88.mjs → dist-mEsrAAc3.mjs} +436 -419
- package/dist/{lazy-sharp-VKOG3WW4.js → lazy-sharp-B5bW34uC.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-B4zETsJ9.mjs → node-CUxep5tc.mjs} +1 -1
- package/dist/{node-BIKrNEtS.js → node-x3YokJkD.js} +1 -1
- package/dist/pipeline-runner/index.js +8 -7
- package/dist/pipeline-runner/index.mjs +7 -6
- package/dist/{process-memory-D9xYwm6L.mjs → process-memory-BCqpTruN.mjs} +1 -1
- package/dist/{process-memory-CMHOmj75.js → process-memory-CFc4D9A2.js} +1 -1
- package/dist/recorder/index.js +169 -334
- package/dist/recorder/index.mjs +164 -329
- package/dist/restream-intent-B1Difcnq.js +313 -0
- package/dist/restream-intent-DWe2kInY.mjs +248 -0
- package/dist/{prebuffer-DmBYRQWg.mjs → retire-root-keys-Csb8z2f3.js} +12 -64
- package/dist/{prebuffer-DqdeLSsk.js → retire-root-keys-DZvlso2l.mjs} +1 -99
- package/dist/{segment-demux-js-DLwL1vmq.js → segment-demux-js-DUZVURQd.js} +1 -1
- package/dist/{segment-demux-js-DZysrgdC.mjs → segment-demux-js-xHtB450F.mjs} +1 -1
- package/dist/session-decode/decode-worker-child.js +25 -5
- package/dist/session-decode/decode-worker-child.mjs +24 -4
- package/dist/stream-broker/_stub.js +2 -2
- 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
- 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
- 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
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-C8tu7NS4.mjs → hostInit-D_cZQhBA.mjs} +3 -3
- package/dist/stream-broker/index.js +181 -48
- package/dist/stream-broker/index.mjs +172 -39
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-D2mMjUXv.js → worker-protocol-CRDaBlJl.js} +1 -1
- package/dist/{worker-protocol-DFlZm_wr.mjs → worker-protocol-oUamwZSn.mjs} +1 -1
- package/package.json +1 -1
- package/dist/restream-intent-B4BXZra7.mjs +0 -72
- package/dist/restream-intent-Cv9x3jmu.js +0 -89
- 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
|
|
10092
|
-
*
|
|
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
|
-
*
|
|
10117
|
-
*
|
|
10118
|
-
*
|
|
10119
|
-
*
|
|
10120
|
-
*
|
|
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
|
|
10123
|
-
*
|
|
10124
|
-
*
|
|
10125
|
-
*
|
|
10126
|
-
*
|
|
10127
|
-
*
|
|
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'`) →
|
|
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
|
-
|
|
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
|
-
*
|
|
10293
|
-
*
|
|
10294
|
-
*
|
|
10295
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
10165
|
+
* Declare a channel and get its gate.
|
|
10300
10166
|
*
|
|
10301
|
-
*
|
|
10302
|
-
*
|
|
10303
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
10312
|
-
*
|
|
10313
|
-
*
|
|
10314
|
-
*
|
|
10315
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
12211
|
+
/** ms-epoch of the next firing across those schedules (earliest), if any. */
|
|
12200
12212
|
nextRunAt: number().optional(),
|
|
12201
|
-
/**
|
|
12202
|
-
|
|
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
|
-
/**
|
|
12366
|
-
|
|
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(
|
|
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",
|