@camstack/addon-provider-rademacher 0.2.45 → 0.2.47

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 (3) hide show
  1. package/dist/addon.js +331 -174
  2. package/dist/addon.mjs +331 -174
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -14169,6 +14169,114 @@ method(object({
14169
14169
  height: number()
14170
14170
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
14171
14171
  /**
14172
+ * `failure-contribution` — the capability an addon reports its OWN losses
14173
+ * through, per camera, with the denominator attached. It stores nothing.
14174
+ *
14175
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
14176
+ *
14177
+ * `load-contribution` answers *what did this camera COST*. This answers *what
14178
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
14179
+ * copied: the contributor reports what it already knows, hub-main adds only
14180
+ * `addonId`, nothing needs global knowledge, and there is no central list for
14181
+ * somebody to forget to edit.
14182
+ *
14183
+ * They are not merged, because their invariants are opposites:
14184
+ *
14185
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
14186
+ * claim a camera cost nothing, which is a measurement nobody made;
14187
+ * - a `failure-contribution` zero is the **most valuable value on the
14188
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14189
+ * and it is exactly what an absent entry cannot say.
14190
+ *
14191
+ * Putting a loss counter on a cost entry would also break the reconciliation
14192
+ * that gives `load-contribution` its point: contributions are subtracted from
14193
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14194
+ * has no process.
14195
+ *
14196
+ * ## Why not a log line, since the counters already exist
14197
+ *
14198
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
14199
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14200
+ * ends in a log line, and a log line is the thing the operator asked to stop
14201
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14202
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14203
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14204
+ * media blackout were both diagnosed. The counters stay; this is where they can
14205
+ * be READ.
14206
+ *
14207
+ * ## The rate is served with its denominator or not at all
14208
+ *
14209
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
14210
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14211
+ * than yesterday" and was **flat across twelve hours** once divided by the
14212
+ * successes on the same path. A surface that publishes only the numerator
14213
+ * reproduces that mistake on every read.
14214
+ *
14215
+ * ## Shape
14216
+ *
14217
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14218
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14219
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14220
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14221
+ * a forked runner's entries reach hub-main over transport that already exists.
14222
+ * No new UDS message, no second registry (D3). The operator reads the assembled
14223
+ * result through `system.getFailureContributions`.
14224
+ */
14225
+ var FailureReasonCountSchema = object({
14226
+ /**
14227
+ * Why the attempt did not land, in the contributor's own vocabulary —
14228
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14229
+ * strings that already appear in this repo's logs and, where one exists, the
14230
+ * same string the per-track `previewMissReason` records (D276): a second
14231
+ * vocabulary for the same loss would make the row and the counter
14232
+ * un-joinable.
14233
+ */
14234
+ reason: string(),
14235
+ count: number().int().nonnegative()
14236
+ });
14237
+ var FailureContributionSchema = object({
14238
+ /**
14239
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14240
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14241
+ * `unit` free: the families are owned by different addons and a shared enum
14242
+ * is a central list that rots invisibly.
14243
+ */
14244
+ family: string(),
14245
+ /**
14246
+ * The NUMERIC device id — the same value every log line carries as
14247
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
14248
+ * cannot name the camera must not emit the entry, because a fleet total
14249
+ * cannot answer the only question anybody asks of this surface.
14250
+ */
14251
+ deviceId: number().int().positive(),
14252
+ /**
14253
+ * A second dimension inside the family: the model / step id for an inference
14254
+ * timeout, so "which camera AND which model" is one read. Absent when the
14255
+ * family has a single variant.
14256
+ */
14257
+ variant: string().optional(),
14258
+ /**
14259
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14260
+ * differencing two reads must drop the interval when it changes, because the
14261
+ * counter restarted from zero in a respawned runner. Same discipline as
14262
+ * `LoadContribution.startedAtMs`.
14263
+ */
14264
+ sinceMs: number(),
14265
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14266
+ atMs: number(),
14267
+ /**
14268
+ * THE DENOMINATOR — every attempt on this path for this camera in the
14269
+ * window. A failure count published without it is the mistake this schema
14270
+ * exists to make impossible.
14271
+ */
14272
+ attempts: number().int().nonnegative(),
14273
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
14274
+ succeeded: number().int().nonnegative(),
14275
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
14276
+ reasons: array(FailureReasonCountSchema).readonly()
14277
+ });
14278
+ method(_void(), array(FailureContributionSchema).readonly());
14279
+ /**
14172
14280
  * filesystem-browse — per-node capability for browsing the node's local
14173
14281
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
14174
14282
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -14690,6 +14798,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
14690
14798
  kind: "mutation",
14691
14799
  auth: "admin"
14692
14800
  });
14801
+ var LoadContributionSchema = object({
14802
+ role: _enum([
14803
+ "decode",
14804
+ "transcode",
14805
+ "recording",
14806
+ "streaming",
14807
+ "detection"
14808
+ ]),
14809
+ /**
14810
+ * The NUMERIC device id — the same value every log line carries as
14811
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14812
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
14813
+ * contributor that cannot name its camera must not emit the entry at all,
14814
+ * because an unnamed per-camera entry is indistinguishable from a shared one
14815
+ * and would quietly turn one camera's cost into everybody's.
14816
+ */
14817
+ deviceId: number().int().positive().nullable(),
14818
+ attribution: _enum([
14819
+ "measured",
14820
+ "accounted",
14821
+ "unattributable"
14822
+ ]),
14823
+ /**
14824
+ * What ONE entry is, in the contributor's own words — `615/high`,
14825
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14826
+ * family and inventing a common one would lose the only information that
14827
+ * makes two entries for the same camera distinguishable.
14828
+ */
14829
+ unit: string(),
14830
+ /**
14831
+ * The OS process this cost lives in, when there is one. Present so a
14832
+ * consumer can (a) tell two generations of the same unit apart across a
14833
+ * restart, and (b) subtract claimed processes from the node's process
14834
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14835
+ * process of its own.
14836
+ */
14837
+ pid: number().int().positive().optional(),
14838
+ /**
14839
+ * When this generation started. The pid's incarnation marker: a consumer
14840
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14841
+ * window when this changes, because the counter restarted from zero in a new
14842
+ * process.
14843
+ */
14844
+ startedAtMs: number().optional(),
14845
+ /**
14846
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14847
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
14848
+ * contribution is asked for.
14849
+ *
14850
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
14851
+ * needs a sampler, and a new per-node sampler is the defect half of
14852
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14853
+ * by whoever already keeps a history; a rate cannot be un-averaged.
14854
+ *
14855
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14856
+ * an entry with no process.
14857
+ */
14858
+ cpuSeconds: number().optional(),
14859
+ /** Resident bytes of this unit's process, same source and same rules. */
14860
+ rssBytes: number().optional()
14861
+ });
14862
+ method(_void(), array(LoadContributionSchema).readonly());
14693
14863
  /**
14694
14864
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
14695
14865
  * through. It stores nothing.
@@ -14766,176 +14936,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
14766
14936
  tags: record(string(), string()).optional()
14767
14937
  }), array(LogEntrySchema).readonly());
14768
14938
  /**
14769
- * `failure-contribution` — the capability an addon reports its OWN losses
14770
- * through, per camera, with the denominator attached. It stores nothing.
14771
- *
14772
- * ## The twin of `load-contribution`, and why it is a twin and not a field
14773
- *
14774
- * `load-contribution` answers *what did this camera COST*. This answers *what
14775
- * did this camera LOSE*. The reporting discipline is identical and deliberately
14776
- * copied: the contributor reports what it already knows, hub-main adds only
14777
- * `addonId`, nothing needs global knowledge, and there is no central list for
14778
- * somebody to forget to edit.
14779
- *
14780
- * They are not merged, because their invariants are opposites:
14781
- *
14782
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
14783
- * claim a camera cost nothing, which is a measurement nobody made;
14784
- * - a `failure-contribution` zero is the **most valuable value on the
14785
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14786
- * and it is exactly what an absent entry cannot say.
14787
- *
14788
- * Putting a loss counter on a cost entry would also break the reconciliation
14789
- * that gives `load-contribution` its point: contributions are subtracted from
14790
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14791
- * has no process.
14792
- *
14793
- * ## Why not a log line, since the counters already exist
14794
- *
14795
- * Several of these paths already counted themselves — `CaptureScheduler`'s
14796
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14797
- * ends in a log line, and a log line is the thing the operator asked to stop
14798
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14799
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14800
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14801
- * media blackout were both diagnosed. The counters stay; this is where they can
14802
- * be READ.
14803
- *
14804
- * ## The rate is served with its denominator or not at all
14805
- *
14806
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
14807
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14808
- * than yesterday" and was **flat across twelve hours** once divided by the
14809
- * successes on the same path. A surface that publishes only the numerator
14810
- * reproduces that mistake on every read.
14811
- *
14812
- * ## Shape
14813
- *
14814
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14815
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14816
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14817
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14818
- * a forked runner's entries reach hub-main over transport that already exists.
14819
- * No new UDS message, no second registry (D3). The operator reads the assembled
14820
- * result through `system.getFailureContributions`.
14821
- */
14822
- var FailureReasonCountSchema = object({
14823
- /**
14824
- * Why the attempt did not land, in the contributor's own vocabulary —
14825
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14826
- * strings that already appear in this repo's logs and, where one exists, the
14827
- * same string the per-track `previewMissReason` records (D276): a second
14828
- * vocabulary for the same loss would make the row and the counter
14829
- * un-joinable.
14830
- */
14831
- reason: string(),
14832
- count: number().int().nonnegative()
14833
- });
14834
- var FailureContributionSchema = object({
14835
- /**
14836
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14837
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14838
- * `unit` free: the families are owned by different addons and a shared enum
14839
- * is a central list that rots invisibly.
14840
- */
14841
- family: string(),
14842
- /**
14843
- * The NUMERIC device id — the same value every log line carries as
14844
- * `tags.deviceId`. Never nullable and never absent: a contributor that
14845
- * cannot name the camera must not emit the entry, because a fleet total
14846
- * cannot answer the only question anybody asks of this surface.
14847
- */
14848
- deviceId: number().int().positive(),
14849
- /**
14850
- * A second dimension inside the family: the model / step id for an inference
14851
- * timeout, so "which camera AND which model" is one read. Absent when the
14852
- * family has a single variant.
14853
- */
14854
- variant: string().optional(),
14855
- /**
14856
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14857
- * differencing two reads must drop the interval when it changes, because the
14858
- * counter restarted from zero in a respawned runner. Same discipline as
14859
- * `LoadContribution.startedAtMs`.
14860
- */
14861
- sinceMs: number(),
14862
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14863
- atMs: number(),
14864
- /**
14865
- * THE DENOMINATOR — every attempt on this path for this camera in the
14866
- * window. A failure count published without it is the mistake this schema
14867
- * exists to make impossible.
14868
- */
14869
- attempts: number().int().nonnegative(),
14870
- /** Attempts that landed. `attempts - succeeded` is the loss. */
14871
- succeeded: number().int().nonnegative(),
14872
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
14873
- reasons: array(FailureReasonCountSchema).readonly()
14874
- });
14875
- method(_void(), array(FailureContributionSchema).readonly());
14876
- var LoadContributionSchema = object({
14877
- role: _enum([
14878
- "decode",
14879
- "transcode",
14880
- "recording",
14881
- "streaming",
14882
- "detection"
14883
- ]),
14884
- /**
14885
- * The NUMERIC device id — the same value every log line carries as
14886
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14887
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
14888
- * contributor that cannot name its camera must not emit the entry at all,
14889
- * because an unnamed per-camera entry is indistinguishable from a shared one
14890
- * and would quietly turn one camera's cost into everybody's.
14891
- */
14892
- deviceId: number().int().positive().nullable(),
14893
- attribution: _enum([
14894
- "measured",
14895
- "accounted",
14896
- "unattributable"
14897
- ]),
14898
- /**
14899
- * What ONE entry is, in the contributor's own words — `615/high`,
14900
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14901
- * family and inventing a common one would lose the only information that
14902
- * makes two entries for the same camera distinguishable.
14903
- */
14904
- unit: string(),
14905
- /**
14906
- * The OS process this cost lives in, when there is one. Present so a
14907
- * consumer can (a) tell two generations of the same unit apart across a
14908
- * restart, and (b) subtract claimed processes from the node's process
14909
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14910
- * process of its own.
14911
- */
14912
- pid: number().int().positive().optional(),
14913
- /**
14914
- * When this generation started. The pid's incarnation marker: a consumer
14915
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14916
- * window when this changes, because the counter restarted from zero in a new
14917
- * process.
14918
- */
14919
- startedAtMs: number().optional(),
14920
- /**
14921
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14922
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
14923
- * contribution is asked for.
14924
- *
14925
- * Cumulative and not a rate on purpose: a rate needs a window, a window
14926
- * needs a sampler, and a new per-node sampler is the defect half of
14927
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14928
- * by whoever already keeps a history; a rate cannot be un-averaged.
14929
- *
14930
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14931
- * an entry with no process.
14932
- */
14933
- cpuSeconds: number().optional(),
14934
- /** Resident bytes of this unit's process, same source and same rules. */
14935
- rssBytes: number().optional()
14936
- });
14937
- method(_void(), array(LoadContributionSchema).readonly());
14938
- /**
14939
14939
  * `login-method` — collection cap through which auth addons contribute
14940
14940
  * their pre-auth login surfaces to the login page. This is the SINGLE,
14941
14941
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -19673,12 +19673,53 @@ var MediaFileKindEnum = _enum([
19673
19673
  "keyFrameSmall",
19674
19674
  "thumbnailSmall"
19675
19675
  ]);
19676
+ /**
19677
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
19678
+ * ARE — never the bytes themselves.
19679
+ *
19680
+ * ## Why `url` and not `base64`
19681
+ *
19682
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
19683
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
19684
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
19685
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
19686
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
19687
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
19688
+ *
19689
+ * `url` points at the `event-media` data plane
19690
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
19691
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
19692
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
19693
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
19694
+ * no less protected than they were inside a `view`-level cap response — see
19695
+ * `data-plane-access.ts` for the rule and the one gap it does not close
19696
+ * (per-device scoping).
19697
+ *
19698
+ * The URL is built from the row's **stored** key, which is not always its
19699
+ * published `kind`: a track's face/plate crop is stored as `crop` under
19700
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
19701
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
19702
+ *
19703
+ * ## `base64` is TRANSITIONAL and is going away
19704
+ *
19705
+ * It is still populated for one reason: the deployed viewer's track-detail
19706
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
19707
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
19708
+ * triangle — not as absence. Removing the field before that viewer ships is an
19709
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
19710
+ * delete this line and the `withBytes` pass-through in
19711
+ * `analytics-query-facade.ts`; nothing else reads it.
19712
+ */
19676
19713
  var MediaFileSchema = object({
19677
19714
  key: string(),
19678
19715
  kind: MediaFileKindEnum,
19679
- base64: string(),
19680
19716
  sizeBytes: number(),
19681
19717
  timestamp: number()
19718
+ }).extend({
19719
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
19720
+ url: string(),
19721
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
19722
+ base64: string()
19682
19723
  });
19683
19724
  /**
19684
19725
  * One media row WITHOUT its bytes.
@@ -19690,7 +19731,9 @@ var MediaFileSchema = object({
19690
19731
  * blocks the whole view.
19691
19732
  *
19692
19733
  * `sizeBytes` is carried because it is what lets a client decide between the
19693
- * stored blob and a `?variant=thumb` rendering without fetching either.
19734
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
19735
+ * `url` because a client that had to build the plane path itself is a second
19736
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
19694
19737
  */
19695
19738
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
19696
19739
  /**
@@ -20037,6 +20080,50 @@ var EventStoreFootprintSchema = object({
20037
20080
  totalBytes: number().int(),
20038
20081
  devices: array(EventStoreDeviceFootprintSchema).readonly()
20039
20082
  });
20083
+ /** Event-media footprint for one {@link MediaFileKind}. */
20084
+ var EventMediaKindFootprintSchema = object({
20085
+ kind: MediaFileKindEnum,
20086
+ /** Media rows of this kind. */
20087
+ rows: number().int(),
20088
+ /** Bytes on disk held by those rows. */
20089
+ bytes: number().int()
20090
+ });
20091
+ /**
20092
+ * The media footprint broken down by KIND — the axis a deletion decision
20093
+ * actually turns on.
20094
+ *
20095
+ * A byte total says how much there is; it cannot say what is safe to remove.
20096
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
20097
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
20098
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
20099
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
20100
+ * nothing else, so sizing a deletion means summing per kind.
20101
+ *
20102
+ * ## Why `unaccounted*` exists
20103
+ *
20104
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
20105
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
20106
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
20107
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
20108
+ * retired code path, or by a version that knew a kind this one does not) would
20109
+ * otherwise vanish from the total silently, and an operator would delete
20110
+ * against a denominator smaller than the disk.
20111
+ *
20112
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
20113
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
20114
+ */
20115
+ var EventMediaKindBreakdownSchema = object({
20116
+ /** Every media row in scope, from one unfiltered aggregate. */
20117
+ totalRows: number().int(),
20118
+ /** Every media byte in scope, from that same aggregate. */
20119
+ totalBytes: number().int(),
20120
+ /** Per-kind footprint, ordered by bytes descending. */
20121
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
20122
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
20123
+ unaccountedRows: number().int(),
20124
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
20125
+ unaccountedBytes: number().int()
20126
+ });
20040
20127
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
20041
20128
  var EventPruneCountsSchema = object({
20042
20129
  motion: number().int(),
@@ -20240,6 +20327,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20240
20327
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
20241
20328
  kind: "query",
20242
20329
  auth: "admin"
20330
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
20331
+ kind: "query",
20332
+ auth: "admin"
20243
20333
  }), method(object({
20244
20334
  olderThanMs: number(),
20245
20335
  reason: OpsLogReasonSchema.optional()
@@ -20379,6 +20469,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20379
20469
  }), array(MediaFileSchema).readonly()), method(object({
20380
20470
  trackId: string(),
20381
20471
  deviceId: number()
20472
+ }), array(MediaFileInfoSchema).readonly()), method(object({
20473
+ eventId: string(),
20474
+ deviceId: number()
20382
20475
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
20383
20476
  kind: "mutation",
20384
20477
  auth: "admin"
@@ -22188,6 +22281,20 @@ method(object({
22188
22281
  error: string().optional()
22189
22282
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
22190
22283
  providerId: string(),
22284
+ /**
22285
+ * The location this config is an UNSAVED edit of, when there is one.
22286
+ *
22287
+ * `listLocations` replaces every declared secret with the redaction
22288
+ * sentinel, so the edit modal's form state holds the sentinel for any
22289
+ * credential the operator did not retype — and posting that here
22290
+ * without a way to resolve it makes the provider try to authenticate
22291
+ * as `__camstack_redacted__` and report the operator's own working
22292
+ * password as wrong. Given this id, the orchestrator restores each
22293
+ * sentinel from the stored config (same rule as `upsertLocation`)
22294
+ * before dispatching. Omitted by the "Add location" wizard, where
22295
+ * every value was typed just now and nothing is stored yet.
22296
+ */
22297
+ locationId: string().optional(),
22191
22298
  config: record(string(), unknown())
22192
22299
  }), object({
22193
22300
  ok: boolean(),
@@ -25369,10 +25476,24 @@ var FaceClusterSchema = object({
25369
25476
  size: number().int(),
25370
25477
  cohesion: number()
25371
25478
  });
25479
+ /**
25480
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25481
+ * are — never the bytes.
25482
+ *
25483
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25484
+ * track/event contract) is still populated because a deployed viewer requires
25485
+ * the field to parse a row at all; this method has no such reader. Its ONE
25486
+ * caller is the admin UI's detail modal, which was building
25487
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25488
+ * dialog already rendering its key FRAME from the `event-media` plane.
25489
+ *
25490
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25491
+ * media key directly, so this needed no new plane and no new access decision.
25492
+ */
25372
25493
  var MediaFileLiteSchema$1 = object({
25373
25494
  key: string(),
25374
25495
  kind: string(),
25375
- base64: string(),
25496
+ url: string(),
25376
25497
  sizeBytes: number(),
25377
25498
  timestamp: number()
25378
25499
  });
@@ -28394,10 +28515,24 @@ var PlateInfoSchema = object({
28394
28515
  */
28395
28516
  cropUrl: string().optional()
28396
28517
  });
28518
+ /**
28519
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
28520
+ * are — never the bytes.
28521
+ *
28522
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
28523
+ * track/event contract) is still populated because a deployed viewer requires
28524
+ * the field to parse a row at all; this method has no such reader. Its ONE
28525
+ * caller is the admin UI's detail modal, which was building
28526
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
28527
+ * dialog already rendering its key FRAME from the `event-media` plane.
28528
+ *
28529
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
28530
+ * media key directly, so this needed no new plane and no new access decision.
28531
+ */
28397
28532
  var MediaFileLiteSchema = object({
28398
28533
  key: string(),
28399
28534
  kind: string(),
28400
- base64: string(),
28535
+ url: string(),
28401
28536
  sizeBytes: number(),
28402
28537
  timestamp: number()
28403
28538
  });
@@ -36198,6 +36333,12 @@ Object.freeze({
36198
36333
  addonId: null,
36199
36334
  access: "view"
36200
36335
  },
36336
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
36337
+ capName: "pipeline-analytics",
36338
+ capScope: "device",
36339
+ addonId: null,
36340
+ access: "view"
36341
+ },
36201
36342
  "pipelineAnalytics.getEventStoreFootprint": {
36202
36343
  capName: "pipeline-analytics",
36203
36344
  capScope: "device",
@@ -36294,6 +36435,12 @@ Object.freeze({
36294
36435
  addonId: null,
36295
36436
  access: "view"
36296
36437
  },
36438
+ "pipelineAnalytics.listEventMedia": {
36439
+ capName: "pipeline-analytics",
36440
+ capScope: "device",
36441
+ addonId: null,
36442
+ access: "view"
36443
+ },
36297
36444
  "pipelineAnalytics.listGroups": {
36298
36445
  capName: "pipeline-analytics",
36299
36446
  capScope: "device",
@@ -39857,6 +40004,11 @@ Object.freeze({
39857
40004
  form: "single",
39858
40005
  optional: false
39859
40006
  }],
40007
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
40008
+ name: "deviceId",
40009
+ form: "single",
40010
+ optional: true
40011
+ }],
39860
40012
  "pipelineAnalytics.getGroup": [{
39861
40013
  name: "deviceId",
39862
40014
  form: "single",
@@ -39917,6 +40069,11 @@ Object.freeze({
39917
40069
  form: "array",
39918
40070
  optional: false
39919
40071
  }],
40072
+ "pipelineAnalytics.listEventMedia": [{
40073
+ name: "deviceId",
40074
+ form: "single",
40075
+ optional: false
40076
+ }],
39920
40077
  "pipelineAnalytics.listGroups": [{
39921
40078
  name: "deviceIds",
39922
40079
  form: "array",
package/dist/addon.mjs CHANGED
@@ -14168,6 +14168,114 @@ method(object({
14168
14168
  height: number()
14169
14169
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
14170
14170
  /**
14171
+ * `failure-contribution` — the capability an addon reports its OWN losses
14172
+ * through, per camera, with the denominator attached. It stores nothing.
14173
+ *
14174
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
14175
+ *
14176
+ * `load-contribution` answers *what did this camera COST*. This answers *what
14177
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
14178
+ * copied: the contributor reports what it already knows, hub-main adds only
14179
+ * `addonId`, nothing needs global knowledge, and there is no central list for
14180
+ * somebody to forget to edit.
14181
+ *
14182
+ * They are not merged, because their invariants are opposites:
14183
+ *
14184
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
14185
+ * claim a camera cost nothing, which is a measurement nobody made;
14186
+ * - a `failure-contribution` zero is the **most valuable value on the
14187
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14188
+ * and it is exactly what an absent entry cannot say.
14189
+ *
14190
+ * Putting a loss counter on a cost entry would also break the reconciliation
14191
+ * that gives `load-contribution` its point: contributions are subtracted from
14192
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14193
+ * has no process.
14194
+ *
14195
+ * ## Why not a log line, since the counters already exist
14196
+ *
14197
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
14198
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14199
+ * ends in a log line, and a log line is the thing the operator asked to stop
14200
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14201
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14202
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14203
+ * media blackout were both diagnosed. The counters stay; this is where they can
14204
+ * be READ.
14205
+ *
14206
+ * ## The rate is served with its denominator or not at all
14207
+ *
14208
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
14209
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14210
+ * than yesterday" and was **flat across twelve hours** once divided by the
14211
+ * successes on the same path. A surface that publishes only the numerator
14212
+ * reproduces that mistake on every read.
14213
+ *
14214
+ * ## Shape
14215
+ *
14216
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14217
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14218
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14219
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14220
+ * a forked runner's entries reach hub-main over transport that already exists.
14221
+ * No new UDS message, no second registry (D3). The operator reads the assembled
14222
+ * result through `system.getFailureContributions`.
14223
+ */
14224
+ var FailureReasonCountSchema = object({
14225
+ /**
14226
+ * Why the attempt did not land, in the contributor's own vocabulary —
14227
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14228
+ * strings that already appear in this repo's logs and, where one exists, the
14229
+ * same string the per-track `previewMissReason` records (D276): a second
14230
+ * vocabulary for the same loss would make the row and the counter
14231
+ * un-joinable.
14232
+ */
14233
+ reason: string(),
14234
+ count: number().int().nonnegative()
14235
+ });
14236
+ var FailureContributionSchema = object({
14237
+ /**
14238
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14239
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14240
+ * `unit` free: the families are owned by different addons and a shared enum
14241
+ * is a central list that rots invisibly.
14242
+ */
14243
+ family: string(),
14244
+ /**
14245
+ * The NUMERIC device id — the same value every log line carries as
14246
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
14247
+ * cannot name the camera must not emit the entry, because a fleet total
14248
+ * cannot answer the only question anybody asks of this surface.
14249
+ */
14250
+ deviceId: number().int().positive(),
14251
+ /**
14252
+ * A second dimension inside the family: the model / step id for an inference
14253
+ * timeout, so "which camera AND which model" is one read. Absent when the
14254
+ * family has a single variant.
14255
+ */
14256
+ variant: string().optional(),
14257
+ /**
14258
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14259
+ * differencing two reads must drop the interval when it changes, because the
14260
+ * counter restarted from zero in a respawned runner. Same discipline as
14261
+ * `LoadContribution.startedAtMs`.
14262
+ */
14263
+ sinceMs: number(),
14264
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14265
+ atMs: number(),
14266
+ /**
14267
+ * THE DENOMINATOR — every attempt on this path for this camera in the
14268
+ * window. A failure count published without it is the mistake this schema
14269
+ * exists to make impossible.
14270
+ */
14271
+ attempts: number().int().nonnegative(),
14272
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
14273
+ succeeded: number().int().nonnegative(),
14274
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
14275
+ reasons: array(FailureReasonCountSchema).readonly()
14276
+ });
14277
+ method(_void(), array(FailureContributionSchema).readonly());
14278
+ /**
14171
14279
  * filesystem-browse — per-node capability for browsing the node's local
14172
14280
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
14173
14281
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -14689,6 +14797,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
14689
14797
  kind: "mutation",
14690
14798
  auth: "admin"
14691
14799
  });
14800
+ var LoadContributionSchema = object({
14801
+ role: _enum([
14802
+ "decode",
14803
+ "transcode",
14804
+ "recording",
14805
+ "streaming",
14806
+ "detection"
14807
+ ]),
14808
+ /**
14809
+ * The NUMERIC device id — the same value every log line carries as
14810
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14811
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
14812
+ * contributor that cannot name its camera must not emit the entry at all,
14813
+ * because an unnamed per-camera entry is indistinguishable from a shared one
14814
+ * and would quietly turn one camera's cost into everybody's.
14815
+ */
14816
+ deviceId: number().int().positive().nullable(),
14817
+ attribution: _enum([
14818
+ "measured",
14819
+ "accounted",
14820
+ "unattributable"
14821
+ ]),
14822
+ /**
14823
+ * What ONE entry is, in the contributor's own words — `615/high`,
14824
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14825
+ * family and inventing a common one would lose the only information that
14826
+ * makes two entries for the same camera distinguishable.
14827
+ */
14828
+ unit: string(),
14829
+ /**
14830
+ * The OS process this cost lives in, when there is one. Present so a
14831
+ * consumer can (a) tell two generations of the same unit apart across a
14832
+ * restart, and (b) subtract claimed processes from the node's process
14833
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14834
+ * process of its own.
14835
+ */
14836
+ pid: number().int().positive().optional(),
14837
+ /**
14838
+ * When this generation started. The pid's incarnation marker: a consumer
14839
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14840
+ * window when this changes, because the counter restarted from zero in a new
14841
+ * process.
14842
+ */
14843
+ startedAtMs: number().optional(),
14844
+ /**
14845
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14846
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
14847
+ * contribution is asked for.
14848
+ *
14849
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
14850
+ * needs a sampler, and a new per-node sampler is the defect half of
14851
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14852
+ * by whoever already keeps a history; a rate cannot be un-averaged.
14853
+ *
14854
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14855
+ * an entry with no process.
14856
+ */
14857
+ cpuSeconds: number().optional(),
14858
+ /** Resident bytes of this unit's process, same source and same rules. */
14859
+ rssBytes: number().optional()
14860
+ });
14861
+ method(_void(), array(LoadContributionSchema).readonly());
14692
14862
  /**
14693
14863
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
14694
14864
  * through. It stores nothing.
@@ -14765,176 +14935,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
14765
14935
  tags: record(string(), string()).optional()
14766
14936
  }), array(LogEntrySchema).readonly());
14767
14937
  /**
14768
- * `failure-contribution` — the capability an addon reports its OWN losses
14769
- * through, per camera, with the denominator attached. It stores nothing.
14770
- *
14771
- * ## The twin of `load-contribution`, and why it is a twin and not a field
14772
- *
14773
- * `load-contribution` answers *what did this camera COST*. This answers *what
14774
- * did this camera LOSE*. The reporting discipline is identical and deliberately
14775
- * copied: the contributor reports what it already knows, hub-main adds only
14776
- * `addonId`, nothing needs global knowledge, and there is no central list for
14777
- * somebody to forget to edit.
14778
- *
14779
- * They are not merged, because their invariants are opposites:
14780
- *
14781
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
14782
- * claim a camera cost nothing, which is a measurement nobody made;
14783
- * - a `failure-contribution` zero is the **most valuable value on the
14784
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14785
- * and it is exactly what an absent entry cannot say.
14786
- *
14787
- * Putting a loss counter on a cost entry would also break the reconciliation
14788
- * that gives `load-contribution` its point: contributions are subtracted from
14789
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14790
- * has no process.
14791
- *
14792
- * ## Why not a log line, since the counters already exist
14793
- *
14794
- * Several of these paths already counted themselves — `CaptureScheduler`'s
14795
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14796
- * ends in a log line, and a log line is the thing the operator asked to stop
14797
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14798
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14799
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14800
- * media blackout were both diagnosed. The counters stay; this is where they can
14801
- * be READ.
14802
- *
14803
- * ## The rate is served with its denominator or not at all
14804
- *
14805
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
14806
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14807
- * than yesterday" and was **flat across twelve hours** once divided by the
14808
- * successes on the same path. A surface that publishes only the numerator
14809
- * reproduces that mistake on every read.
14810
- *
14811
- * ## Shape
14812
- *
14813
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14814
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14815
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14816
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14817
- * a forked runner's entries reach hub-main over transport that already exists.
14818
- * No new UDS message, no second registry (D3). The operator reads the assembled
14819
- * result through `system.getFailureContributions`.
14820
- */
14821
- var FailureReasonCountSchema = object({
14822
- /**
14823
- * Why the attempt did not land, in the contributor's own vocabulary —
14824
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14825
- * strings that already appear in this repo's logs and, where one exists, the
14826
- * same string the per-track `previewMissReason` records (D276): a second
14827
- * vocabulary for the same loss would make the row and the counter
14828
- * un-joinable.
14829
- */
14830
- reason: string(),
14831
- count: number().int().nonnegative()
14832
- });
14833
- var FailureContributionSchema = object({
14834
- /**
14835
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14836
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14837
- * `unit` free: the families are owned by different addons and a shared enum
14838
- * is a central list that rots invisibly.
14839
- */
14840
- family: string(),
14841
- /**
14842
- * The NUMERIC device id — the same value every log line carries as
14843
- * `tags.deviceId`. Never nullable and never absent: a contributor that
14844
- * cannot name the camera must not emit the entry, because a fleet total
14845
- * cannot answer the only question anybody asks of this surface.
14846
- */
14847
- deviceId: number().int().positive(),
14848
- /**
14849
- * A second dimension inside the family: the model / step id for an inference
14850
- * timeout, so "which camera AND which model" is one read. Absent when the
14851
- * family has a single variant.
14852
- */
14853
- variant: string().optional(),
14854
- /**
14855
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14856
- * differencing two reads must drop the interval when it changes, because the
14857
- * counter restarted from zero in a respawned runner. Same discipline as
14858
- * `LoadContribution.startedAtMs`.
14859
- */
14860
- sinceMs: number(),
14861
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14862
- atMs: number(),
14863
- /**
14864
- * THE DENOMINATOR — every attempt on this path for this camera in the
14865
- * window. A failure count published without it is the mistake this schema
14866
- * exists to make impossible.
14867
- */
14868
- attempts: number().int().nonnegative(),
14869
- /** Attempts that landed. `attempts - succeeded` is the loss. */
14870
- succeeded: number().int().nonnegative(),
14871
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
14872
- reasons: array(FailureReasonCountSchema).readonly()
14873
- });
14874
- method(_void(), array(FailureContributionSchema).readonly());
14875
- var LoadContributionSchema = object({
14876
- role: _enum([
14877
- "decode",
14878
- "transcode",
14879
- "recording",
14880
- "streaming",
14881
- "detection"
14882
- ]),
14883
- /**
14884
- * The NUMERIC device id — the same value every log line carries as
14885
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14886
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
14887
- * contributor that cannot name its camera must not emit the entry at all,
14888
- * because an unnamed per-camera entry is indistinguishable from a shared one
14889
- * and would quietly turn one camera's cost into everybody's.
14890
- */
14891
- deviceId: number().int().positive().nullable(),
14892
- attribution: _enum([
14893
- "measured",
14894
- "accounted",
14895
- "unattributable"
14896
- ]),
14897
- /**
14898
- * What ONE entry is, in the contributor's own words — `615/high`,
14899
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14900
- * family and inventing a common one would lose the only information that
14901
- * makes two entries for the same camera distinguishable.
14902
- */
14903
- unit: string(),
14904
- /**
14905
- * The OS process this cost lives in, when there is one. Present so a
14906
- * consumer can (a) tell two generations of the same unit apart across a
14907
- * restart, and (b) subtract claimed processes from the node's process
14908
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14909
- * process of its own.
14910
- */
14911
- pid: number().int().positive().optional(),
14912
- /**
14913
- * When this generation started. The pid's incarnation marker: a consumer
14914
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14915
- * window when this changes, because the counter restarted from zero in a new
14916
- * process.
14917
- */
14918
- startedAtMs: number().optional(),
14919
- /**
14920
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14921
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
14922
- * contribution is asked for.
14923
- *
14924
- * Cumulative and not a rate on purpose: a rate needs a window, a window
14925
- * needs a sampler, and a new per-node sampler is the defect half of
14926
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14927
- * by whoever already keeps a history; a rate cannot be un-averaged.
14928
- *
14929
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14930
- * an entry with no process.
14931
- */
14932
- cpuSeconds: number().optional(),
14933
- /** Resident bytes of this unit's process, same source and same rules. */
14934
- rssBytes: number().optional()
14935
- });
14936
- method(_void(), array(LoadContributionSchema).readonly());
14937
- /**
14938
14938
  * `login-method` — collection cap through which auth addons contribute
14939
14939
  * their pre-auth login surfaces to the login page. This is the SINGLE,
14940
14940
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -19672,12 +19672,53 @@ var MediaFileKindEnum = _enum([
19672
19672
  "keyFrameSmall",
19673
19673
  "thumbnailSmall"
19674
19674
  ]);
19675
+ /**
19676
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
19677
+ * ARE — never the bytes themselves.
19678
+ *
19679
+ * ## Why `url` and not `base64`
19680
+ *
19681
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
19682
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
19683
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
19684
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
19685
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
19686
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
19687
+ *
19688
+ * `url` points at the `event-media` data plane
19689
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
19690
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
19691
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
19692
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
19693
+ * no less protected than they were inside a `view`-level cap response — see
19694
+ * `data-plane-access.ts` for the rule and the one gap it does not close
19695
+ * (per-device scoping).
19696
+ *
19697
+ * The URL is built from the row's **stored** key, which is not always its
19698
+ * published `kind`: a track's face/plate crop is stored as `crop` under
19699
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
19700
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
19701
+ *
19702
+ * ## `base64` is TRANSITIONAL and is going away
19703
+ *
19704
+ * It is still populated for one reason: the deployed viewer's track-detail
19705
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
19706
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
19707
+ * triangle — not as absence. Removing the field before that viewer ships is an
19708
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
19709
+ * delete this line and the `withBytes` pass-through in
19710
+ * `analytics-query-facade.ts`; nothing else reads it.
19711
+ */
19675
19712
  var MediaFileSchema = object({
19676
19713
  key: string(),
19677
19714
  kind: MediaFileKindEnum,
19678
- base64: string(),
19679
19715
  sizeBytes: number(),
19680
19716
  timestamp: number()
19717
+ }).extend({
19718
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
19719
+ url: string(),
19720
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
19721
+ base64: string()
19681
19722
  });
19682
19723
  /**
19683
19724
  * One media row WITHOUT its bytes.
@@ -19689,7 +19730,9 @@ var MediaFileSchema = object({
19689
19730
  * blocks the whole view.
19690
19731
  *
19691
19732
  * `sizeBytes` is carried because it is what lets a client decide between the
19692
- * stored blob and a `?variant=thumb` rendering without fetching either.
19733
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
19734
+ * `url` because a client that had to build the plane path itself is a second
19735
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
19693
19736
  */
19694
19737
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
19695
19738
  /**
@@ -20036,6 +20079,50 @@ var EventStoreFootprintSchema = object({
20036
20079
  totalBytes: number().int(),
20037
20080
  devices: array(EventStoreDeviceFootprintSchema).readonly()
20038
20081
  });
20082
+ /** Event-media footprint for one {@link MediaFileKind}. */
20083
+ var EventMediaKindFootprintSchema = object({
20084
+ kind: MediaFileKindEnum,
20085
+ /** Media rows of this kind. */
20086
+ rows: number().int(),
20087
+ /** Bytes on disk held by those rows. */
20088
+ bytes: number().int()
20089
+ });
20090
+ /**
20091
+ * The media footprint broken down by KIND — the axis a deletion decision
20092
+ * actually turns on.
20093
+ *
20094
+ * A byte total says how much there is; it cannot say what is safe to remove.
20095
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
20096
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
20097
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
20098
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
20099
+ * nothing else, so sizing a deletion means summing per kind.
20100
+ *
20101
+ * ## Why `unaccounted*` exists
20102
+ *
20103
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
20104
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
20105
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
20106
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
20107
+ * retired code path, or by a version that knew a kind this one does not) would
20108
+ * otherwise vanish from the total silently, and an operator would delete
20109
+ * against a denominator smaller than the disk.
20110
+ *
20111
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
20112
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
20113
+ */
20114
+ var EventMediaKindBreakdownSchema = object({
20115
+ /** Every media row in scope, from one unfiltered aggregate. */
20116
+ totalRows: number().int(),
20117
+ /** Every media byte in scope, from that same aggregate. */
20118
+ totalBytes: number().int(),
20119
+ /** Per-kind footprint, ordered by bytes descending. */
20120
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
20121
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
20122
+ unaccountedRows: number().int(),
20123
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
20124
+ unaccountedBytes: number().int()
20125
+ });
20039
20126
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
20040
20127
  var EventPruneCountsSchema = object({
20041
20128
  motion: number().int(),
@@ -20239,6 +20326,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20239
20326
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
20240
20327
  kind: "query",
20241
20328
  auth: "admin"
20329
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
20330
+ kind: "query",
20331
+ auth: "admin"
20242
20332
  }), method(object({
20243
20333
  olderThanMs: number(),
20244
20334
  reason: OpsLogReasonSchema.optional()
@@ -20378,6 +20468,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20378
20468
  }), array(MediaFileSchema).readonly()), method(object({
20379
20469
  trackId: string(),
20380
20470
  deviceId: number()
20471
+ }), array(MediaFileInfoSchema).readonly()), method(object({
20472
+ eventId: string(),
20473
+ deviceId: number()
20381
20474
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
20382
20475
  kind: "mutation",
20383
20476
  auth: "admin"
@@ -22187,6 +22280,20 @@ method(object({
22187
22280
  error: string().optional()
22188
22281
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
22189
22282
  providerId: string(),
22283
+ /**
22284
+ * The location this config is an UNSAVED edit of, when there is one.
22285
+ *
22286
+ * `listLocations` replaces every declared secret with the redaction
22287
+ * sentinel, so the edit modal's form state holds the sentinel for any
22288
+ * credential the operator did not retype — and posting that here
22289
+ * without a way to resolve it makes the provider try to authenticate
22290
+ * as `__camstack_redacted__` and report the operator's own working
22291
+ * password as wrong. Given this id, the orchestrator restores each
22292
+ * sentinel from the stored config (same rule as `upsertLocation`)
22293
+ * before dispatching. Omitted by the "Add location" wizard, where
22294
+ * every value was typed just now and nothing is stored yet.
22295
+ */
22296
+ locationId: string().optional(),
22190
22297
  config: record(string(), unknown())
22191
22298
  }), object({
22192
22299
  ok: boolean(),
@@ -25368,10 +25475,24 @@ var FaceClusterSchema = object({
25368
25475
  size: number().int(),
25369
25476
  cohesion: number()
25370
25477
  });
25478
+ /**
25479
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25480
+ * are — never the bytes.
25481
+ *
25482
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25483
+ * track/event contract) is still populated because a deployed viewer requires
25484
+ * the field to parse a row at all; this method has no such reader. Its ONE
25485
+ * caller is the admin UI's detail modal, which was building
25486
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25487
+ * dialog already rendering its key FRAME from the `event-media` plane.
25488
+ *
25489
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25490
+ * media key directly, so this needed no new plane and no new access decision.
25491
+ */
25371
25492
  var MediaFileLiteSchema$1 = object({
25372
25493
  key: string(),
25373
25494
  kind: string(),
25374
- base64: string(),
25495
+ url: string(),
25375
25496
  sizeBytes: number(),
25376
25497
  timestamp: number()
25377
25498
  });
@@ -28393,10 +28514,24 @@ var PlateInfoSchema = object({
28393
28514
  */
28394
28515
  cropUrl: string().optional()
28395
28516
  });
28517
+ /**
28518
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
28519
+ * are — never the bytes.
28520
+ *
28521
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
28522
+ * track/event contract) is still populated because a deployed viewer requires
28523
+ * the field to parse a row at all; this method has no such reader. Its ONE
28524
+ * caller is the admin UI's detail modal, which was building
28525
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
28526
+ * dialog already rendering its key FRAME from the `event-media` plane.
28527
+ *
28528
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
28529
+ * media key directly, so this needed no new plane and no new access decision.
28530
+ */
28396
28531
  var MediaFileLiteSchema = object({
28397
28532
  key: string(),
28398
28533
  kind: string(),
28399
- base64: string(),
28534
+ url: string(),
28400
28535
  sizeBytes: number(),
28401
28536
  timestamp: number()
28402
28537
  });
@@ -36197,6 +36332,12 @@ Object.freeze({
36197
36332
  addonId: null,
36198
36333
  access: "view"
36199
36334
  },
36335
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
36336
+ capName: "pipeline-analytics",
36337
+ capScope: "device",
36338
+ addonId: null,
36339
+ access: "view"
36340
+ },
36200
36341
  "pipelineAnalytics.getEventStoreFootprint": {
36201
36342
  capName: "pipeline-analytics",
36202
36343
  capScope: "device",
@@ -36293,6 +36434,12 @@ Object.freeze({
36293
36434
  addonId: null,
36294
36435
  access: "view"
36295
36436
  },
36437
+ "pipelineAnalytics.listEventMedia": {
36438
+ capName: "pipeline-analytics",
36439
+ capScope: "device",
36440
+ addonId: null,
36441
+ access: "view"
36442
+ },
36296
36443
  "pipelineAnalytics.listGroups": {
36297
36444
  capName: "pipeline-analytics",
36298
36445
  capScope: "device",
@@ -39856,6 +40003,11 @@ Object.freeze({
39856
40003
  form: "single",
39857
40004
  optional: false
39858
40005
  }],
40006
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
40007
+ name: "deviceId",
40008
+ form: "single",
40009
+ optional: true
40010
+ }],
39859
40011
  "pipelineAnalytics.getGroup": [{
39860
40012
  name: "deviceId",
39861
40013
  form: "single",
@@ -39916,6 +40068,11 @@ Object.freeze({
39916
40068
  form: "array",
39917
40069
  optional: false
39918
40070
  }],
40071
+ "pipelineAnalytics.listEventMedia": [{
40072
+ name: "deviceId",
40073
+ form: "single",
40074
+ optional: false
40075
+ }],
39919
40076
  "pipelineAnalytics.listGroups": [{
39920
40077
  name: "deviceIds",
39921
40078
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-rademacher",
3
- "version": "0.2.45",
3
+ "version": "0.2.47",
4
4
  "description": "Rademacher HomePilot device-provider addon for CamStack — wraps the @apocaliss92/noderademacher local-hub client (roller shutters over the cover cap)",
5
5
  "keywords": [
6
6
  "camstack",