@camstack/addon-export-alexa 1.2.52 → 1.2.54

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.
@@ -13215,6 +13215,114 @@ method(object({
13215
13215
  height: number()
13216
13216
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13217
13217
  /**
13218
+ * `failure-contribution` — the capability an addon reports its OWN losses
13219
+ * through, per camera, with the denominator attached. It stores nothing.
13220
+ *
13221
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13222
+ *
13223
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13224
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13225
+ * copied: the contributor reports what it already knows, hub-main adds only
13226
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13227
+ * somebody to forget to edit.
13228
+ *
13229
+ * They are not merged, because their invariants are opposites:
13230
+ *
13231
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13232
+ * claim a camera cost nothing, which is a measurement nobody made;
13233
+ * - a `failure-contribution` zero is the **most valuable value on the
13234
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13235
+ * and it is exactly what an absent entry cannot say.
13236
+ *
13237
+ * Putting a loss counter on a cost entry would also break the reconciliation
13238
+ * that gives `load-contribution` its point: contributions are subtracted from
13239
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13240
+ * has no process.
13241
+ *
13242
+ * ## Why not a log line, since the counters already exist
13243
+ *
13244
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13245
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13246
+ * ends in a log line, and a log line is the thing the operator asked to stop
13247
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13248
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13249
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13250
+ * media blackout were both diagnosed. The counters stay; this is where they can
13251
+ * be READ.
13252
+ *
13253
+ * ## The rate is served with its denominator or not at all
13254
+ *
13255
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13256
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13257
+ * than yesterday" and was **flat across twelve hours** once divided by the
13258
+ * successes on the same path. A surface that publishes only the numerator
13259
+ * reproduces that mistake on every read.
13260
+ *
13261
+ * ## Shape
13262
+ *
13263
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13264
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13265
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13266
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13267
+ * a forked runner's entries reach hub-main over transport that already exists.
13268
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13269
+ * result through `system.getFailureContributions`.
13270
+ */
13271
+ var FailureReasonCountSchema = object({
13272
+ /**
13273
+ * Why the attempt did not land, in the contributor's own vocabulary —
13274
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13275
+ * strings that already appear in this repo's logs and, where one exists, the
13276
+ * same string the per-track `previewMissReason` records (D276): a second
13277
+ * vocabulary for the same loss would make the row and the counter
13278
+ * un-joinable.
13279
+ */
13280
+ reason: string(),
13281
+ count: number().int().nonnegative()
13282
+ });
13283
+ var FailureContributionSchema = object({
13284
+ /**
13285
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13286
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13287
+ * `unit` free: the families are owned by different addons and a shared enum
13288
+ * is a central list that rots invisibly.
13289
+ */
13290
+ family: string(),
13291
+ /**
13292
+ * The NUMERIC device id — the same value every log line carries as
13293
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13294
+ * cannot name the camera must not emit the entry, because a fleet total
13295
+ * cannot answer the only question anybody asks of this surface.
13296
+ */
13297
+ deviceId: number().int().positive(),
13298
+ /**
13299
+ * A second dimension inside the family: the model / step id for an inference
13300
+ * timeout, so "which camera AND which model" is one read. Absent when the
13301
+ * family has a single variant.
13302
+ */
13303
+ variant: string().optional(),
13304
+ /**
13305
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13306
+ * differencing two reads must drop the interval when it changes, because the
13307
+ * counter restarted from zero in a respawned runner. Same discipline as
13308
+ * `LoadContribution.startedAtMs`.
13309
+ */
13310
+ sinceMs: number(),
13311
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13312
+ atMs: number(),
13313
+ /**
13314
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13315
+ * window. A failure count published without it is the mistake this schema
13316
+ * exists to make impossible.
13317
+ */
13318
+ attempts: number().int().nonnegative(),
13319
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13320
+ succeeded: number().int().nonnegative(),
13321
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13322
+ reasons: array(FailureReasonCountSchema).readonly()
13323
+ });
13324
+ method(_void(), array(FailureContributionSchema).readonly());
13325
+ /**
13218
13326
  * filesystem-browse — per-node capability for browsing the node's local
13219
13327
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13220
13328
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13736,6 +13844,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13736
13844
  kind: "mutation",
13737
13845
  auth: "admin"
13738
13846
  });
13847
+ var LoadContributionSchema = object({
13848
+ role: _enum([
13849
+ "decode",
13850
+ "transcode",
13851
+ "recording",
13852
+ "streaming",
13853
+ "detection"
13854
+ ]),
13855
+ /**
13856
+ * The NUMERIC device id — the same value every log line carries as
13857
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13858
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13859
+ * contributor that cannot name its camera must not emit the entry at all,
13860
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13861
+ * and would quietly turn one camera's cost into everybody's.
13862
+ */
13863
+ deviceId: number().int().positive().nullable(),
13864
+ attribution: _enum([
13865
+ "measured",
13866
+ "accounted",
13867
+ "unattributable"
13868
+ ]),
13869
+ /**
13870
+ * What ONE entry is, in the contributor's own words — `615/high`,
13871
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13872
+ * family and inventing a common one would lose the only information that
13873
+ * makes two entries for the same camera distinguishable.
13874
+ */
13875
+ unit: string(),
13876
+ /**
13877
+ * The OS process this cost lives in, when there is one. Present so a
13878
+ * consumer can (a) tell two generations of the same unit apart across a
13879
+ * restart, and (b) subtract claimed processes from the node's process
13880
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13881
+ * process of its own.
13882
+ */
13883
+ pid: number().int().positive().optional(),
13884
+ /**
13885
+ * When this generation started. The pid's incarnation marker: a consumer
13886
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13887
+ * window when this changes, because the counter restarted from zero in a new
13888
+ * process.
13889
+ */
13890
+ startedAtMs: number().optional(),
13891
+ /**
13892
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13893
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13894
+ * contribution is asked for.
13895
+ *
13896
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13897
+ * needs a sampler, and a new per-node sampler is the defect half of
13898
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13899
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13900
+ *
13901
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13902
+ * an entry with no process.
13903
+ */
13904
+ cpuSeconds: number().optional(),
13905
+ /** Resident bytes of this unit's process, same source and same rules. */
13906
+ rssBytes: number().optional()
13907
+ });
13908
+ method(_void(), array(LoadContributionSchema).readonly());
13739
13909
  /**
13740
13910
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13741
13911
  * through. It stores nothing.
@@ -13812,176 +13982,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13812
13982
  tags: record(string(), string()).optional()
13813
13983
  }), array(LogEntrySchema).readonly());
13814
13984
  /**
13815
- * `failure-contribution` — the capability an addon reports its OWN losses
13816
- * through, per camera, with the denominator attached. It stores nothing.
13817
- *
13818
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13819
- *
13820
- * `load-contribution` answers *what did this camera COST*. This answers *what
13821
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13822
- * copied: the contributor reports what it already knows, hub-main adds only
13823
- * `addonId`, nothing needs global knowledge, and there is no central list for
13824
- * somebody to forget to edit.
13825
- *
13826
- * They are not merged, because their invariants are opposites:
13827
- *
13828
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13829
- * claim a camera cost nothing, which is a measurement nobody made;
13830
- * - a `failure-contribution` zero is the **most valuable value on the
13831
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13832
- * and it is exactly what an absent entry cannot say.
13833
- *
13834
- * Putting a loss counter on a cost entry would also break the reconciliation
13835
- * that gives `load-contribution` its point: contributions are subtracted from
13836
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13837
- * has no process.
13838
- *
13839
- * ## Why not a log line, since the counters already exist
13840
- *
13841
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13842
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13843
- * ends in a log line, and a log line is the thing the operator asked to stop
13844
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13845
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13846
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13847
- * media blackout were both diagnosed. The counters stay; this is where they can
13848
- * be READ.
13849
- *
13850
- * ## The rate is served with its denominator or not at all
13851
- *
13852
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13853
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13854
- * than yesterday" and was **flat across twelve hours** once divided by the
13855
- * successes on the same path. A surface that publishes only the numerator
13856
- * reproduces that mistake on every read.
13857
- *
13858
- * ## Shape
13859
- *
13860
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13861
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13862
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13863
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13864
- * a forked runner's entries reach hub-main over transport that already exists.
13865
- * No new UDS message, no second registry (D3). The operator reads the assembled
13866
- * result through `system.getFailureContributions`.
13867
- */
13868
- var FailureReasonCountSchema = object({
13869
- /**
13870
- * Why the attempt did not land, in the contributor's own vocabulary —
13871
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13872
- * strings that already appear in this repo's logs and, where one exists, the
13873
- * same string the per-track `previewMissReason` records (D276): a second
13874
- * vocabulary for the same loss would make the row and the counter
13875
- * un-joinable.
13876
- */
13877
- reason: string(),
13878
- count: number().int().nonnegative()
13879
- });
13880
- var FailureContributionSchema = object({
13881
- /**
13882
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13883
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13884
- * `unit` free: the families are owned by different addons and a shared enum
13885
- * is a central list that rots invisibly.
13886
- */
13887
- family: string(),
13888
- /**
13889
- * The NUMERIC device id — the same value every log line carries as
13890
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13891
- * cannot name the camera must not emit the entry, because a fleet total
13892
- * cannot answer the only question anybody asks of this surface.
13893
- */
13894
- deviceId: number().int().positive(),
13895
- /**
13896
- * A second dimension inside the family: the model / step id for an inference
13897
- * timeout, so "which camera AND which model" is one read. Absent when the
13898
- * family has a single variant.
13899
- */
13900
- variant: string().optional(),
13901
- /**
13902
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13903
- * differencing two reads must drop the interval when it changes, because the
13904
- * counter restarted from zero in a respawned runner. Same discipline as
13905
- * `LoadContribution.startedAtMs`.
13906
- */
13907
- sinceMs: number(),
13908
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13909
- atMs: number(),
13910
- /**
13911
- * THE DENOMINATOR — every attempt on this path for this camera in the
13912
- * window. A failure count published without it is the mistake this schema
13913
- * exists to make impossible.
13914
- */
13915
- attempts: number().int().nonnegative(),
13916
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13917
- succeeded: number().int().nonnegative(),
13918
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13919
- reasons: array(FailureReasonCountSchema).readonly()
13920
- });
13921
- method(_void(), array(FailureContributionSchema).readonly());
13922
- var LoadContributionSchema = object({
13923
- role: _enum([
13924
- "decode",
13925
- "transcode",
13926
- "recording",
13927
- "streaming",
13928
- "detection"
13929
- ]),
13930
- /**
13931
- * The NUMERIC device id — the same value every log line carries as
13932
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13933
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13934
- * contributor that cannot name its camera must not emit the entry at all,
13935
- * because an unnamed per-camera entry is indistinguishable from a shared one
13936
- * and would quietly turn one camera's cost into everybody's.
13937
- */
13938
- deviceId: number().int().positive().nullable(),
13939
- attribution: _enum([
13940
- "measured",
13941
- "accounted",
13942
- "unattributable"
13943
- ]),
13944
- /**
13945
- * What ONE entry is, in the contributor's own words — `615/high`,
13946
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13947
- * family and inventing a common one would lose the only information that
13948
- * makes two entries for the same camera distinguishable.
13949
- */
13950
- unit: string(),
13951
- /**
13952
- * The OS process this cost lives in, when there is one. Present so a
13953
- * consumer can (a) tell two generations of the same unit apart across a
13954
- * restart, and (b) subtract claimed processes from the node's process
13955
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13956
- * process of its own.
13957
- */
13958
- pid: number().int().positive().optional(),
13959
- /**
13960
- * When this generation started. The pid's incarnation marker: a consumer
13961
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13962
- * window when this changes, because the counter restarted from zero in a new
13963
- * process.
13964
- */
13965
- startedAtMs: number().optional(),
13966
- /**
13967
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13968
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13969
- * contribution is asked for.
13970
- *
13971
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13972
- * needs a sampler, and a new per-node sampler is the defect half of
13973
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13974
- * by whoever already keeps a history; a rate cannot be un-averaged.
13975
- *
13976
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13977
- * an entry with no process.
13978
- */
13979
- cpuSeconds: number().optional(),
13980
- /** Resident bytes of this unit's process, same source and same rules. */
13981
- rssBytes: number().optional()
13982
- });
13983
- method(_void(), array(LoadContributionSchema).readonly());
13984
- /**
13985
13985
  * `login-method` — collection cap through which auth addons contribute
13986
13986
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13987
13987
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18657,12 +18657,53 @@ var MediaFileKindEnum = _enum([
18657
18657
  "keyFrameSmall",
18658
18658
  "thumbnailSmall"
18659
18659
  ]);
18660
+ /**
18661
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18662
+ * ARE — never the bytes themselves.
18663
+ *
18664
+ * ## Why `url` and not `base64`
18665
+ *
18666
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18667
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18668
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18669
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18670
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18671
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18672
+ *
18673
+ * `url` points at the `event-media` data plane
18674
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18675
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18676
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18677
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18678
+ * no less protected than they were inside a `view`-level cap response — see
18679
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18680
+ * (per-device scoping).
18681
+ *
18682
+ * The URL is built from the row's **stored** key, which is not always its
18683
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18684
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18685
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18686
+ *
18687
+ * ## `base64` is TRANSITIONAL and is going away
18688
+ *
18689
+ * It is still populated for one reason: the deployed viewer's track-detail
18690
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18691
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18692
+ * triangle — not as absence. Removing the field before that viewer ships is an
18693
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18694
+ * delete this line and the `withBytes` pass-through in
18695
+ * `analytics-query-facade.ts`; nothing else reads it.
18696
+ */
18660
18697
  var MediaFileSchema = object({
18661
18698
  key: string(),
18662
18699
  kind: MediaFileKindEnum,
18663
- base64: string(),
18664
18700
  sizeBytes: number(),
18665
18701
  timestamp: number()
18702
+ }).extend({
18703
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18704
+ url: string(),
18705
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18706
+ base64: string()
18666
18707
  });
18667
18708
  /**
18668
18709
  * One media row WITHOUT its bytes.
@@ -18674,7 +18715,9 @@ var MediaFileSchema = object({
18674
18715
  * blocks the whole view.
18675
18716
  *
18676
18717
  * `sizeBytes` is carried because it is what lets a client decide between the
18677
- * stored blob and a `?variant=thumb` rendering without fetching either.
18718
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18719
+ * `url` because a client that had to build the plane path itself is a second
18720
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18678
18721
  */
18679
18722
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18680
18723
  /**
@@ -19021,6 +19064,50 @@ var EventStoreFootprintSchema = object({
19021
19064
  totalBytes: number().int(),
19022
19065
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19023
19066
  });
19067
+ /** Event-media footprint for one {@link MediaFileKind}. */
19068
+ var EventMediaKindFootprintSchema = object({
19069
+ kind: MediaFileKindEnum,
19070
+ /** Media rows of this kind. */
19071
+ rows: number().int(),
19072
+ /** Bytes on disk held by those rows. */
19073
+ bytes: number().int()
19074
+ });
19075
+ /**
19076
+ * The media footprint broken down by KIND — the axis a deletion decision
19077
+ * actually turns on.
19078
+ *
19079
+ * A byte total says how much there is; it cannot say what is safe to remove.
19080
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19081
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19082
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19083
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19084
+ * nothing else, so sizing a deletion means summing per kind.
19085
+ *
19086
+ * ## Why `unaccounted*` exists
19087
+ *
19088
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19089
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19090
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19091
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19092
+ * retired code path, or by a version that knew a kind this one does not) would
19093
+ * otherwise vanish from the total silently, and an operator would delete
19094
+ * against a denominator smaller than the disk.
19095
+ *
19096
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19097
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19098
+ */
19099
+ var EventMediaKindBreakdownSchema = object({
19100
+ /** Every media row in scope, from one unfiltered aggregate. */
19101
+ totalRows: number().int(),
19102
+ /** Every media byte in scope, from that same aggregate. */
19103
+ totalBytes: number().int(),
19104
+ /** Per-kind footprint, ordered by bytes descending. */
19105
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19106
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19107
+ unaccountedRows: number().int(),
19108
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19109
+ unaccountedBytes: number().int()
19110
+ });
19024
19111
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19025
19112
  var EventPruneCountsSchema = object({
19026
19113
  motion: number().int(),
@@ -19224,6 +19311,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19224
19311
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19225
19312
  kind: "query",
19226
19313
  auth: "admin"
19314
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19315
+ kind: "query",
19316
+ auth: "admin"
19227
19317
  }), method(object({
19228
19318
  olderThanMs: number(),
19229
19319
  reason: OpsLogReasonSchema.optional()
@@ -19363,6 +19453,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19363
19453
  }), array(MediaFileSchema).readonly()), method(object({
19364
19454
  trackId: string(),
19365
19455
  deviceId: number()
19456
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19457
+ eventId: string(),
19458
+ deviceId: number()
19366
19459
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19367
19460
  kind: "mutation",
19368
19461
  auth: "admin"
@@ -21172,6 +21265,20 @@ method(object({
21172
21265
  error: string().optional()
21173
21266
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21174
21267
  providerId: string(),
21268
+ /**
21269
+ * The location this config is an UNSAVED edit of, when there is one.
21270
+ *
21271
+ * `listLocations` replaces every declared secret with the redaction
21272
+ * sentinel, so the edit modal's form state holds the sentinel for any
21273
+ * credential the operator did not retype — and posting that here
21274
+ * without a way to resolve it makes the provider try to authenticate
21275
+ * as `__camstack_redacted__` and report the operator's own working
21276
+ * password as wrong. Given this id, the orchestrator restores each
21277
+ * sentinel from the stored config (same rule as `upsertLocation`)
21278
+ * before dispatching. Omitted by the "Add location" wizard, where
21279
+ * every value was typed just now and nothing is stored yet.
21280
+ */
21281
+ locationId: string().optional(),
21175
21282
  config: record(string(), unknown())
21176
21283
  }), object({
21177
21284
  ok: boolean(),
@@ -23623,10 +23730,24 @@ var FaceClusterSchema = object({
23623
23730
  size: number().int(),
23624
23731
  cohesion: number()
23625
23732
  });
23733
+ /**
23734
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23735
+ * are — never the bytes.
23736
+ *
23737
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23738
+ * track/event contract) is still populated because a deployed viewer requires
23739
+ * the field to parse a row at all; this method has no such reader. Its ONE
23740
+ * caller is the admin UI's detail modal, which was building
23741
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23742
+ * dialog already rendering its key FRAME from the `event-media` plane.
23743
+ *
23744
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23745
+ * media key directly, so this needed no new plane and no new access decision.
23746
+ */
23626
23747
  var MediaFileLiteSchema$1 = object({
23627
23748
  key: string(),
23628
23749
  kind: string(),
23629
- base64: string(),
23750
+ url: string(),
23630
23751
  sizeBytes: number(),
23631
23752
  timestamp: number()
23632
23753
  });
@@ -25878,10 +25999,24 @@ var PlateInfoSchema = object({
25878
25999
  */
25879
26000
  cropUrl: string().optional()
25880
26001
  });
26002
+ /**
26003
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
26004
+ * are — never the bytes.
26005
+ *
26006
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
26007
+ * track/event contract) is still populated because a deployed viewer requires
26008
+ * the field to parse a row at all; this method has no such reader. Its ONE
26009
+ * caller is the admin UI's detail modal, which was building
26010
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
26011
+ * dialog already rendering its key FRAME from the `event-media` plane.
26012
+ *
26013
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
26014
+ * media key directly, so this needed no new plane and no new access decision.
26015
+ */
25881
26016
  var MediaFileLiteSchema = object({
25882
26017
  key: string(),
25883
26018
  kind: string(),
25884
- base64: string(),
26019
+ url: string(),
25885
26020
  sizeBytes: number(),
25886
26021
  timestamp: number()
25887
26022
  });
@@ -31798,6 +31933,12 @@ Object.freeze({
31798
31933
  addonId: null,
31799
31934
  access: "view"
31800
31935
  },
31936
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
31937
+ capName: "pipeline-analytics",
31938
+ capScope: "device",
31939
+ addonId: null,
31940
+ access: "view"
31941
+ },
31801
31942
  "pipelineAnalytics.getEventStoreFootprint": {
31802
31943
  capName: "pipeline-analytics",
31803
31944
  capScope: "device",
@@ -31894,6 +32035,12 @@ Object.freeze({
31894
32035
  addonId: null,
31895
32036
  access: "view"
31896
32037
  },
32038
+ "pipelineAnalytics.listEventMedia": {
32039
+ capName: "pipeline-analytics",
32040
+ capScope: "device",
32041
+ addonId: null,
32042
+ access: "view"
32043
+ },
31897
32044
  "pipelineAnalytics.listGroups": {
31898
32045
  capName: "pipeline-analytics",
31899
32046
  capScope: "device",
@@ -35457,6 +35604,11 @@ Object.freeze({
35457
35604
  form: "single",
35458
35605
  optional: false
35459
35606
  }],
35607
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
35608
+ name: "deviceId",
35609
+ form: "single",
35610
+ optional: true
35611
+ }],
35460
35612
  "pipelineAnalytics.getGroup": [{
35461
35613
  name: "deviceId",
35462
35614
  form: "single",
@@ -35517,6 +35669,11 @@ Object.freeze({
35517
35669
  form: "array",
35518
35670
  optional: false
35519
35671
  }],
35672
+ "pipelineAnalytics.listEventMedia": [{
35673
+ name: "deviceId",
35674
+ form: "single",
35675
+ optional: false
35676
+ }],
35520
35677
  "pipelineAnalytics.listGroups": [{
35521
35678
  name: "deviceIds",
35522
35679
  form: "array",
@@ -13188,6 +13188,114 @@ method(object({
13188
13188
  height: number()
13189
13189
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13190
13190
  /**
13191
+ * `failure-contribution` — the capability an addon reports its OWN losses
13192
+ * through, per camera, with the denominator attached. It stores nothing.
13193
+ *
13194
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13195
+ *
13196
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13197
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13198
+ * copied: the contributor reports what it already knows, hub-main adds only
13199
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13200
+ * somebody to forget to edit.
13201
+ *
13202
+ * They are not merged, because their invariants are opposites:
13203
+ *
13204
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13205
+ * claim a camera cost nothing, which is a measurement nobody made;
13206
+ * - a `failure-contribution` zero is the **most valuable value on the
13207
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13208
+ * and it is exactly what an absent entry cannot say.
13209
+ *
13210
+ * Putting a loss counter on a cost entry would also break the reconciliation
13211
+ * that gives `load-contribution` its point: contributions are subtracted from
13212
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13213
+ * has no process.
13214
+ *
13215
+ * ## Why not a log line, since the counters already exist
13216
+ *
13217
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13218
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13219
+ * ends in a log line, and a log line is the thing the operator asked to stop
13220
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13221
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13222
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13223
+ * media blackout were both diagnosed. The counters stay; this is where they can
13224
+ * be READ.
13225
+ *
13226
+ * ## The rate is served with its denominator or not at all
13227
+ *
13228
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13229
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13230
+ * than yesterday" and was **flat across twelve hours** once divided by the
13231
+ * successes on the same path. A surface that publishes only the numerator
13232
+ * reproduces that mistake on every read.
13233
+ *
13234
+ * ## Shape
13235
+ *
13236
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13237
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13238
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13239
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13240
+ * a forked runner's entries reach hub-main over transport that already exists.
13241
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13242
+ * result through `system.getFailureContributions`.
13243
+ */
13244
+ var FailureReasonCountSchema = object({
13245
+ /**
13246
+ * Why the attempt did not land, in the contributor's own vocabulary —
13247
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13248
+ * strings that already appear in this repo's logs and, where one exists, the
13249
+ * same string the per-track `previewMissReason` records (D276): a second
13250
+ * vocabulary for the same loss would make the row and the counter
13251
+ * un-joinable.
13252
+ */
13253
+ reason: string(),
13254
+ count: number().int().nonnegative()
13255
+ });
13256
+ var FailureContributionSchema = object({
13257
+ /**
13258
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13259
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13260
+ * `unit` free: the families are owned by different addons and a shared enum
13261
+ * is a central list that rots invisibly.
13262
+ */
13263
+ family: string(),
13264
+ /**
13265
+ * The NUMERIC device id — the same value every log line carries as
13266
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13267
+ * cannot name the camera must not emit the entry, because a fleet total
13268
+ * cannot answer the only question anybody asks of this surface.
13269
+ */
13270
+ deviceId: number().int().positive(),
13271
+ /**
13272
+ * A second dimension inside the family: the model / step id for an inference
13273
+ * timeout, so "which camera AND which model" is one read. Absent when the
13274
+ * family has a single variant.
13275
+ */
13276
+ variant: string().optional(),
13277
+ /**
13278
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13279
+ * differencing two reads must drop the interval when it changes, because the
13280
+ * counter restarted from zero in a respawned runner. Same discipline as
13281
+ * `LoadContribution.startedAtMs`.
13282
+ */
13283
+ sinceMs: number(),
13284
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13285
+ atMs: number(),
13286
+ /**
13287
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13288
+ * window. A failure count published without it is the mistake this schema
13289
+ * exists to make impossible.
13290
+ */
13291
+ attempts: number().int().nonnegative(),
13292
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13293
+ succeeded: number().int().nonnegative(),
13294
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13295
+ reasons: array(FailureReasonCountSchema).readonly()
13296
+ });
13297
+ method(_void(), array(FailureContributionSchema).readonly());
13298
+ /**
13191
13299
  * filesystem-browse — per-node capability for browsing the node's local
13192
13300
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13193
13301
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13709,6 +13817,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13709
13817
  kind: "mutation",
13710
13818
  auth: "admin"
13711
13819
  });
13820
+ var LoadContributionSchema = object({
13821
+ role: _enum([
13822
+ "decode",
13823
+ "transcode",
13824
+ "recording",
13825
+ "streaming",
13826
+ "detection"
13827
+ ]),
13828
+ /**
13829
+ * The NUMERIC device id — the same value every log line carries as
13830
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13831
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13832
+ * contributor that cannot name its camera must not emit the entry at all,
13833
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13834
+ * and would quietly turn one camera's cost into everybody's.
13835
+ */
13836
+ deviceId: number().int().positive().nullable(),
13837
+ attribution: _enum([
13838
+ "measured",
13839
+ "accounted",
13840
+ "unattributable"
13841
+ ]),
13842
+ /**
13843
+ * What ONE entry is, in the contributor's own words — `615/high`,
13844
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13845
+ * family and inventing a common one would lose the only information that
13846
+ * makes two entries for the same camera distinguishable.
13847
+ */
13848
+ unit: string(),
13849
+ /**
13850
+ * The OS process this cost lives in, when there is one. Present so a
13851
+ * consumer can (a) tell two generations of the same unit apart across a
13852
+ * restart, and (b) subtract claimed processes from the node's process
13853
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13854
+ * process of its own.
13855
+ */
13856
+ pid: number().int().positive().optional(),
13857
+ /**
13858
+ * When this generation started. The pid's incarnation marker: a consumer
13859
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13860
+ * window when this changes, because the counter restarted from zero in a new
13861
+ * process.
13862
+ */
13863
+ startedAtMs: number().optional(),
13864
+ /**
13865
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13866
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13867
+ * contribution is asked for.
13868
+ *
13869
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13870
+ * needs a sampler, and a new per-node sampler is the defect half of
13871
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13872
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13873
+ *
13874
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13875
+ * an entry with no process.
13876
+ */
13877
+ cpuSeconds: number().optional(),
13878
+ /** Resident bytes of this unit's process, same source and same rules. */
13879
+ rssBytes: number().optional()
13880
+ });
13881
+ method(_void(), array(LoadContributionSchema).readonly());
13712
13882
  /**
13713
13883
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13714
13884
  * through. It stores nothing.
@@ -13785,176 +13955,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13785
13955
  tags: record(string(), string()).optional()
13786
13956
  }), array(LogEntrySchema).readonly());
13787
13957
  /**
13788
- * `failure-contribution` — the capability an addon reports its OWN losses
13789
- * through, per camera, with the denominator attached. It stores nothing.
13790
- *
13791
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13792
- *
13793
- * `load-contribution` answers *what did this camera COST*. This answers *what
13794
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13795
- * copied: the contributor reports what it already knows, hub-main adds only
13796
- * `addonId`, nothing needs global knowledge, and there is no central list for
13797
- * somebody to forget to edit.
13798
- *
13799
- * They are not merged, because their invariants are opposites:
13800
- *
13801
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13802
- * claim a camera cost nothing, which is a measurement nobody made;
13803
- * - a `failure-contribution` zero is the **most valuable value on the
13804
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13805
- * and it is exactly what an absent entry cannot say.
13806
- *
13807
- * Putting a loss counter on a cost entry would also break the reconciliation
13808
- * that gives `load-contribution` its point: contributions are subtracted from
13809
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13810
- * has no process.
13811
- *
13812
- * ## Why not a log line, since the counters already exist
13813
- *
13814
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13815
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13816
- * ends in a log line, and a log line is the thing the operator asked to stop
13817
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13818
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13819
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13820
- * media blackout were both diagnosed. The counters stay; this is where they can
13821
- * be READ.
13822
- *
13823
- * ## The rate is served with its denominator or not at all
13824
- *
13825
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13826
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13827
- * than yesterday" and was **flat across twelve hours** once divided by the
13828
- * successes on the same path. A surface that publishes only the numerator
13829
- * reproduces that mistake on every read.
13830
- *
13831
- * ## Shape
13832
- *
13833
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13834
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13835
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13836
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13837
- * a forked runner's entries reach hub-main over transport that already exists.
13838
- * No new UDS message, no second registry (D3). The operator reads the assembled
13839
- * result through `system.getFailureContributions`.
13840
- */
13841
- var FailureReasonCountSchema = object({
13842
- /**
13843
- * Why the attempt did not land, in the contributor's own vocabulary —
13844
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13845
- * strings that already appear in this repo's logs and, where one exists, the
13846
- * same string the per-track `previewMissReason` records (D276): a second
13847
- * vocabulary for the same loss would make the row and the counter
13848
- * un-joinable.
13849
- */
13850
- reason: string(),
13851
- count: number().int().nonnegative()
13852
- });
13853
- var FailureContributionSchema = object({
13854
- /**
13855
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13856
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13857
- * `unit` free: the families are owned by different addons and a shared enum
13858
- * is a central list that rots invisibly.
13859
- */
13860
- family: string(),
13861
- /**
13862
- * The NUMERIC device id — the same value every log line carries as
13863
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13864
- * cannot name the camera must not emit the entry, because a fleet total
13865
- * cannot answer the only question anybody asks of this surface.
13866
- */
13867
- deviceId: number().int().positive(),
13868
- /**
13869
- * A second dimension inside the family: the model / step id for an inference
13870
- * timeout, so "which camera AND which model" is one read. Absent when the
13871
- * family has a single variant.
13872
- */
13873
- variant: string().optional(),
13874
- /**
13875
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13876
- * differencing two reads must drop the interval when it changes, because the
13877
- * counter restarted from zero in a respawned runner. Same discipline as
13878
- * `LoadContribution.startedAtMs`.
13879
- */
13880
- sinceMs: number(),
13881
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13882
- atMs: number(),
13883
- /**
13884
- * THE DENOMINATOR — every attempt on this path for this camera in the
13885
- * window. A failure count published without it is the mistake this schema
13886
- * exists to make impossible.
13887
- */
13888
- attempts: number().int().nonnegative(),
13889
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13890
- succeeded: number().int().nonnegative(),
13891
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13892
- reasons: array(FailureReasonCountSchema).readonly()
13893
- });
13894
- method(_void(), array(FailureContributionSchema).readonly());
13895
- var LoadContributionSchema = object({
13896
- role: _enum([
13897
- "decode",
13898
- "transcode",
13899
- "recording",
13900
- "streaming",
13901
- "detection"
13902
- ]),
13903
- /**
13904
- * The NUMERIC device id — the same value every log line carries as
13905
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13906
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13907
- * contributor that cannot name its camera must not emit the entry at all,
13908
- * because an unnamed per-camera entry is indistinguishable from a shared one
13909
- * and would quietly turn one camera's cost into everybody's.
13910
- */
13911
- deviceId: number().int().positive().nullable(),
13912
- attribution: _enum([
13913
- "measured",
13914
- "accounted",
13915
- "unattributable"
13916
- ]),
13917
- /**
13918
- * What ONE entry is, in the contributor's own words — `615/high`,
13919
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13920
- * family and inventing a common one would lose the only information that
13921
- * makes two entries for the same camera distinguishable.
13922
- */
13923
- unit: string(),
13924
- /**
13925
- * The OS process this cost lives in, when there is one. Present so a
13926
- * consumer can (a) tell two generations of the same unit apart across a
13927
- * restart, and (b) subtract claimed processes from the node's process
13928
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13929
- * process of its own.
13930
- */
13931
- pid: number().int().positive().optional(),
13932
- /**
13933
- * When this generation started. The pid's incarnation marker: a consumer
13934
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13935
- * window when this changes, because the counter restarted from zero in a new
13936
- * process.
13937
- */
13938
- startedAtMs: number().optional(),
13939
- /**
13940
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13941
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13942
- * contribution is asked for.
13943
- *
13944
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13945
- * needs a sampler, and a new per-node sampler is the defect half of
13946
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13947
- * by whoever already keeps a history; a rate cannot be un-averaged.
13948
- *
13949
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13950
- * an entry with no process.
13951
- */
13952
- cpuSeconds: number().optional(),
13953
- /** Resident bytes of this unit's process, same source and same rules. */
13954
- rssBytes: number().optional()
13955
- });
13956
- method(_void(), array(LoadContributionSchema).readonly());
13957
- /**
13958
13958
  * `login-method` — collection cap through which auth addons contribute
13959
13959
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13960
13960
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18630,12 +18630,53 @@ var MediaFileKindEnum = _enum([
18630
18630
  "keyFrameSmall",
18631
18631
  "thumbnailSmall"
18632
18632
  ]);
18633
+ /**
18634
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18635
+ * ARE — never the bytes themselves.
18636
+ *
18637
+ * ## Why `url` and not `base64`
18638
+ *
18639
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18640
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18641
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18642
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18643
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18644
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18645
+ *
18646
+ * `url` points at the `event-media` data plane
18647
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18648
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18649
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18650
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18651
+ * no less protected than they were inside a `view`-level cap response — see
18652
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18653
+ * (per-device scoping).
18654
+ *
18655
+ * The URL is built from the row's **stored** key, which is not always its
18656
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18657
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18658
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18659
+ *
18660
+ * ## `base64` is TRANSITIONAL and is going away
18661
+ *
18662
+ * It is still populated for one reason: the deployed viewer's track-detail
18663
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18664
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18665
+ * triangle — not as absence. Removing the field before that viewer ships is an
18666
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18667
+ * delete this line and the `withBytes` pass-through in
18668
+ * `analytics-query-facade.ts`; nothing else reads it.
18669
+ */
18633
18670
  var MediaFileSchema = object({
18634
18671
  key: string(),
18635
18672
  kind: MediaFileKindEnum,
18636
- base64: string(),
18637
18673
  sizeBytes: number(),
18638
18674
  timestamp: number()
18675
+ }).extend({
18676
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18677
+ url: string(),
18678
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18679
+ base64: string()
18639
18680
  });
18640
18681
  /**
18641
18682
  * One media row WITHOUT its bytes.
@@ -18647,7 +18688,9 @@ var MediaFileSchema = object({
18647
18688
  * blocks the whole view.
18648
18689
  *
18649
18690
  * `sizeBytes` is carried because it is what lets a client decide between the
18650
- * stored blob and a `?variant=thumb` rendering without fetching either.
18691
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18692
+ * `url` because a client that had to build the plane path itself is a second
18693
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18651
18694
  */
18652
18695
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18653
18696
  /**
@@ -18994,6 +19037,50 @@ var EventStoreFootprintSchema = object({
18994
19037
  totalBytes: number().int(),
18995
19038
  devices: array(EventStoreDeviceFootprintSchema).readonly()
18996
19039
  });
19040
+ /** Event-media footprint for one {@link MediaFileKind}. */
19041
+ var EventMediaKindFootprintSchema = object({
19042
+ kind: MediaFileKindEnum,
19043
+ /** Media rows of this kind. */
19044
+ rows: number().int(),
19045
+ /** Bytes on disk held by those rows. */
19046
+ bytes: number().int()
19047
+ });
19048
+ /**
19049
+ * The media footprint broken down by KIND — the axis a deletion decision
19050
+ * actually turns on.
19051
+ *
19052
+ * A byte total says how much there is; it cannot say what is safe to remove.
19053
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19054
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19055
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19056
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19057
+ * nothing else, so sizing a deletion means summing per kind.
19058
+ *
19059
+ * ## Why `unaccounted*` exists
19060
+ *
19061
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19062
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19063
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19064
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19065
+ * retired code path, or by a version that knew a kind this one does not) would
19066
+ * otherwise vanish from the total silently, and an operator would delete
19067
+ * against a denominator smaller than the disk.
19068
+ *
19069
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19070
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19071
+ */
19072
+ var EventMediaKindBreakdownSchema = object({
19073
+ /** Every media row in scope, from one unfiltered aggregate. */
19074
+ totalRows: number().int(),
19075
+ /** Every media byte in scope, from that same aggregate. */
19076
+ totalBytes: number().int(),
19077
+ /** Per-kind footprint, ordered by bytes descending. */
19078
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19079
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19080
+ unaccountedRows: number().int(),
19081
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19082
+ unaccountedBytes: number().int()
19083
+ });
18997
19084
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
18998
19085
  var EventPruneCountsSchema = object({
18999
19086
  motion: number().int(),
@@ -19197,6 +19284,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19197
19284
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19198
19285
  kind: "query",
19199
19286
  auth: "admin"
19287
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19288
+ kind: "query",
19289
+ auth: "admin"
19200
19290
  }), method(object({
19201
19291
  olderThanMs: number(),
19202
19292
  reason: OpsLogReasonSchema.optional()
@@ -19336,6 +19426,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19336
19426
  }), array(MediaFileSchema).readonly()), method(object({
19337
19427
  trackId: string(),
19338
19428
  deviceId: number()
19429
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19430
+ eventId: string(),
19431
+ deviceId: number()
19339
19432
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19340
19433
  kind: "mutation",
19341
19434
  auth: "admin"
@@ -21145,6 +21238,20 @@ method(object({
21145
21238
  error: string().optional()
21146
21239
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21147
21240
  providerId: string(),
21241
+ /**
21242
+ * The location this config is an UNSAVED edit of, when there is one.
21243
+ *
21244
+ * `listLocations` replaces every declared secret with the redaction
21245
+ * sentinel, so the edit modal's form state holds the sentinel for any
21246
+ * credential the operator did not retype — and posting that here
21247
+ * without a way to resolve it makes the provider try to authenticate
21248
+ * as `__camstack_redacted__` and report the operator's own working
21249
+ * password as wrong. Given this id, the orchestrator restores each
21250
+ * sentinel from the stored config (same rule as `upsertLocation`)
21251
+ * before dispatching. Omitted by the "Add location" wizard, where
21252
+ * every value was typed just now and nothing is stored yet.
21253
+ */
21254
+ locationId: string().optional(),
21148
21255
  config: record(string(), unknown())
21149
21256
  }), object({
21150
21257
  ok: boolean(),
@@ -23596,10 +23703,24 @@ var FaceClusterSchema = object({
23596
23703
  size: number().int(),
23597
23704
  cohesion: number()
23598
23705
  });
23706
+ /**
23707
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23708
+ * are — never the bytes.
23709
+ *
23710
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23711
+ * track/event contract) is still populated because a deployed viewer requires
23712
+ * the field to parse a row at all; this method has no such reader. Its ONE
23713
+ * caller is the admin UI's detail modal, which was building
23714
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23715
+ * dialog already rendering its key FRAME from the `event-media` plane.
23716
+ *
23717
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23718
+ * media key directly, so this needed no new plane and no new access decision.
23719
+ */
23599
23720
  var MediaFileLiteSchema$1 = object({
23600
23721
  key: string(),
23601
23722
  kind: string(),
23602
- base64: string(),
23723
+ url: string(),
23603
23724
  sizeBytes: number(),
23604
23725
  timestamp: number()
23605
23726
  });
@@ -25851,10 +25972,24 @@ var PlateInfoSchema = object({
25851
25972
  */
25852
25973
  cropUrl: string().optional()
25853
25974
  });
25975
+ /**
25976
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25977
+ * are — never the bytes.
25978
+ *
25979
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25980
+ * track/event contract) is still populated because a deployed viewer requires
25981
+ * the field to parse a row at all; this method has no such reader. Its ONE
25982
+ * caller is the admin UI's detail modal, which was building
25983
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25984
+ * dialog already rendering its key FRAME from the `event-media` plane.
25985
+ *
25986
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25987
+ * media key directly, so this needed no new plane and no new access decision.
25988
+ */
25854
25989
  var MediaFileLiteSchema = object({
25855
25990
  key: string(),
25856
25991
  kind: string(),
25857
- base64: string(),
25992
+ url: string(),
25858
25993
  sizeBytes: number(),
25859
25994
  timestamp: number()
25860
25995
  });
@@ -31771,6 +31906,12 @@ Object.freeze({
31771
31906
  addonId: null,
31772
31907
  access: "view"
31773
31908
  },
31909
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
31910
+ capName: "pipeline-analytics",
31911
+ capScope: "device",
31912
+ addonId: null,
31913
+ access: "view"
31914
+ },
31774
31915
  "pipelineAnalytics.getEventStoreFootprint": {
31775
31916
  capName: "pipeline-analytics",
31776
31917
  capScope: "device",
@@ -31867,6 +32008,12 @@ Object.freeze({
31867
32008
  addonId: null,
31868
32009
  access: "view"
31869
32010
  },
32011
+ "pipelineAnalytics.listEventMedia": {
32012
+ capName: "pipeline-analytics",
32013
+ capScope: "device",
32014
+ addonId: null,
32015
+ access: "view"
32016
+ },
31870
32017
  "pipelineAnalytics.listGroups": {
31871
32018
  capName: "pipeline-analytics",
31872
32019
  capScope: "device",
@@ -35430,6 +35577,11 @@ Object.freeze({
35430
35577
  form: "single",
35431
35578
  optional: false
35432
35579
  }],
35580
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
35581
+ name: "deviceId",
35582
+ form: "single",
35583
+ optional: true
35584
+ }],
35433
35585
  "pipelineAnalytics.getGroup": [{
35434
35586
  name: "deviceId",
35435
35587
  form: "single",
@@ -35490,6 +35642,11 @@ Object.freeze({
35490
35642
  form: "array",
35491
35643
  optional: false
35492
35644
  }],
35645
+ "pipelineAnalytics.listEventMedia": [{
35646
+ name: "deviceId",
35647
+ form: "single",
35648
+ optional: false
35649
+ }],
35493
35650
  "pipelineAnalytics.listGroups": [{
35494
35651
  name: "deviceIds",
35495
35652
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-export-alexa",
3
- "version": "1.2.52",
3
+ "version": "1.2.54",
4
4
  "description": "Alexa Smart Home Skill export — hub-side OAuth provider + directive handler. The companion AWS Lambda forwards Alexa directives to this addon's /addon/export-alexa/directive endpoint.",
5
5
  "keywords": [
6
6
  "camstack",