@camstack/addon-provider-rtsp 1.2.46 → 1.2.48

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
@@ -13240,6 +13240,114 @@ method(object({
13240
13240
  height: number()
13241
13241
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13242
13242
  /**
13243
+ * `failure-contribution` — the capability an addon reports its OWN losses
13244
+ * through, per camera, with the denominator attached. It stores nothing.
13245
+ *
13246
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13247
+ *
13248
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13249
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13250
+ * copied: the contributor reports what it already knows, hub-main adds only
13251
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13252
+ * somebody to forget to edit.
13253
+ *
13254
+ * They are not merged, because their invariants are opposites:
13255
+ *
13256
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13257
+ * claim a camera cost nothing, which is a measurement nobody made;
13258
+ * - a `failure-contribution` zero is the **most valuable value on the
13259
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13260
+ * and it is exactly what an absent entry cannot say.
13261
+ *
13262
+ * Putting a loss counter on a cost entry would also break the reconciliation
13263
+ * that gives `load-contribution` its point: contributions are subtracted from
13264
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13265
+ * has no process.
13266
+ *
13267
+ * ## Why not a log line, since the counters already exist
13268
+ *
13269
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13270
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13271
+ * ends in a log line, and a log line is the thing the operator asked to stop
13272
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13273
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13274
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13275
+ * media blackout were both diagnosed. The counters stay; this is where they can
13276
+ * be READ.
13277
+ *
13278
+ * ## The rate is served with its denominator or not at all
13279
+ *
13280
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13281
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13282
+ * than yesterday" and was **flat across twelve hours** once divided by the
13283
+ * successes on the same path. A surface that publishes only the numerator
13284
+ * reproduces that mistake on every read.
13285
+ *
13286
+ * ## Shape
13287
+ *
13288
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13289
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13290
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13291
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13292
+ * a forked runner's entries reach hub-main over transport that already exists.
13293
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13294
+ * result through `system.getFailureContributions`.
13295
+ */
13296
+ var FailureReasonCountSchema = object({
13297
+ /**
13298
+ * Why the attempt did not land, in the contributor's own vocabulary —
13299
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13300
+ * strings that already appear in this repo's logs and, where one exists, the
13301
+ * same string the per-track `previewMissReason` records (D276): a second
13302
+ * vocabulary for the same loss would make the row and the counter
13303
+ * un-joinable.
13304
+ */
13305
+ reason: string(),
13306
+ count: number().int().nonnegative()
13307
+ });
13308
+ var FailureContributionSchema = object({
13309
+ /**
13310
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13311
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13312
+ * `unit` free: the families are owned by different addons and a shared enum
13313
+ * is a central list that rots invisibly.
13314
+ */
13315
+ family: string(),
13316
+ /**
13317
+ * The NUMERIC device id — the same value every log line carries as
13318
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13319
+ * cannot name the camera must not emit the entry, because a fleet total
13320
+ * cannot answer the only question anybody asks of this surface.
13321
+ */
13322
+ deviceId: number().int().positive(),
13323
+ /**
13324
+ * A second dimension inside the family: the model / step id for an inference
13325
+ * timeout, so "which camera AND which model" is one read. Absent when the
13326
+ * family has a single variant.
13327
+ */
13328
+ variant: string().optional(),
13329
+ /**
13330
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13331
+ * differencing two reads must drop the interval when it changes, because the
13332
+ * counter restarted from zero in a respawned runner. Same discipline as
13333
+ * `LoadContribution.startedAtMs`.
13334
+ */
13335
+ sinceMs: number(),
13336
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13337
+ atMs: number(),
13338
+ /**
13339
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13340
+ * window. A failure count published without it is the mistake this schema
13341
+ * exists to make impossible.
13342
+ */
13343
+ attempts: number().int().nonnegative(),
13344
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13345
+ succeeded: number().int().nonnegative(),
13346
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13347
+ reasons: array(FailureReasonCountSchema).readonly()
13348
+ });
13349
+ method(_void(), array(FailureContributionSchema).readonly());
13350
+ /**
13243
13351
  * filesystem-browse — per-node capability for browsing the node's local
13244
13352
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13245
13353
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13761,6 +13869,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13761
13869
  kind: "mutation",
13762
13870
  auth: "admin"
13763
13871
  });
13872
+ var LoadContributionSchema = object({
13873
+ role: _enum([
13874
+ "decode",
13875
+ "transcode",
13876
+ "recording",
13877
+ "streaming",
13878
+ "detection"
13879
+ ]),
13880
+ /**
13881
+ * The NUMERIC device id — the same value every log line carries as
13882
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13883
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13884
+ * contributor that cannot name its camera must not emit the entry at all,
13885
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13886
+ * and would quietly turn one camera's cost into everybody's.
13887
+ */
13888
+ deviceId: number().int().positive().nullable(),
13889
+ attribution: _enum([
13890
+ "measured",
13891
+ "accounted",
13892
+ "unattributable"
13893
+ ]),
13894
+ /**
13895
+ * What ONE entry is, in the contributor's own words — `615/high`,
13896
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13897
+ * family and inventing a common one would lose the only information that
13898
+ * makes two entries for the same camera distinguishable.
13899
+ */
13900
+ unit: string(),
13901
+ /**
13902
+ * The OS process this cost lives in, when there is one. Present so a
13903
+ * consumer can (a) tell two generations of the same unit apart across a
13904
+ * restart, and (b) subtract claimed processes from the node's process
13905
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13906
+ * process of its own.
13907
+ */
13908
+ pid: number().int().positive().optional(),
13909
+ /**
13910
+ * When this generation started. The pid's incarnation marker: a consumer
13911
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13912
+ * window when this changes, because the counter restarted from zero in a new
13913
+ * process.
13914
+ */
13915
+ startedAtMs: number().optional(),
13916
+ /**
13917
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13918
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13919
+ * contribution is asked for.
13920
+ *
13921
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13922
+ * needs a sampler, and a new per-node sampler is the defect half of
13923
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13924
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13925
+ *
13926
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13927
+ * an entry with no process.
13928
+ */
13929
+ cpuSeconds: number().optional(),
13930
+ /** Resident bytes of this unit's process, same source and same rules. */
13931
+ rssBytes: number().optional()
13932
+ });
13933
+ method(_void(), array(LoadContributionSchema).readonly());
13764
13934
  /**
13765
13935
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13766
13936
  * through. It stores nothing.
@@ -13837,176 +14007,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13837
14007
  tags: record(string(), string()).optional()
13838
14008
  }), array(LogEntrySchema).readonly());
13839
14009
  /**
13840
- * `failure-contribution` — the capability an addon reports its OWN losses
13841
- * through, per camera, with the denominator attached. It stores nothing.
13842
- *
13843
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13844
- *
13845
- * `load-contribution` answers *what did this camera COST*. This answers *what
13846
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13847
- * copied: the contributor reports what it already knows, hub-main adds only
13848
- * `addonId`, nothing needs global knowledge, and there is no central list for
13849
- * somebody to forget to edit.
13850
- *
13851
- * They are not merged, because their invariants are opposites:
13852
- *
13853
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13854
- * claim a camera cost nothing, which is a measurement nobody made;
13855
- * - a `failure-contribution` zero is the **most valuable value on the
13856
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13857
- * and it is exactly what an absent entry cannot say.
13858
- *
13859
- * Putting a loss counter on a cost entry would also break the reconciliation
13860
- * that gives `load-contribution` its point: contributions are subtracted from
13861
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13862
- * has no process.
13863
- *
13864
- * ## Why not a log line, since the counters already exist
13865
- *
13866
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13867
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13868
- * ends in a log line, and a log line is the thing the operator asked to stop
13869
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13870
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13871
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13872
- * media blackout were both diagnosed. The counters stay; this is where they can
13873
- * be READ.
13874
- *
13875
- * ## The rate is served with its denominator or not at all
13876
- *
13877
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13878
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13879
- * than yesterday" and was **flat across twelve hours** once divided by the
13880
- * successes on the same path. A surface that publishes only the numerator
13881
- * reproduces that mistake on every read.
13882
- *
13883
- * ## Shape
13884
- *
13885
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13886
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13887
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13888
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13889
- * a forked runner's entries reach hub-main over transport that already exists.
13890
- * No new UDS message, no second registry (D3). The operator reads the assembled
13891
- * result through `system.getFailureContributions`.
13892
- */
13893
- var FailureReasonCountSchema = object({
13894
- /**
13895
- * Why the attempt did not land, in the contributor's own vocabulary —
13896
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13897
- * strings that already appear in this repo's logs and, where one exists, the
13898
- * same string the per-track `previewMissReason` records (D276): a second
13899
- * vocabulary for the same loss would make the row and the counter
13900
- * un-joinable.
13901
- */
13902
- reason: string(),
13903
- count: number().int().nonnegative()
13904
- });
13905
- var FailureContributionSchema = object({
13906
- /**
13907
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13908
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13909
- * `unit` free: the families are owned by different addons and a shared enum
13910
- * is a central list that rots invisibly.
13911
- */
13912
- family: string(),
13913
- /**
13914
- * The NUMERIC device id — the same value every log line carries as
13915
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13916
- * cannot name the camera must not emit the entry, because a fleet total
13917
- * cannot answer the only question anybody asks of this surface.
13918
- */
13919
- deviceId: number().int().positive(),
13920
- /**
13921
- * A second dimension inside the family: the model / step id for an inference
13922
- * timeout, so "which camera AND which model" is one read. Absent when the
13923
- * family has a single variant.
13924
- */
13925
- variant: string().optional(),
13926
- /**
13927
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13928
- * differencing two reads must drop the interval when it changes, because the
13929
- * counter restarted from zero in a respawned runner. Same discipline as
13930
- * `LoadContribution.startedAtMs`.
13931
- */
13932
- sinceMs: number(),
13933
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13934
- atMs: number(),
13935
- /**
13936
- * THE DENOMINATOR — every attempt on this path for this camera in the
13937
- * window. A failure count published without it is the mistake this schema
13938
- * exists to make impossible.
13939
- */
13940
- attempts: number().int().nonnegative(),
13941
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13942
- succeeded: number().int().nonnegative(),
13943
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13944
- reasons: array(FailureReasonCountSchema).readonly()
13945
- });
13946
- method(_void(), array(FailureContributionSchema).readonly());
13947
- var LoadContributionSchema = object({
13948
- role: _enum([
13949
- "decode",
13950
- "transcode",
13951
- "recording",
13952
- "streaming",
13953
- "detection"
13954
- ]),
13955
- /**
13956
- * The NUMERIC device id — the same value every log line carries as
13957
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13958
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13959
- * contributor that cannot name its camera must not emit the entry at all,
13960
- * because an unnamed per-camera entry is indistinguishable from a shared one
13961
- * and would quietly turn one camera's cost into everybody's.
13962
- */
13963
- deviceId: number().int().positive().nullable(),
13964
- attribution: _enum([
13965
- "measured",
13966
- "accounted",
13967
- "unattributable"
13968
- ]),
13969
- /**
13970
- * What ONE entry is, in the contributor's own words — `615/high`,
13971
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13972
- * family and inventing a common one would lose the only information that
13973
- * makes two entries for the same camera distinguishable.
13974
- */
13975
- unit: string(),
13976
- /**
13977
- * The OS process this cost lives in, when there is one. Present so a
13978
- * consumer can (a) tell two generations of the same unit apart across a
13979
- * restart, and (b) subtract claimed processes from the node's process
13980
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13981
- * process of its own.
13982
- */
13983
- pid: number().int().positive().optional(),
13984
- /**
13985
- * When this generation started. The pid's incarnation marker: a consumer
13986
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13987
- * window when this changes, because the counter restarted from zero in a new
13988
- * process.
13989
- */
13990
- startedAtMs: number().optional(),
13991
- /**
13992
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13993
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13994
- * contribution is asked for.
13995
- *
13996
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13997
- * needs a sampler, and a new per-node sampler is the defect half of
13998
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13999
- * by whoever already keeps a history; a rate cannot be un-averaged.
14000
- *
14001
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14002
- * an entry with no process.
14003
- */
14004
- cpuSeconds: number().optional(),
14005
- /** Resident bytes of this unit's process, same source and same rules. */
14006
- rssBytes: number().optional()
14007
- });
14008
- method(_void(), array(LoadContributionSchema).readonly());
14009
- /**
14010
14010
  * `login-method` — collection cap through which auth addons contribute
14011
14011
  * their pre-auth login surfaces to the login page. This is the SINGLE,
14012
14012
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18744,12 +18744,53 @@ var MediaFileKindEnum = _enum([
18744
18744
  "keyFrameSmall",
18745
18745
  "thumbnailSmall"
18746
18746
  ]);
18747
+ /**
18748
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18749
+ * ARE — never the bytes themselves.
18750
+ *
18751
+ * ## Why `url` and not `base64`
18752
+ *
18753
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18754
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18755
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18756
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18757
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18758
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18759
+ *
18760
+ * `url` points at the `event-media` data plane
18761
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18762
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18763
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18764
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18765
+ * no less protected than they were inside a `view`-level cap response — see
18766
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18767
+ * (per-device scoping).
18768
+ *
18769
+ * The URL is built from the row's **stored** key, which is not always its
18770
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18771
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18772
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18773
+ *
18774
+ * ## `base64` is TRANSITIONAL and is going away
18775
+ *
18776
+ * It is still populated for one reason: the deployed viewer's track-detail
18777
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18778
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18779
+ * triangle — not as absence. Removing the field before that viewer ships is an
18780
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18781
+ * delete this line and the `withBytes` pass-through in
18782
+ * `analytics-query-facade.ts`; nothing else reads it.
18783
+ */
18747
18784
  var MediaFileSchema = object({
18748
18785
  key: string(),
18749
18786
  kind: MediaFileKindEnum,
18750
- base64: string(),
18751
18787
  sizeBytes: number(),
18752
18788
  timestamp: number()
18789
+ }).extend({
18790
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18791
+ url: string(),
18792
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18793
+ base64: string()
18753
18794
  });
18754
18795
  /**
18755
18796
  * One media row WITHOUT its bytes.
@@ -18761,7 +18802,9 @@ var MediaFileSchema = object({
18761
18802
  * blocks the whole view.
18762
18803
  *
18763
18804
  * `sizeBytes` is carried because it is what lets a client decide between the
18764
- * stored blob and a `?variant=thumb` rendering without fetching either.
18805
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18806
+ * `url` because a client that had to build the plane path itself is a second
18807
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18765
18808
  */
18766
18809
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18767
18810
  /**
@@ -19108,6 +19151,50 @@ var EventStoreFootprintSchema = object({
19108
19151
  totalBytes: number().int(),
19109
19152
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19110
19153
  });
19154
+ /** Event-media footprint for one {@link MediaFileKind}. */
19155
+ var EventMediaKindFootprintSchema = object({
19156
+ kind: MediaFileKindEnum,
19157
+ /** Media rows of this kind. */
19158
+ rows: number().int(),
19159
+ /** Bytes on disk held by those rows. */
19160
+ bytes: number().int()
19161
+ });
19162
+ /**
19163
+ * The media footprint broken down by KIND — the axis a deletion decision
19164
+ * actually turns on.
19165
+ *
19166
+ * A byte total says how much there is; it cannot say what is safe to remove.
19167
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19168
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19169
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19170
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19171
+ * nothing else, so sizing a deletion means summing per kind.
19172
+ *
19173
+ * ## Why `unaccounted*` exists
19174
+ *
19175
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19176
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19177
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19178
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19179
+ * retired code path, or by a version that knew a kind this one does not) would
19180
+ * otherwise vanish from the total silently, and an operator would delete
19181
+ * against a denominator smaller than the disk.
19182
+ *
19183
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19184
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19185
+ */
19186
+ var EventMediaKindBreakdownSchema = object({
19187
+ /** Every media row in scope, from one unfiltered aggregate. */
19188
+ totalRows: number().int(),
19189
+ /** Every media byte in scope, from that same aggregate. */
19190
+ totalBytes: number().int(),
19191
+ /** Per-kind footprint, ordered by bytes descending. */
19192
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19193
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19194
+ unaccountedRows: number().int(),
19195
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19196
+ unaccountedBytes: number().int()
19197
+ });
19111
19198
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19112
19199
  var EventPruneCountsSchema = object({
19113
19200
  motion: number().int(),
@@ -19311,6 +19398,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19311
19398
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19312
19399
  kind: "query",
19313
19400
  auth: "admin"
19401
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19402
+ kind: "query",
19403
+ auth: "admin"
19314
19404
  }), method(object({
19315
19405
  olderThanMs: number(),
19316
19406
  reason: OpsLogReasonSchema.optional()
@@ -19450,6 +19540,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19450
19540
  }), array(MediaFileSchema).readonly()), method(object({
19451
19541
  trackId: string(),
19452
19542
  deviceId: number()
19543
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19544
+ eventId: string(),
19545
+ deviceId: number()
19453
19546
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19454
19547
  kind: "mutation",
19455
19548
  auth: "admin"
@@ -21363,6 +21456,20 @@ method(object({
21363
21456
  error: string().optional()
21364
21457
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21365
21458
  providerId: string(),
21459
+ /**
21460
+ * The location this config is an UNSAVED edit of, when there is one.
21461
+ *
21462
+ * `listLocations` replaces every declared secret with the redaction
21463
+ * sentinel, so the edit modal's form state holds the sentinel for any
21464
+ * credential the operator did not retype — and posting that here
21465
+ * without a way to resolve it makes the provider try to authenticate
21466
+ * as `__camstack_redacted__` and report the operator's own working
21467
+ * password as wrong. Given this id, the orchestrator restores each
21468
+ * sentinel from the stored config (same rule as `upsertLocation`)
21469
+ * before dispatching. Omitted by the "Add location" wizard, where
21470
+ * every value was typed just now and nothing is stored yet.
21471
+ */
21472
+ locationId: string().optional(),
21366
21473
  config: record(string(), unknown())
21367
21474
  }), object({
21368
21475
  ok: boolean(),
@@ -24552,10 +24659,24 @@ var FaceClusterSchema = object({
24552
24659
  size: number().int(),
24553
24660
  cohesion: number()
24554
24661
  });
24662
+ /**
24663
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24664
+ * are — never the bytes.
24665
+ *
24666
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24667
+ * track/event contract) is still populated because a deployed viewer requires
24668
+ * the field to parse a row at all; this method has no such reader. Its ONE
24669
+ * caller is the admin UI's detail modal, which was building
24670
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24671
+ * dialog already rendering its key FRAME from the `event-media` plane.
24672
+ *
24673
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24674
+ * media key directly, so this needed no new plane and no new access decision.
24675
+ */
24555
24676
  var MediaFileLiteSchema$1 = object({
24556
24677
  key: string(),
24557
24678
  kind: string(),
24558
- base64: string(),
24679
+ url: string(),
24559
24680
  sizeBytes: number(),
24560
24681
  timestamp: number()
24561
24682
  });
@@ -27577,10 +27698,24 @@ var PlateInfoSchema = object({
27577
27698
  */
27578
27699
  cropUrl: string().optional()
27579
27700
  });
27701
+ /**
27702
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27703
+ * are — never the bytes.
27704
+ *
27705
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27706
+ * track/event contract) is still populated because a deployed viewer requires
27707
+ * the field to parse a row at all; this method has no such reader. Its ONE
27708
+ * caller is the admin UI's detail modal, which was building
27709
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27710
+ * dialog already rendering its key FRAME from the `event-media` plane.
27711
+ *
27712
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27713
+ * media key directly, so this needed no new plane and no new access decision.
27714
+ */
27580
27715
  var MediaFileLiteSchema = object({
27581
27716
  key: string(),
27582
27717
  kind: string(),
27583
- base64: string(),
27718
+ url: string(),
27584
27719
  sizeBytes: number(),
27585
27720
  timestamp: number()
27586
27721
  });
@@ -35447,6 +35582,12 @@ Object.freeze({
35447
35582
  addonId: null,
35448
35583
  access: "view"
35449
35584
  },
35585
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
35586
+ capName: "pipeline-analytics",
35587
+ capScope: "device",
35588
+ addonId: null,
35589
+ access: "view"
35590
+ },
35450
35591
  "pipelineAnalytics.getEventStoreFootprint": {
35451
35592
  capName: "pipeline-analytics",
35452
35593
  capScope: "device",
@@ -35543,6 +35684,12 @@ Object.freeze({
35543
35684
  addonId: null,
35544
35685
  access: "view"
35545
35686
  },
35687
+ "pipelineAnalytics.listEventMedia": {
35688
+ capName: "pipeline-analytics",
35689
+ capScope: "device",
35690
+ addonId: null,
35691
+ access: "view"
35692
+ },
35546
35693
  "pipelineAnalytics.listGroups": {
35547
35694
  capName: "pipeline-analytics",
35548
35695
  capScope: "device",
@@ -39106,6 +39253,11 @@ Object.freeze({
39106
39253
  form: "single",
39107
39254
  optional: false
39108
39255
  }],
39256
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
39257
+ name: "deviceId",
39258
+ form: "single",
39259
+ optional: true
39260
+ }],
39109
39261
  "pipelineAnalytics.getGroup": [{
39110
39262
  name: "deviceId",
39111
39263
  form: "single",
@@ -39166,6 +39318,11 @@ Object.freeze({
39166
39318
  form: "array",
39167
39319
  optional: false
39168
39320
  }],
39321
+ "pipelineAnalytics.listEventMedia": [{
39322
+ name: "deviceId",
39323
+ form: "single",
39324
+ optional: false
39325
+ }],
39169
39326
  "pipelineAnalytics.listGroups": [{
39170
39327
  name: "deviceIds",
39171
39328
  form: "array",
package/dist/addon.mjs CHANGED
@@ -13216,6 +13216,114 @@ method(object({
13216
13216
  height: number()
13217
13217
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13218
13218
  /**
13219
+ * `failure-contribution` — the capability an addon reports its OWN losses
13220
+ * through, per camera, with the denominator attached. It stores nothing.
13221
+ *
13222
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13223
+ *
13224
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13225
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13226
+ * copied: the contributor reports what it already knows, hub-main adds only
13227
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13228
+ * somebody to forget to edit.
13229
+ *
13230
+ * They are not merged, because their invariants are opposites:
13231
+ *
13232
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13233
+ * claim a camera cost nothing, which is a measurement nobody made;
13234
+ * - a `failure-contribution` zero is the **most valuable value on the
13235
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13236
+ * and it is exactly what an absent entry cannot say.
13237
+ *
13238
+ * Putting a loss counter on a cost entry would also break the reconciliation
13239
+ * that gives `load-contribution` its point: contributions are subtracted from
13240
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13241
+ * has no process.
13242
+ *
13243
+ * ## Why not a log line, since the counters already exist
13244
+ *
13245
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13246
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13247
+ * ends in a log line, and a log line is the thing the operator asked to stop
13248
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13249
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13250
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13251
+ * media blackout were both diagnosed. The counters stay; this is where they can
13252
+ * be READ.
13253
+ *
13254
+ * ## The rate is served with its denominator or not at all
13255
+ *
13256
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13257
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13258
+ * than yesterday" and was **flat across twelve hours** once divided by the
13259
+ * successes on the same path. A surface that publishes only the numerator
13260
+ * reproduces that mistake on every read.
13261
+ *
13262
+ * ## Shape
13263
+ *
13264
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13265
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13266
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13267
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13268
+ * a forked runner's entries reach hub-main over transport that already exists.
13269
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13270
+ * result through `system.getFailureContributions`.
13271
+ */
13272
+ var FailureReasonCountSchema = object({
13273
+ /**
13274
+ * Why the attempt did not land, in the contributor's own vocabulary —
13275
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13276
+ * strings that already appear in this repo's logs and, where one exists, the
13277
+ * same string the per-track `previewMissReason` records (D276): a second
13278
+ * vocabulary for the same loss would make the row and the counter
13279
+ * un-joinable.
13280
+ */
13281
+ reason: string(),
13282
+ count: number().int().nonnegative()
13283
+ });
13284
+ var FailureContributionSchema = object({
13285
+ /**
13286
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13287
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13288
+ * `unit` free: the families are owned by different addons and a shared enum
13289
+ * is a central list that rots invisibly.
13290
+ */
13291
+ family: string(),
13292
+ /**
13293
+ * The NUMERIC device id — the same value every log line carries as
13294
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13295
+ * cannot name the camera must not emit the entry, because a fleet total
13296
+ * cannot answer the only question anybody asks of this surface.
13297
+ */
13298
+ deviceId: number().int().positive(),
13299
+ /**
13300
+ * A second dimension inside the family: the model / step id for an inference
13301
+ * timeout, so "which camera AND which model" is one read. Absent when the
13302
+ * family has a single variant.
13303
+ */
13304
+ variant: string().optional(),
13305
+ /**
13306
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13307
+ * differencing two reads must drop the interval when it changes, because the
13308
+ * counter restarted from zero in a respawned runner. Same discipline as
13309
+ * `LoadContribution.startedAtMs`.
13310
+ */
13311
+ sinceMs: number(),
13312
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13313
+ atMs: number(),
13314
+ /**
13315
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13316
+ * window. A failure count published without it is the mistake this schema
13317
+ * exists to make impossible.
13318
+ */
13319
+ attempts: number().int().nonnegative(),
13320
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13321
+ succeeded: number().int().nonnegative(),
13322
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13323
+ reasons: array(FailureReasonCountSchema).readonly()
13324
+ });
13325
+ method(_void(), array(FailureContributionSchema).readonly());
13326
+ /**
13219
13327
  * filesystem-browse — per-node capability for browsing the node's local
13220
13328
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13221
13329
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13737,6 +13845,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13737
13845
  kind: "mutation",
13738
13846
  auth: "admin"
13739
13847
  });
13848
+ var LoadContributionSchema = object({
13849
+ role: _enum([
13850
+ "decode",
13851
+ "transcode",
13852
+ "recording",
13853
+ "streaming",
13854
+ "detection"
13855
+ ]),
13856
+ /**
13857
+ * The NUMERIC device id — the same value every log line carries as
13858
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13859
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13860
+ * contributor that cannot name its camera must not emit the entry at all,
13861
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13862
+ * and would quietly turn one camera's cost into everybody's.
13863
+ */
13864
+ deviceId: number().int().positive().nullable(),
13865
+ attribution: _enum([
13866
+ "measured",
13867
+ "accounted",
13868
+ "unattributable"
13869
+ ]),
13870
+ /**
13871
+ * What ONE entry is, in the contributor's own words — `615/high`,
13872
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13873
+ * family and inventing a common one would lose the only information that
13874
+ * makes two entries for the same camera distinguishable.
13875
+ */
13876
+ unit: string(),
13877
+ /**
13878
+ * The OS process this cost lives in, when there is one. Present so a
13879
+ * consumer can (a) tell two generations of the same unit apart across a
13880
+ * restart, and (b) subtract claimed processes from the node's process
13881
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13882
+ * process of its own.
13883
+ */
13884
+ pid: number().int().positive().optional(),
13885
+ /**
13886
+ * When this generation started. The pid's incarnation marker: a consumer
13887
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13888
+ * window when this changes, because the counter restarted from zero in a new
13889
+ * process.
13890
+ */
13891
+ startedAtMs: number().optional(),
13892
+ /**
13893
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13894
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13895
+ * contribution is asked for.
13896
+ *
13897
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13898
+ * needs a sampler, and a new per-node sampler is the defect half of
13899
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13900
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13901
+ *
13902
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13903
+ * an entry with no process.
13904
+ */
13905
+ cpuSeconds: number().optional(),
13906
+ /** Resident bytes of this unit's process, same source and same rules. */
13907
+ rssBytes: number().optional()
13908
+ });
13909
+ method(_void(), array(LoadContributionSchema).readonly());
13740
13910
  /**
13741
13911
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13742
13912
  * through. It stores nothing.
@@ -13813,176 +13983,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13813
13983
  tags: record(string(), string()).optional()
13814
13984
  }), array(LogEntrySchema).readonly());
13815
13985
  /**
13816
- * `failure-contribution` — the capability an addon reports its OWN losses
13817
- * through, per camera, with the denominator attached. It stores nothing.
13818
- *
13819
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13820
- *
13821
- * `load-contribution` answers *what did this camera COST*. This answers *what
13822
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13823
- * copied: the contributor reports what it already knows, hub-main adds only
13824
- * `addonId`, nothing needs global knowledge, and there is no central list for
13825
- * somebody to forget to edit.
13826
- *
13827
- * They are not merged, because their invariants are opposites:
13828
- *
13829
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13830
- * claim a camera cost nothing, which is a measurement nobody made;
13831
- * - a `failure-contribution` zero is the **most valuable value on the
13832
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13833
- * and it is exactly what an absent entry cannot say.
13834
- *
13835
- * Putting a loss counter on a cost entry would also break the reconciliation
13836
- * that gives `load-contribution` its point: contributions are subtracted from
13837
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13838
- * has no process.
13839
- *
13840
- * ## Why not a log line, since the counters already exist
13841
- *
13842
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13843
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13844
- * ends in a log line, and a log line is the thing the operator asked to stop
13845
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13846
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13847
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13848
- * media blackout were both diagnosed. The counters stay; this is where they can
13849
- * be READ.
13850
- *
13851
- * ## The rate is served with its denominator or not at all
13852
- *
13853
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13854
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13855
- * than yesterday" and was **flat across twelve hours** once divided by the
13856
- * successes on the same path. A surface that publishes only the numerator
13857
- * reproduces that mistake on every read.
13858
- *
13859
- * ## Shape
13860
- *
13861
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13862
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13863
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13864
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13865
- * a forked runner's entries reach hub-main over transport that already exists.
13866
- * No new UDS message, no second registry (D3). The operator reads the assembled
13867
- * result through `system.getFailureContributions`.
13868
- */
13869
- var FailureReasonCountSchema = object({
13870
- /**
13871
- * Why the attempt did not land, in the contributor's own vocabulary —
13872
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13873
- * strings that already appear in this repo's logs and, where one exists, the
13874
- * same string the per-track `previewMissReason` records (D276): a second
13875
- * vocabulary for the same loss would make the row and the counter
13876
- * un-joinable.
13877
- */
13878
- reason: string(),
13879
- count: number().int().nonnegative()
13880
- });
13881
- var FailureContributionSchema = object({
13882
- /**
13883
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13884
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13885
- * `unit` free: the families are owned by different addons and a shared enum
13886
- * is a central list that rots invisibly.
13887
- */
13888
- family: string(),
13889
- /**
13890
- * The NUMERIC device id — the same value every log line carries as
13891
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13892
- * cannot name the camera must not emit the entry, because a fleet total
13893
- * cannot answer the only question anybody asks of this surface.
13894
- */
13895
- deviceId: number().int().positive(),
13896
- /**
13897
- * A second dimension inside the family: the model / step id for an inference
13898
- * timeout, so "which camera AND which model" is one read. Absent when the
13899
- * family has a single variant.
13900
- */
13901
- variant: string().optional(),
13902
- /**
13903
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13904
- * differencing two reads must drop the interval when it changes, because the
13905
- * counter restarted from zero in a respawned runner. Same discipline as
13906
- * `LoadContribution.startedAtMs`.
13907
- */
13908
- sinceMs: number(),
13909
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13910
- atMs: number(),
13911
- /**
13912
- * THE DENOMINATOR — every attempt on this path for this camera in the
13913
- * window. A failure count published without it is the mistake this schema
13914
- * exists to make impossible.
13915
- */
13916
- attempts: number().int().nonnegative(),
13917
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13918
- succeeded: number().int().nonnegative(),
13919
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13920
- reasons: array(FailureReasonCountSchema).readonly()
13921
- });
13922
- method(_void(), array(FailureContributionSchema).readonly());
13923
- var LoadContributionSchema = object({
13924
- role: _enum([
13925
- "decode",
13926
- "transcode",
13927
- "recording",
13928
- "streaming",
13929
- "detection"
13930
- ]),
13931
- /**
13932
- * The NUMERIC device id — the same value every log line carries as
13933
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13934
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13935
- * contributor that cannot name its camera must not emit the entry at all,
13936
- * because an unnamed per-camera entry is indistinguishable from a shared one
13937
- * and would quietly turn one camera's cost into everybody's.
13938
- */
13939
- deviceId: number().int().positive().nullable(),
13940
- attribution: _enum([
13941
- "measured",
13942
- "accounted",
13943
- "unattributable"
13944
- ]),
13945
- /**
13946
- * What ONE entry is, in the contributor's own words — `615/high`,
13947
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13948
- * family and inventing a common one would lose the only information that
13949
- * makes two entries for the same camera distinguishable.
13950
- */
13951
- unit: string(),
13952
- /**
13953
- * The OS process this cost lives in, when there is one. Present so a
13954
- * consumer can (a) tell two generations of the same unit apart across a
13955
- * restart, and (b) subtract claimed processes from the node's process
13956
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13957
- * process of its own.
13958
- */
13959
- pid: number().int().positive().optional(),
13960
- /**
13961
- * When this generation started. The pid's incarnation marker: a consumer
13962
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13963
- * window when this changes, because the counter restarted from zero in a new
13964
- * process.
13965
- */
13966
- startedAtMs: number().optional(),
13967
- /**
13968
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13969
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13970
- * contribution is asked for.
13971
- *
13972
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13973
- * needs a sampler, and a new per-node sampler is the defect half of
13974
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13975
- * by whoever already keeps a history; a rate cannot be un-averaged.
13976
- *
13977
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13978
- * an entry with no process.
13979
- */
13980
- cpuSeconds: number().optional(),
13981
- /** Resident bytes of this unit's process, same source and same rules. */
13982
- rssBytes: number().optional()
13983
- });
13984
- method(_void(), array(LoadContributionSchema).readonly());
13985
- /**
13986
13986
  * `login-method` — collection cap through which auth addons contribute
13987
13987
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13988
13988
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18720,12 +18720,53 @@ var MediaFileKindEnum = _enum([
18720
18720
  "keyFrameSmall",
18721
18721
  "thumbnailSmall"
18722
18722
  ]);
18723
+ /**
18724
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18725
+ * ARE — never the bytes themselves.
18726
+ *
18727
+ * ## Why `url` and not `base64`
18728
+ *
18729
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18730
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18731
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18732
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18733
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18734
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18735
+ *
18736
+ * `url` points at the `event-media` data plane
18737
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18738
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18739
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18740
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18741
+ * no less protected than they were inside a `view`-level cap response — see
18742
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18743
+ * (per-device scoping).
18744
+ *
18745
+ * The URL is built from the row's **stored** key, which is not always its
18746
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18747
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18748
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18749
+ *
18750
+ * ## `base64` is TRANSITIONAL and is going away
18751
+ *
18752
+ * It is still populated for one reason: the deployed viewer's track-detail
18753
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18754
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18755
+ * triangle — not as absence. Removing the field before that viewer ships is an
18756
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18757
+ * delete this line and the `withBytes` pass-through in
18758
+ * `analytics-query-facade.ts`; nothing else reads it.
18759
+ */
18723
18760
  var MediaFileSchema = object({
18724
18761
  key: string(),
18725
18762
  kind: MediaFileKindEnum,
18726
- base64: string(),
18727
18763
  sizeBytes: number(),
18728
18764
  timestamp: number()
18765
+ }).extend({
18766
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18767
+ url: string(),
18768
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18769
+ base64: string()
18729
18770
  });
18730
18771
  /**
18731
18772
  * One media row WITHOUT its bytes.
@@ -18737,7 +18778,9 @@ var MediaFileSchema = object({
18737
18778
  * blocks the whole view.
18738
18779
  *
18739
18780
  * `sizeBytes` is carried because it is what lets a client decide between the
18740
- * stored blob and a `?variant=thumb` rendering without fetching either.
18781
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18782
+ * `url` because a client that had to build the plane path itself is a second
18783
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18741
18784
  */
18742
18785
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18743
18786
  /**
@@ -19084,6 +19127,50 @@ var EventStoreFootprintSchema = object({
19084
19127
  totalBytes: number().int(),
19085
19128
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19086
19129
  });
19130
+ /** Event-media footprint for one {@link MediaFileKind}. */
19131
+ var EventMediaKindFootprintSchema = object({
19132
+ kind: MediaFileKindEnum,
19133
+ /** Media rows of this kind. */
19134
+ rows: number().int(),
19135
+ /** Bytes on disk held by those rows. */
19136
+ bytes: number().int()
19137
+ });
19138
+ /**
19139
+ * The media footprint broken down by KIND — the axis a deletion decision
19140
+ * actually turns on.
19141
+ *
19142
+ * A byte total says how much there is; it cannot say what is safe to remove.
19143
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19144
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19145
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19146
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19147
+ * nothing else, so sizing a deletion means summing per kind.
19148
+ *
19149
+ * ## Why `unaccounted*` exists
19150
+ *
19151
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19152
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19153
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19154
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19155
+ * retired code path, or by a version that knew a kind this one does not) would
19156
+ * otherwise vanish from the total silently, and an operator would delete
19157
+ * against a denominator smaller than the disk.
19158
+ *
19159
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19160
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19161
+ */
19162
+ var EventMediaKindBreakdownSchema = object({
19163
+ /** Every media row in scope, from one unfiltered aggregate. */
19164
+ totalRows: number().int(),
19165
+ /** Every media byte in scope, from that same aggregate. */
19166
+ totalBytes: number().int(),
19167
+ /** Per-kind footprint, ordered by bytes descending. */
19168
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19169
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19170
+ unaccountedRows: number().int(),
19171
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19172
+ unaccountedBytes: number().int()
19173
+ });
19087
19174
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19088
19175
  var EventPruneCountsSchema = object({
19089
19176
  motion: number().int(),
@@ -19287,6 +19374,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19287
19374
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19288
19375
  kind: "query",
19289
19376
  auth: "admin"
19377
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19378
+ kind: "query",
19379
+ auth: "admin"
19290
19380
  }), method(object({
19291
19381
  olderThanMs: number(),
19292
19382
  reason: OpsLogReasonSchema.optional()
@@ -19426,6 +19516,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19426
19516
  }), array(MediaFileSchema).readonly()), method(object({
19427
19517
  trackId: string(),
19428
19518
  deviceId: number()
19519
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19520
+ eventId: string(),
19521
+ deviceId: number()
19429
19522
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19430
19523
  kind: "mutation",
19431
19524
  auth: "admin"
@@ -21339,6 +21432,20 @@ method(object({
21339
21432
  error: string().optional()
21340
21433
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21341
21434
  providerId: string(),
21435
+ /**
21436
+ * The location this config is an UNSAVED edit of, when there is one.
21437
+ *
21438
+ * `listLocations` replaces every declared secret with the redaction
21439
+ * sentinel, so the edit modal's form state holds the sentinel for any
21440
+ * credential the operator did not retype — and posting that here
21441
+ * without a way to resolve it makes the provider try to authenticate
21442
+ * as `__camstack_redacted__` and report the operator's own working
21443
+ * password as wrong. Given this id, the orchestrator restores each
21444
+ * sentinel from the stored config (same rule as `upsertLocation`)
21445
+ * before dispatching. Omitted by the "Add location" wizard, where
21446
+ * every value was typed just now and nothing is stored yet.
21447
+ */
21448
+ locationId: string().optional(),
21342
21449
  config: record(string(), unknown())
21343
21450
  }), object({
21344
21451
  ok: boolean(),
@@ -24528,10 +24635,24 @@ var FaceClusterSchema = object({
24528
24635
  size: number().int(),
24529
24636
  cohesion: number()
24530
24637
  });
24638
+ /**
24639
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24640
+ * are — never the bytes.
24641
+ *
24642
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24643
+ * track/event contract) is still populated because a deployed viewer requires
24644
+ * the field to parse a row at all; this method has no such reader. Its ONE
24645
+ * caller is the admin UI's detail modal, which was building
24646
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24647
+ * dialog already rendering its key FRAME from the `event-media` plane.
24648
+ *
24649
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24650
+ * media key directly, so this needed no new plane and no new access decision.
24651
+ */
24531
24652
  var MediaFileLiteSchema$1 = object({
24532
24653
  key: string(),
24533
24654
  kind: string(),
24534
- base64: string(),
24655
+ url: string(),
24535
24656
  sizeBytes: number(),
24536
24657
  timestamp: number()
24537
24658
  });
@@ -27553,10 +27674,24 @@ var PlateInfoSchema = object({
27553
27674
  */
27554
27675
  cropUrl: string().optional()
27555
27676
  });
27677
+ /**
27678
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27679
+ * are — never the bytes.
27680
+ *
27681
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27682
+ * track/event contract) is still populated because a deployed viewer requires
27683
+ * the field to parse a row at all; this method has no such reader. Its ONE
27684
+ * caller is the admin UI's detail modal, which was building
27685
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27686
+ * dialog already rendering its key FRAME from the `event-media` plane.
27687
+ *
27688
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27689
+ * media key directly, so this needed no new plane and no new access decision.
27690
+ */
27556
27691
  var MediaFileLiteSchema = object({
27557
27692
  key: string(),
27558
27693
  kind: string(),
27559
- base64: string(),
27694
+ url: string(),
27560
27695
  sizeBytes: number(),
27561
27696
  timestamp: number()
27562
27697
  });
@@ -35423,6 +35558,12 @@ Object.freeze({
35423
35558
  addonId: null,
35424
35559
  access: "view"
35425
35560
  },
35561
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
35562
+ capName: "pipeline-analytics",
35563
+ capScope: "device",
35564
+ addonId: null,
35565
+ access: "view"
35566
+ },
35426
35567
  "pipelineAnalytics.getEventStoreFootprint": {
35427
35568
  capName: "pipeline-analytics",
35428
35569
  capScope: "device",
@@ -35519,6 +35660,12 @@ Object.freeze({
35519
35660
  addonId: null,
35520
35661
  access: "view"
35521
35662
  },
35663
+ "pipelineAnalytics.listEventMedia": {
35664
+ capName: "pipeline-analytics",
35665
+ capScope: "device",
35666
+ addonId: null,
35667
+ access: "view"
35668
+ },
35522
35669
  "pipelineAnalytics.listGroups": {
35523
35670
  capName: "pipeline-analytics",
35524
35671
  capScope: "device",
@@ -39082,6 +39229,11 @@ Object.freeze({
39082
39229
  form: "single",
39083
39230
  optional: false
39084
39231
  }],
39232
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
39233
+ name: "deviceId",
39234
+ form: "single",
39235
+ optional: true
39236
+ }],
39085
39237
  "pipelineAnalytics.getGroup": [{
39086
39238
  name: "deviceId",
39087
39239
  form: "single",
@@ -39142,6 +39294,11 @@ Object.freeze({
39142
39294
  form: "array",
39143
39295
  optional: false
39144
39296
  }],
39297
+ "pipelineAnalytics.listEventMedia": [{
39298
+ name: "deviceId",
39299
+ form: "single",
39300
+ optional: false
39301
+ }],
39145
39302
  "pipelineAnalytics.listGroups": [{
39146
39303
  name: "deviceIds",
39147
39304
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-rtsp",
3
- "version": "1.2.46",
3
+ "version": "1.2.48",
4
4
  "description": "Generic RTSP camera device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",