@camstack/addon-terminal 0.1.51 → 0.1.53

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
@@ -13228,6 +13228,114 @@ method(object({
13228
13228
  height: number()
13229
13229
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13230
13230
  /**
13231
+ * `failure-contribution` — the capability an addon reports its OWN losses
13232
+ * through, per camera, with the denominator attached. It stores nothing.
13233
+ *
13234
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13235
+ *
13236
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13237
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13238
+ * copied: the contributor reports what it already knows, hub-main adds only
13239
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13240
+ * somebody to forget to edit.
13241
+ *
13242
+ * They are not merged, because their invariants are opposites:
13243
+ *
13244
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13245
+ * claim a camera cost nothing, which is a measurement nobody made;
13246
+ * - a `failure-contribution` zero is the **most valuable value on the
13247
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13248
+ * and it is exactly what an absent entry cannot say.
13249
+ *
13250
+ * Putting a loss counter on a cost entry would also break the reconciliation
13251
+ * that gives `load-contribution` its point: contributions are subtracted from
13252
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13253
+ * has no process.
13254
+ *
13255
+ * ## Why not a log line, since the counters already exist
13256
+ *
13257
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13258
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13259
+ * ends in a log line, and a log line is the thing the operator asked to stop
13260
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13261
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13262
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13263
+ * media blackout were both diagnosed. The counters stay; this is where they can
13264
+ * be READ.
13265
+ *
13266
+ * ## The rate is served with its denominator or not at all
13267
+ *
13268
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13269
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13270
+ * than yesterday" and was **flat across twelve hours** once divided by the
13271
+ * successes on the same path. A surface that publishes only the numerator
13272
+ * reproduces that mistake on every read.
13273
+ *
13274
+ * ## Shape
13275
+ *
13276
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13277
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13278
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13279
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13280
+ * a forked runner's entries reach hub-main over transport that already exists.
13281
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13282
+ * result through `system.getFailureContributions`.
13283
+ */
13284
+ var FailureReasonCountSchema = object({
13285
+ /**
13286
+ * Why the attempt did not land, in the contributor's own vocabulary —
13287
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13288
+ * strings that already appear in this repo's logs and, where one exists, the
13289
+ * same string the per-track `previewMissReason` records (D276): a second
13290
+ * vocabulary for the same loss would make the row and the counter
13291
+ * un-joinable.
13292
+ */
13293
+ reason: string(),
13294
+ count: number().int().nonnegative()
13295
+ });
13296
+ var FailureContributionSchema = object({
13297
+ /**
13298
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13299
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13300
+ * `unit` free: the families are owned by different addons and a shared enum
13301
+ * is a central list that rots invisibly.
13302
+ */
13303
+ family: string(),
13304
+ /**
13305
+ * The NUMERIC device id — the same value every log line carries as
13306
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13307
+ * cannot name the camera must not emit the entry, because a fleet total
13308
+ * cannot answer the only question anybody asks of this surface.
13309
+ */
13310
+ deviceId: number().int().positive(),
13311
+ /**
13312
+ * A second dimension inside the family: the model / step id for an inference
13313
+ * timeout, so "which camera AND which model" is one read. Absent when the
13314
+ * family has a single variant.
13315
+ */
13316
+ variant: string().optional(),
13317
+ /**
13318
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13319
+ * differencing two reads must drop the interval when it changes, because the
13320
+ * counter restarted from zero in a respawned runner. Same discipline as
13321
+ * `LoadContribution.startedAtMs`.
13322
+ */
13323
+ sinceMs: number(),
13324
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13325
+ atMs: number(),
13326
+ /**
13327
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13328
+ * window. A failure count published without it is the mistake this schema
13329
+ * exists to make impossible.
13330
+ */
13331
+ attempts: number().int().nonnegative(),
13332
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13333
+ succeeded: number().int().nonnegative(),
13334
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13335
+ reasons: array(FailureReasonCountSchema).readonly()
13336
+ });
13337
+ method(_void(), array(FailureContributionSchema).readonly());
13338
+ /**
13231
13339
  * filesystem-browse — per-node capability for browsing the node's local
13232
13340
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13233
13341
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13749,6 +13857,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13749
13857
  kind: "mutation",
13750
13858
  auth: "admin"
13751
13859
  });
13860
+ var LoadContributionSchema = object({
13861
+ role: _enum([
13862
+ "decode",
13863
+ "transcode",
13864
+ "recording",
13865
+ "streaming",
13866
+ "detection"
13867
+ ]),
13868
+ /**
13869
+ * The NUMERIC device id — the same value every log line carries as
13870
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13871
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13872
+ * contributor that cannot name its camera must not emit the entry at all,
13873
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13874
+ * and would quietly turn one camera's cost into everybody's.
13875
+ */
13876
+ deviceId: number().int().positive().nullable(),
13877
+ attribution: _enum([
13878
+ "measured",
13879
+ "accounted",
13880
+ "unattributable"
13881
+ ]),
13882
+ /**
13883
+ * What ONE entry is, in the contributor's own words — `615/high`,
13884
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13885
+ * family and inventing a common one would lose the only information that
13886
+ * makes two entries for the same camera distinguishable.
13887
+ */
13888
+ unit: string(),
13889
+ /**
13890
+ * The OS process this cost lives in, when there is one. Present so a
13891
+ * consumer can (a) tell two generations of the same unit apart across a
13892
+ * restart, and (b) subtract claimed processes from the node's process
13893
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13894
+ * process of its own.
13895
+ */
13896
+ pid: number().int().positive().optional(),
13897
+ /**
13898
+ * When this generation started. The pid's incarnation marker: a consumer
13899
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13900
+ * window when this changes, because the counter restarted from zero in a new
13901
+ * process.
13902
+ */
13903
+ startedAtMs: number().optional(),
13904
+ /**
13905
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13906
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13907
+ * contribution is asked for.
13908
+ *
13909
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13910
+ * needs a sampler, and a new per-node sampler is the defect half of
13911
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13912
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13913
+ *
13914
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13915
+ * an entry with no process.
13916
+ */
13917
+ cpuSeconds: number().optional(),
13918
+ /** Resident bytes of this unit's process, same source and same rules. */
13919
+ rssBytes: number().optional()
13920
+ });
13921
+ method(_void(), array(LoadContributionSchema).readonly());
13752
13922
  /**
13753
13923
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13754
13924
  * through. It stores nothing.
@@ -13825,176 +13995,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13825
13995
  tags: record(string(), string()).optional()
13826
13996
  }), array(LogEntrySchema).readonly());
13827
13997
  /**
13828
- * `failure-contribution` — the capability an addon reports its OWN losses
13829
- * through, per camera, with the denominator attached. It stores nothing.
13830
- *
13831
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13832
- *
13833
- * `load-contribution` answers *what did this camera COST*. This answers *what
13834
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13835
- * copied: the contributor reports what it already knows, hub-main adds only
13836
- * `addonId`, nothing needs global knowledge, and there is no central list for
13837
- * somebody to forget to edit.
13838
- *
13839
- * They are not merged, because their invariants are opposites:
13840
- *
13841
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13842
- * claim a camera cost nothing, which is a measurement nobody made;
13843
- * - a `failure-contribution` zero is the **most valuable value on the
13844
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13845
- * and it is exactly what an absent entry cannot say.
13846
- *
13847
- * Putting a loss counter on a cost entry would also break the reconciliation
13848
- * that gives `load-contribution` its point: contributions are subtracted from
13849
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13850
- * has no process.
13851
- *
13852
- * ## Why not a log line, since the counters already exist
13853
- *
13854
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13855
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13856
- * ends in a log line, and a log line is the thing the operator asked to stop
13857
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13858
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13859
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13860
- * media blackout were both diagnosed. The counters stay; this is where they can
13861
- * be READ.
13862
- *
13863
- * ## The rate is served with its denominator or not at all
13864
- *
13865
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13866
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13867
- * than yesterday" and was **flat across twelve hours** once divided by the
13868
- * successes on the same path. A surface that publishes only the numerator
13869
- * reproduces that mistake on every read.
13870
- *
13871
- * ## Shape
13872
- *
13873
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13874
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13875
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13876
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13877
- * a forked runner's entries reach hub-main over transport that already exists.
13878
- * No new UDS message, no second registry (D3). The operator reads the assembled
13879
- * result through `system.getFailureContributions`.
13880
- */
13881
- var FailureReasonCountSchema = object({
13882
- /**
13883
- * Why the attempt did not land, in the contributor's own vocabulary —
13884
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13885
- * strings that already appear in this repo's logs and, where one exists, the
13886
- * same string the per-track `previewMissReason` records (D276): a second
13887
- * vocabulary for the same loss would make the row and the counter
13888
- * un-joinable.
13889
- */
13890
- reason: string(),
13891
- count: number().int().nonnegative()
13892
- });
13893
- var FailureContributionSchema = object({
13894
- /**
13895
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13896
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13897
- * `unit` free: the families are owned by different addons and a shared enum
13898
- * is a central list that rots invisibly.
13899
- */
13900
- family: string(),
13901
- /**
13902
- * The NUMERIC device id — the same value every log line carries as
13903
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13904
- * cannot name the camera must not emit the entry, because a fleet total
13905
- * cannot answer the only question anybody asks of this surface.
13906
- */
13907
- deviceId: number().int().positive(),
13908
- /**
13909
- * A second dimension inside the family: the model / step id for an inference
13910
- * timeout, so "which camera AND which model" is one read. Absent when the
13911
- * family has a single variant.
13912
- */
13913
- variant: string().optional(),
13914
- /**
13915
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13916
- * differencing two reads must drop the interval when it changes, because the
13917
- * counter restarted from zero in a respawned runner. Same discipline as
13918
- * `LoadContribution.startedAtMs`.
13919
- */
13920
- sinceMs: number(),
13921
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13922
- atMs: number(),
13923
- /**
13924
- * THE DENOMINATOR — every attempt on this path for this camera in the
13925
- * window. A failure count published without it is the mistake this schema
13926
- * exists to make impossible.
13927
- */
13928
- attempts: number().int().nonnegative(),
13929
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13930
- succeeded: number().int().nonnegative(),
13931
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13932
- reasons: array(FailureReasonCountSchema).readonly()
13933
- });
13934
- method(_void(), array(FailureContributionSchema).readonly());
13935
- var LoadContributionSchema = object({
13936
- role: _enum([
13937
- "decode",
13938
- "transcode",
13939
- "recording",
13940
- "streaming",
13941
- "detection"
13942
- ]),
13943
- /**
13944
- * The NUMERIC device id — the same value every log line carries as
13945
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13946
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13947
- * contributor that cannot name its camera must not emit the entry at all,
13948
- * because an unnamed per-camera entry is indistinguishable from a shared one
13949
- * and would quietly turn one camera's cost into everybody's.
13950
- */
13951
- deviceId: number().int().positive().nullable(),
13952
- attribution: _enum([
13953
- "measured",
13954
- "accounted",
13955
- "unattributable"
13956
- ]),
13957
- /**
13958
- * What ONE entry is, in the contributor's own words — `615/high`,
13959
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13960
- * family and inventing a common one would lose the only information that
13961
- * makes two entries for the same camera distinguishable.
13962
- */
13963
- unit: string(),
13964
- /**
13965
- * The OS process this cost lives in, when there is one. Present so a
13966
- * consumer can (a) tell two generations of the same unit apart across a
13967
- * restart, and (b) subtract claimed processes from the node's process
13968
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13969
- * process of its own.
13970
- */
13971
- pid: number().int().positive().optional(),
13972
- /**
13973
- * When this generation started. The pid's incarnation marker: a consumer
13974
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13975
- * window when this changes, because the counter restarted from zero in a new
13976
- * process.
13977
- */
13978
- startedAtMs: number().optional(),
13979
- /**
13980
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13981
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13982
- * contribution is asked for.
13983
- *
13984
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13985
- * needs a sampler, and a new per-node sampler is the defect half of
13986
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13987
- * by whoever already keeps a history; a rate cannot be un-averaged.
13988
- *
13989
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13990
- * an entry with no process.
13991
- */
13992
- cpuSeconds: number().optional(),
13993
- /** Resident bytes of this unit's process, same source and same rules. */
13994
- rssBytes: number().optional()
13995
- });
13996
- method(_void(), array(LoadContributionSchema).readonly());
13997
- /**
13998
13998
  * `login-method` — collection cap through which auth addons contribute
13999
13999
  * their pre-auth login surfaces to the login page. This is the SINGLE,
14000
14000
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18732,12 +18732,53 @@ var MediaFileKindEnum = _enum([
18732
18732
  "keyFrameSmall",
18733
18733
  "thumbnailSmall"
18734
18734
  ]);
18735
+ /**
18736
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18737
+ * ARE — never the bytes themselves.
18738
+ *
18739
+ * ## Why `url` and not `base64`
18740
+ *
18741
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18742
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18743
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18744
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18745
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18746
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18747
+ *
18748
+ * `url` points at the `event-media` data plane
18749
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18750
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18751
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18752
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18753
+ * no less protected than they were inside a `view`-level cap response — see
18754
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18755
+ * (per-device scoping).
18756
+ *
18757
+ * The URL is built from the row's **stored** key, which is not always its
18758
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18759
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18760
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18761
+ *
18762
+ * ## `base64` is TRANSITIONAL and is going away
18763
+ *
18764
+ * It is still populated for one reason: the deployed viewer's track-detail
18765
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18766
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18767
+ * triangle — not as absence. Removing the field before that viewer ships is an
18768
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18769
+ * delete this line and the `withBytes` pass-through in
18770
+ * `analytics-query-facade.ts`; nothing else reads it.
18771
+ */
18735
18772
  var MediaFileSchema = object({
18736
18773
  key: string(),
18737
18774
  kind: MediaFileKindEnum,
18738
- base64: string(),
18739
18775
  sizeBytes: number(),
18740
18776
  timestamp: number()
18777
+ }).extend({
18778
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18779
+ url: string(),
18780
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18781
+ base64: string()
18741
18782
  });
18742
18783
  /**
18743
18784
  * One media row WITHOUT its bytes.
@@ -18749,7 +18790,9 @@ var MediaFileSchema = object({
18749
18790
  * blocks the whole view.
18750
18791
  *
18751
18792
  * `sizeBytes` is carried because it is what lets a client decide between the
18752
- * stored blob and a `?variant=thumb` rendering without fetching either.
18793
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18794
+ * `url` because a client that had to build the plane path itself is a second
18795
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18753
18796
  */
18754
18797
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18755
18798
  /**
@@ -19096,6 +19139,50 @@ var EventStoreFootprintSchema = object({
19096
19139
  totalBytes: number().int(),
19097
19140
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19098
19141
  });
19142
+ /** Event-media footprint for one {@link MediaFileKind}. */
19143
+ var EventMediaKindFootprintSchema = object({
19144
+ kind: MediaFileKindEnum,
19145
+ /** Media rows of this kind. */
19146
+ rows: number().int(),
19147
+ /** Bytes on disk held by those rows. */
19148
+ bytes: number().int()
19149
+ });
19150
+ /**
19151
+ * The media footprint broken down by KIND — the axis a deletion decision
19152
+ * actually turns on.
19153
+ *
19154
+ * A byte total says how much there is; it cannot say what is safe to remove.
19155
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19156
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19157
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19158
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19159
+ * nothing else, so sizing a deletion means summing per kind.
19160
+ *
19161
+ * ## Why `unaccounted*` exists
19162
+ *
19163
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19164
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19165
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19166
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19167
+ * retired code path, or by a version that knew a kind this one does not) would
19168
+ * otherwise vanish from the total silently, and an operator would delete
19169
+ * against a denominator smaller than the disk.
19170
+ *
19171
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19172
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19173
+ */
19174
+ var EventMediaKindBreakdownSchema = object({
19175
+ /** Every media row in scope, from one unfiltered aggregate. */
19176
+ totalRows: number().int(),
19177
+ /** Every media byte in scope, from that same aggregate. */
19178
+ totalBytes: number().int(),
19179
+ /** Per-kind footprint, ordered by bytes descending. */
19180
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19181
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19182
+ unaccountedRows: number().int(),
19183
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19184
+ unaccountedBytes: number().int()
19185
+ });
19099
19186
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19100
19187
  var EventPruneCountsSchema = object({
19101
19188
  motion: number().int(),
@@ -19299,6 +19386,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19299
19386
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19300
19387
  kind: "query",
19301
19388
  auth: "admin"
19389
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19390
+ kind: "query",
19391
+ auth: "admin"
19302
19392
  }), method(object({
19303
19393
  olderThanMs: number(),
19304
19394
  reason: OpsLogReasonSchema.optional()
@@ -19438,6 +19528,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19438
19528
  }), array(MediaFileSchema).readonly()), method(object({
19439
19529
  trackId: string(),
19440
19530
  deviceId: number()
19531
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19532
+ eventId: string(),
19533
+ deviceId: number()
19441
19534
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19442
19535
  kind: "mutation",
19443
19536
  auth: "admin"
@@ -21351,6 +21444,20 @@ method(object({
21351
21444
  error: string().optional()
21352
21445
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21353
21446
  providerId: string(),
21447
+ /**
21448
+ * The location this config is an UNSAVED edit of, when there is one.
21449
+ *
21450
+ * `listLocations` replaces every declared secret with the redaction
21451
+ * sentinel, so the edit modal's form state holds the sentinel for any
21452
+ * credential the operator did not retype — and posting that here
21453
+ * without a way to resolve it makes the provider try to authenticate
21454
+ * as `__camstack_redacted__` and report the operator's own working
21455
+ * password as wrong. Given this id, the orchestrator restores each
21456
+ * sentinel from the stored config (same rule as `upsertLocation`)
21457
+ * before dispatching. Omitted by the "Add location" wizard, where
21458
+ * every value was typed just now and nothing is stored yet.
21459
+ */
21460
+ locationId: string().optional(),
21354
21461
  config: record(string(), unknown())
21355
21462
  }), object({
21356
21463
  ok: boolean(),
@@ -24576,10 +24683,24 @@ var FaceClusterSchema = object({
24576
24683
  size: number().int(),
24577
24684
  cohesion: number()
24578
24685
  });
24686
+ /**
24687
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24688
+ * are — never the bytes.
24689
+ *
24690
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24691
+ * track/event contract) is still populated because a deployed viewer requires
24692
+ * the field to parse a row at all; this method has no such reader. Its ONE
24693
+ * caller is the admin UI's detail modal, which was building
24694
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24695
+ * dialog already rendering its key FRAME from the `event-media` plane.
24696
+ *
24697
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24698
+ * media key directly, so this needed no new plane and no new access decision.
24699
+ */
24579
24700
  var MediaFileLiteSchema$1 = object({
24580
24701
  key: string(),
24581
24702
  kind: string(),
24582
- base64: string(),
24703
+ url: string(),
24583
24704
  sizeBytes: number(),
24584
24705
  timestamp: number()
24585
24706
  });
@@ -27601,10 +27722,24 @@ var PlateInfoSchema = object({
27601
27722
  */
27602
27723
  cropUrl: string().optional()
27603
27724
  });
27725
+ /**
27726
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27727
+ * are — never the bytes.
27728
+ *
27729
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27730
+ * track/event contract) is still populated because a deployed viewer requires
27731
+ * the field to parse a row at all; this method has no such reader. Its ONE
27732
+ * caller is the admin UI's detail modal, which was building
27733
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27734
+ * dialog already rendering its key FRAME from the `event-media` plane.
27735
+ *
27736
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27737
+ * media key directly, so this needed no new plane and no new access decision.
27738
+ */
27604
27739
  var MediaFileLiteSchema = object({
27605
27740
  key: string(),
27606
27741
  kind: string(),
27607
- base64: string(),
27742
+ url: string(),
27608
27743
  sizeBytes: number(),
27609
27744
  timestamp: number()
27610
27745
  });
@@ -35403,6 +35538,12 @@ Object.freeze({
35403
35538
  addonId: null,
35404
35539
  access: "view"
35405
35540
  },
35541
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
35542
+ capName: "pipeline-analytics",
35543
+ capScope: "device",
35544
+ addonId: null,
35545
+ access: "view"
35546
+ },
35406
35547
  "pipelineAnalytics.getEventStoreFootprint": {
35407
35548
  capName: "pipeline-analytics",
35408
35549
  capScope: "device",
@@ -35499,6 +35640,12 @@ Object.freeze({
35499
35640
  addonId: null,
35500
35641
  access: "view"
35501
35642
  },
35643
+ "pipelineAnalytics.listEventMedia": {
35644
+ capName: "pipeline-analytics",
35645
+ capScope: "device",
35646
+ addonId: null,
35647
+ access: "view"
35648
+ },
35502
35649
  "pipelineAnalytics.listGroups": {
35503
35650
  capName: "pipeline-analytics",
35504
35651
  capScope: "device",
@@ -39062,6 +39209,11 @@ Object.freeze({
39062
39209
  form: "single",
39063
39210
  optional: false
39064
39211
  }],
39212
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
39213
+ name: "deviceId",
39214
+ form: "single",
39215
+ optional: true
39216
+ }],
39065
39217
  "pipelineAnalytics.getGroup": [{
39066
39218
  name: "deviceId",
39067
39219
  form: "single",
@@ -39122,6 +39274,11 @@ Object.freeze({
39122
39274
  form: "array",
39123
39275
  optional: false
39124
39276
  }],
39277
+ "pipelineAnalytics.listEventMedia": [{
39278
+ name: "deviceId",
39279
+ form: "single",
39280
+ optional: false
39281
+ }],
39125
39282
  "pipelineAnalytics.listGroups": [{
39126
39283
  name: "deviceIds",
39127
39284
  form: "array",
package/dist/addon.mjs CHANGED
@@ -13205,6 +13205,114 @@ method(object({
13205
13205
  height: number()
13206
13206
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13207
13207
  /**
13208
+ * `failure-contribution` — the capability an addon reports its OWN losses
13209
+ * through, per camera, with the denominator attached. It stores nothing.
13210
+ *
13211
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13212
+ *
13213
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13214
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13215
+ * copied: the contributor reports what it already knows, hub-main adds only
13216
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13217
+ * somebody to forget to edit.
13218
+ *
13219
+ * They are not merged, because their invariants are opposites:
13220
+ *
13221
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13222
+ * claim a camera cost nothing, which is a measurement nobody made;
13223
+ * - a `failure-contribution` zero is the **most valuable value on the
13224
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13225
+ * and it is exactly what an absent entry cannot say.
13226
+ *
13227
+ * Putting a loss counter on a cost entry would also break the reconciliation
13228
+ * that gives `load-contribution` its point: contributions are subtracted from
13229
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13230
+ * has no process.
13231
+ *
13232
+ * ## Why not a log line, since the counters already exist
13233
+ *
13234
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13235
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13236
+ * ends in a log line, and a log line is the thing the operator asked to stop
13237
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13238
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13239
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13240
+ * media blackout were both diagnosed. The counters stay; this is where they can
13241
+ * be READ.
13242
+ *
13243
+ * ## The rate is served with its denominator or not at all
13244
+ *
13245
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13246
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13247
+ * than yesterday" and was **flat across twelve hours** once divided by the
13248
+ * successes on the same path. A surface that publishes only the numerator
13249
+ * reproduces that mistake on every read.
13250
+ *
13251
+ * ## Shape
13252
+ *
13253
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13254
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13255
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13256
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13257
+ * a forked runner's entries reach hub-main over transport that already exists.
13258
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13259
+ * result through `system.getFailureContributions`.
13260
+ */
13261
+ var FailureReasonCountSchema = object({
13262
+ /**
13263
+ * Why the attempt did not land, in the contributor's own vocabulary —
13264
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13265
+ * strings that already appear in this repo's logs and, where one exists, the
13266
+ * same string the per-track `previewMissReason` records (D276): a second
13267
+ * vocabulary for the same loss would make the row and the counter
13268
+ * un-joinable.
13269
+ */
13270
+ reason: string(),
13271
+ count: number().int().nonnegative()
13272
+ });
13273
+ var FailureContributionSchema = object({
13274
+ /**
13275
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13276
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13277
+ * `unit` free: the families are owned by different addons and a shared enum
13278
+ * is a central list that rots invisibly.
13279
+ */
13280
+ family: string(),
13281
+ /**
13282
+ * The NUMERIC device id — the same value every log line carries as
13283
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13284
+ * cannot name the camera must not emit the entry, because a fleet total
13285
+ * cannot answer the only question anybody asks of this surface.
13286
+ */
13287
+ deviceId: number().int().positive(),
13288
+ /**
13289
+ * A second dimension inside the family: the model / step id for an inference
13290
+ * timeout, so "which camera AND which model" is one read. Absent when the
13291
+ * family has a single variant.
13292
+ */
13293
+ variant: string().optional(),
13294
+ /**
13295
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13296
+ * differencing two reads must drop the interval when it changes, because the
13297
+ * counter restarted from zero in a respawned runner. Same discipline as
13298
+ * `LoadContribution.startedAtMs`.
13299
+ */
13300
+ sinceMs: number(),
13301
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13302
+ atMs: number(),
13303
+ /**
13304
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13305
+ * window. A failure count published without it is the mistake this schema
13306
+ * exists to make impossible.
13307
+ */
13308
+ attempts: number().int().nonnegative(),
13309
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13310
+ succeeded: number().int().nonnegative(),
13311
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13312
+ reasons: array(FailureReasonCountSchema).readonly()
13313
+ });
13314
+ method(_void(), array(FailureContributionSchema).readonly());
13315
+ /**
13208
13316
  * filesystem-browse — per-node capability for browsing the node's local
13209
13317
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13210
13318
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13726,6 +13834,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13726
13834
  kind: "mutation",
13727
13835
  auth: "admin"
13728
13836
  });
13837
+ var LoadContributionSchema = object({
13838
+ role: _enum([
13839
+ "decode",
13840
+ "transcode",
13841
+ "recording",
13842
+ "streaming",
13843
+ "detection"
13844
+ ]),
13845
+ /**
13846
+ * The NUMERIC device id — the same value every log line carries as
13847
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13848
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13849
+ * contributor that cannot name its camera must not emit the entry at all,
13850
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13851
+ * and would quietly turn one camera's cost into everybody's.
13852
+ */
13853
+ deviceId: number().int().positive().nullable(),
13854
+ attribution: _enum([
13855
+ "measured",
13856
+ "accounted",
13857
+ "unattributable"
13858
+ ]),
13859
+ /**
13860
+ * What ONE entry is, in the contributor's own words — `615/high`,
13861
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13862
+ * family and inventing a common one would lose the only information that
13863
+ * makes two entries for the same camera distinguishable.
13864
+ */
13865
+ unit: string(),
13866
+ /**
13867
+ * The OS process this cost lives in, when there is one. Present so a
13868
+ * consumer can (a) tell two generations of the same unit apart across a
13869
+ * restart, and (b) subtract claimed processes from the node's process
13870
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13871
+ * process of its own.
13872
+ */
13873
+ pid: number().int().positive().optional(),
13874
+ /**
13875
+ * When this generation started. The pid's incarnation marker: a consumer
13876
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13877
+ * window when this changes, because the counter restarted from zero in a new
13878
+ * process.
13879
+ */
13880
+ startedAtMs: number().optional(),
13881
+ /**
13882
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13883
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13884
+ * contribution is asked for.
13885
+ *
13886
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13887
+ * needs a sampler, and a new per-node sampler is the defect half of
13888
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13889
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13890
+ *
13891
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13892
+ * an entry with no process.
13893
+ */
13894
+ cpuSeconds: number().optional(),
13895
+ /** Resident bytes of this unit's process, same source and same rules. */
13896
+ rssBytes: number().optional()
13897
+ });
13898
+ method(_void(), array(LoadContributionSchema).readonly());
13729
13899
  /**
13730
13900
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13731
13901
  * through. It stores nothing.
@@ -13802,176 +13972,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13802
13972
  tags: record(string(), string()).optional()
13803
13973
  }), array(LogEntrySchema).readonly());
13804
13974
  /**
13805
- * `failure-contribution` — the capability an addon reports its OWN losses
13806
- * through, per camera, with the denominator attached. It stores nothing.
13807
- *
13808
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13809
- *
13810
- * `load-contribution` answers *what did this camera COST*. This answers *what
13811
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13812
- * copied: the contributor reports what it already knows, hub-main adds only
13813
- * `addonId`, nothing needs global knowledge, and there is no central list for
13814
- * somebody to forget to edit.
13815
- *
13816
- * They are not merged, because their invariants are opposites:
13817
- *
13818
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13819
- * claim a camera cost nothing, which is a measurement nobody made;
13820
- * - a `failure-contribution` zero is the **most valuable value on the
13821
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13822
- * and it is exactly what an absent entry cannot say.
13823
- *
13824
- * Putting a loss counter on a cost entry would also break the reconciliation
13825
- * that gives `load-contribution` its point: contributions are subtracted from
13826
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13827
- * has no process.
13828
- *
13829
- * ## Why not a log line, since the counters already exist
13830
- *
13831
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13832
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13833
- * ends in a log line, and a log line is the thing the operator asked to stop
13834
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13835
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13836
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13837
- * media blackout were both diagnosed. The counters stay; this is where they can
13838
- * be READ.
13839
- *
13840
- * ## The rate is served with its denominator or not at all
13841
- *
13842
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13843
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13844
- * than yesterday" and was **flat across twelve hours** once divided by the
13845
- * successes on the same path. A surface that publishes only the numerator
13846
- * reproduces that mistake on every read.
13847
- *
13848
- * ## Shape
13849
- *
13850
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13851
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13852
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13853
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13854
- * a forked runner's entries reach hub-main over transport that already exists.
13855
- * No new UDS message, no second registry (D3). The operator reads the assembled
13856
- * result through `system.getFailureContributions`.
13857
- */
13858
- var FailureReasonCountSchema = object({
13859
- /**
13860
- * Why the attempt did not land, in the contributor's own vocabulary —
13861
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13862
- * strings that already appear in this repo's logs and, where one exists, the
13863
- * same string the per-track `previewMissReason` records (D276): a second
13864
- * vocabulary for the same loss would make the row and the counter
13865
- * un-joinable.
13866
- */
13867
- reason: string(),
13868
- count: number().int().nonnegative()
13869
- });
13870
- var FailureContributionSchema = object({
13871
- /**
13872
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13873
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13874
- * `unit` free: the families are owned by different addons and a shared enum
13875
- * is a central list that rots invisibly.
13876
- */
13877
- family: string(),
13878
- /**
13879
- * The NUMERIC device id — the same value every log line carries as
13880
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13881
- * cannot name the camera must not emit the entry, because a fleet total
13882
- * cannot answer the only question anybody asks of this surface.
13883
- */
13884
- deviceId: number().int().positive(),
13885
- /**
13886
- * A second dimension inside the family: the model / step id for an inference
13887
- * timeout, so "which camera AND which model" is one read. Absent when the
13888
- * family has a single variant.
13889
- */
13890
- variant: string().optional(),
13891
- /**
13892
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13893
- * differencing two reads must drop the interval when it changes, because the
13894
- * counter restarted from zero in a respawned runner. Same discipline as
13895
- * `LoadContribution.startedAtMs`.
13896
- */
13897
- sinceMs: number(),
13898
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13899
- atMs: number(),
13900
- /**
13901
- * THE DENOMINATOR — every attempt on this path for this camera in the
13902
- * window. A failure count published without it is the mistake this schema
13903
- * exists to make impossible.
13904
- */
13905
- attempts: number().int().nonnegative(),
13906
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13907
- succeeded: number().int().nonnegative(),
13908
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13909
- reasons: array(FailureReasonCountSchema).readonly()
13910
- });
13911
- method(_void(), array(FailureContributionSchema).readonly());
13912
- var LoadContributionSchema = object({
13913
- role: _enum([
13914
- "decode",
13915
- "transcode",
13916
- "recording",
13917
- "streaming",
13918
- "detection"
13919
- ]),
13920
- /**
13921
- * The NUMERIC device id — the same value every log line carries as
13922
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13923
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13924
- * contributor that cannot name its camera must not emit the entry at all,
13925
- * because an unnamed per-camera entry is indistinguishable from a shared one
13926
- * and would quietly turn one camera's cost into everybody's.
13927
- */
13928
- deviceId: number().int().positive().nullable(),
13929
- attribution: _enum([
13930
- "measured",
13931
- "accounted",
13932
- "unattributable"
13933
- ]),
13934
- /**
13935
- * What ONE entry is, in the contributor's own words — `615/high`,
13936
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13937
- * family and inventing a common one would lose the only information that
13938
- * makes two entries for the same camera distinguishable.
13939
- */
13940
- unit: string(),
13941
- /**
13942
- * The OS process this cost lives in, when there is one. Present so a
13943
- * consumer can (a) tell two generations of the same unit apart across a
13944
- * restart, and (b) subtract claimed processes from the node's process
13945
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13946
- * process of its own.
13947
- */
13948
- pid: number().int().positive().optional(),
13949
- /**
13950
- * When this generation started. The pid's incarnation marker: a consumer
13951
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13952
- * window when this changes, because the counter restarted from zero in a new
13953
- * process.
13954
- */
13955
- startedAtMs: number().optional(),
13956
- /**
13957
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13958
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13959
- * contribution is asked for.
13960
- *
13961
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13962
- * needs a sampler, and a new per-node sampler is the defect half of
13963
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13964
- * by whoever already keeps a history; a rate cannot be un-averaged.
13965
- *
13966
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13967
- * an entry with no process.
13968
- */
13969
- cpuSeconds: number().optional(),
13970
- /** Resident bytes of this unit's process, same source and same rules. */
13971
- rssBytes: number().optional()
13972
- });
13973
- method(_void(), array(LoadContributionSchema).readonly());
13974
- /**
13975
13975
  * `login-method` — collection cap through which auth addons contribute
13976
13976
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13977
13977
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18709,12 +18709,53 @@ var MediaFileKindEnum = _enum([
18709
18709
  "keyFrameSmall",
18710
18710
  "thumbnailSmall"
18711
18711
  ]);
18712
+ /**
18713
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18714
+ * ARE — never the bytes themselves.
18715
+ *
18716
+ * ## Why `url` and not `base64`
18717
+ *
18718
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18719
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18720
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18721
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18722
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18723
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18724
+ *
18725
+ * `url` points at the `event-media` data plane
18726
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18727
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18728
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18729
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18730
+ * no less protected than they were inside a `view`-level cap response — see
18731
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18732
+ * (per-device scoping).
18733
+ *
18734
+ * The URL is built from the row's **stored** key, which is not always its
18735
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18736
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18737
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18738
+ *
18739
+ * ## `base64` is TRANSITIONAL and is going away
18740
+ *
18741
+ * It is still populated for one reason: the deployed viewer's track-detail
18742
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18743
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18744
+ * triangle — not as absence. Removing the field before that viewer ships is an
18745
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18746
+ * delete this line and the `withBytes` pass-through in
18747
+ * `analytics-query-facade.ts`; nothing else reads it.
18748
+ */
18712
18749
  var MediaFileSchema = object({
18713
18750
  key: string(),
18714
18751
  kind: MediaFileKindEnum,
18715
- base64: string(),
18716
18752
  sizeBytes: number(),
18717
18753
  timestamp: number()
18754
+ }).extend({
18755
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18756
+ url: string(),
18757
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18758
+ base64: string()
18718
18759
  });
18719
18760
  /**
18720
18761
  * One media row WITHOUT its bytes.
@@ -18726,7 +18767,9 @@ var MediaFileSchema = object({
18726
18767
  * blocks the whole view.
18727
18768
  *
18728
18769
  * `sizeBytes` is carried because it is what lets a client decide between the
18729
- * stored blob and a `?variant=thumb` rendering without fetching either.
18770
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18771
+ * `url` because a client that had to build the plane path itself is a second
18772
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18730
18773
  */
18731
18774
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18732
18775
  /**
@@ -19073,6 +19116,50 @@ var EventStoreFootprintSchema = object({
19073
19116
  totalBytes: number().int(),
19074
19117
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19075
19118
  });
19119
+ /** Event-media footprint for one {@link MediaFileKind}. */
19120
+ var EventMediaKindFootprintSchema = object({
19121
+ kind: MediaFileKindEnum,
19122
+ /** Media rows of this kind. */
19123
+ rows: number().int(),
19124
+ /** Bytes on disk held by those rows. */
19125
+ bytes: number().int()
19126
+ });
19127
+ /**
19128
+ * The media footprint broken down by KIND — the axis a deletion decision
19129
+ * actually turns on.
19130
+ *
19131
+ * A byte total says how much there is; it cannot say what is safe to remove.
19132
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19133
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19134
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19135
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19136
+ * nothing else, so sizing a deletion means summing per kind.
19137
+ *
19138
+ * ## Why `unaccounted*` exists
19139
+ *
19140
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19141
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19142
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19143
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19144
+ * retired code path, or by a version that knew a kind this one does not) would
19145
+ * otherwise vanish from the total silently, and an operator would delete
19146
+ * against a denominator smaller than the disk.
19147
+ *
19148
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19149
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19150
+ */
19151
+ var EventMediaKindBreakdownSchema = object({
19152
+ /** Every media row in scope, from one unfiltered aggregate. */
19153
+ totalRows: number().int(),
19154
+ /** Every media byte in scope, from that same aggregate. */
19155
+ totalBytes: number().int(),
19156
+ /** Per-kind footprint, ordered by bytes descending. */
19157
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19158
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19159
+ unaccountedRows: number().int(),
19160
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19161
+ unaccountedBytes: number().int()
19162
+ });
19076
19163
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19077
19164
  var EventPruneCountsSchema = object({
19078
19165
  motion: number().int(),
@@ -19276,6 +19363,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19276
19363
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19277
19364
  kind: "query",
19278
19365
  auth: "admin"
19366
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19367
+ kind: "query",
19368
+ auth: "admin"
19279
19369
  }), method(object({
19280
19370
  olderThanMs: number(),
19281
19371
  reason: OpsLogReasonSchema.optional()
@@ -19415,6 +19505,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19415
19505
  }), array(MediaFileSchema).readonly()), method(object({
19416
19506
  trackId: string(),
19417
19507
  deviceId: number()
19508
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19509
+ eventId: string(),
19510
+ deviceId: number()
19418
19511
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19419
19512
  kind: "mutation",
19420
19513
  auth: "admin"
@@ -21328,6 +21421,20 @@ method(object({
21328
21421
  error: string().optional()
21329
21422
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21330
21423
  providerId: string(),
21424
+ /**
21425
+ * The location this config is an UNSAVED edit of, when there is one.
21426
+ *
21427
+ * `listLocations` replaces every declared secret with the redaction
21428
+ * sentinel, so the edit modal's form state holds the sentinel for any
21429
+ * credential the operator did not retype — and posting that here
21430
+ * without a way to resolve it makes the provider try to authenticate
21431
+ * as `__camstack_redacted__` and report the operator's own working
21432
+ * password as wrong. Given this id, the orchestrator restores each
21433
+ * sentinel from the stored config (same rule as `upsertLocation`)
21434
+ * before dispatching. Omitted by the "Add location" wizard, where
21435
+ * every value was typed just now and nothing is stored yet.
21436
+ */
21437
+ locationId: string().optional(),
21331
21438
  config: record(string(), unknown())
21332
21439
  }), object({
21333
21440
  ok: boolean(),
@@ -24553,10 +24660,24 @@ var FaceClusterSchema = object({
24553
24660
  size: number().int(),
24554
24661
  cohesion: number()
24555
24662
  });
24663
+ /**
24664
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24665
+ * are — never the bytes.
24666
+ *
24667
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24668
+ * track/event contract) is still populated because a deployed viewer requires
24669
+ * the field to parse a row at all; this method has no such reader. Its ONE
24670
+ * caller is the admin UI's detail modal, which was building
24671
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24672
+ * dialog already rendering its key FRAME from the `event-media` plane.
24673
+ *
24674
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24675
+ * media key directly, so this needed no new plane and no new access decision.
24676
+ */
24556
24677
  var MediaFileLiteSchema$1 = object({
24557
24678
  key: string(),
24558
24679
  kind: string(),
24559
- base64: string(),
24680
+ url: string(),
24560
24681
  sizeBytes: number(),
24561
24682
  timestamp: number()
24562
24683
  });
@@ -27578,10 +27699,24 @@ var PlateInfoSchema = object({
27578
27699
  */
27579
27700
  cropUrl: string().optional()
27580
27701
  });
27702
+ /**
27703
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27704
+ * are — never the bytes.
27705
+ *
27706
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27707
+ * track/event contract) is still populated because a deployed viewer requires
27708
+ * the field to parse a row at all; this method has no such reader. Its ONE
27709
+ * caller is the admin UI's detail modal, which was building
27710
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27711
+ * dialog already rendering its key FRAME from the `event-media` plane.
27712
+ *
27713
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27714
+ * media key directly, so this needed no new plane and no new access decision.
27715
+ */
27581
27716
  var MediaFileLiteSchema = object({
27582
27717
  key: string(),
27583
27718
  kind: string(),
27584
- base64: string(),
27719
+ url: string(),
27585
27720
  sizeBytes: number(),
27586
27721
  timestamp: number()
27587
27722
  });
@@ -35380,6 +35515,12 @@ Object.freeze({
35380
35515
  addonId: null,
35381
35516
  access: "view"
35382
35517
  },
35518
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
35519
+ capName: "pipeline-analytics",
35520
+ capScope: "device",
35521
+ addonId: null,
35522
+ access: "view"
35523
+ },
35383
35524
  "pipelineAnalytics.getEventStoreFootprint": {
35384
35525
  capName: "pipeline-analytics",
35385
35526
  capScope: "device",
@@ -35476,6 +35617,12 @@ Object.freeze({
35476
35617
  addonId: null,
35477
35618
  access: "view"
35478
35619
  },
35620
+ "pipelineAnalytics.listEventMedia": {
35621
+ capName: "pipeline-analytics",
35622
+ capScope: "device",
35623
+ addonId: null,
35624
+ access: "view"
35625
+ },
35479
35626
  "pipelineAnalytics.listGroups": {
35480
35627
  capName: "pipeline-analytics",
35481
35628
  capScope: "device",
@@ -39039,6 +39186,11 @@ Object.freeze({
39039
39186
  form: "single",
39040
39187
  optional: false
39041
39188
  }],
39189
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
39190
+ name: "deviceId",
39191
+ form: "single",
39192
+ optional: true
39193
+ }],
39042
39194
  "pipelineAnalytics.getGroup": [{
39043
39195
  name: "deviceId",
39044
39196
  form: "single",
@@ -39099,6 +39251,11 @@ Object.freeze({
39099
39251
  form: "array",
39100
39252
  optional: false
39101
39253
  }],
39254
+ "pipelineAnalytics.listEventMedia": [{
39255
+ name: "deviceId",
39256
+ form: "single",
39257
+ optional: false
39258
+ }],
39102
39259
  "pipelineAnalytics.listGroups": [{
39103
39260
  name: "deviceIds",
39104
39261
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-terminal",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
4
4
  "description": "Interactive terminal sessions (pty + xterm) as a CamStack addon",
5
5
  "keywords": [
6
6
  "camstack",