@camstack/addon-provider-amcrest 0.2.47 → 0.2.49

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
@@ -13177,6 +13177,114 @@ method(object({
13177
13177
  height: number()
13178
13178
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13179
13179
  /**
13180
+ * `failure-contribution` — the capability an addon reports its OWN losses
13181
+ * through, per camera, with the denominator attached. It stores nothing.
13182
+ *
13183
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13184
+ *
13185
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13186
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13187
+ * copied: the contributor reports what it already knows, hub-main adds only
13188
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13189
+ * somebody to forget to edit.
13190
+ *
13191
+ * They are not merged, because their invariants are opposites:
13192
+ *
13193
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13194
+ * claim a camera cost nothing, which is a measurement nobody made;
13195
+ * - a `failure-contribution` zero is the **most valuable value on the
13196
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13197
+ * and it is exactly what an absent entry cannot say.
13198
+ *
13199
+ * Putting a loss counter on a cost entry would also break the reconciliation
13200
+ * that gives `load-contribution` its point: contributions are subtracted from
13201
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13202
+ * has no process.
13203
+ *
13204
+ * ## Why not a log line, since the counters already exist
13205
+ *
13206
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13207
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13208
+ * ends in a log line, and a log line is the thing the operator asked to stop
13209
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13210
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13211
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13212
+ * media blackout were both diagnosed. The counters stay; this is where they can
13213
+ * be READ.
13214
+ *
13215
+ * ## The rate is served with its denominator or not at all
13216
+ *
13217
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13218
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13219
+ * than yesterday" and was **flat across twelve hours** once divided by the
13220
+ * successes on the same path. A surface that publishes only the numerator
13221
+ * reproduces that mistake on every read.
13222
+ *
13223
+ * ## Shape
13224
+ *
13225
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13226
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13227
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13228
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13229
+ * a forked runner's entries reach hub-main over transport that already exists.
13230
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13231
+ * result through `system.getFailureContributions`.
13232
+ */
13233
+ var FailureReasonCountSchema = object({
13234
+ /**
13235
+ * Why the attempt did not land, in the contributor's own vocabulary —
13236
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13237
+ * strings that already appear in this repo's logs and, where one exists, the
13238
+ * same string the per-track `previewMissReason` records (D276): a second
13239
+ * vocabulary for the same loss would make the row and the counter
13240
+ * un-joinable.
13241
+ */
13242
+ reason: string(),
13243
+ count: number().int().nonnegative()
13244
+ });
13245
+ var FailureContributionSchema = object({
13246
+ /**
13247
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13248
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13249
+ * `unit` free: the families are owned by different addons and a shared enum
13250
+ * is a central list that rots invisibly.
13251
+ */
13252
+ family: string(),
13253
+ /**
13254
+ * The NUMERIC device id — the same value every log line carries as
13255
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13256
+ * cannot name the camera must not emit the entry, because a fleet total
13257
+ * cannot answer the only question anybody asks of this surface.
13258
+ */
13259
+ deviceId: number().int().positive(),
13260
+ /**
13261
+ * A second dimension inside the family: the model / step id for an inference
13262
+ * timeout, so "which camera AND which model" is one read. Absent when the
13263
+ * family has a single variant.
13264
+ */
13265
+ variant: string().optional(),
13266
+ /**
13267
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13268
+ * differencing two reads must drop the interval when it changes, because the
13269
+ * counter restarted from zero in a respawned runner. Same discipline as
13270
+ * `LoadContribution.startedAtMs`.
13271
+ */
13272
+ sinceMs: number(),
13273
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13274
+ atMs: number(),
13275
+ /**
13276
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13277
+ * window. A failure count published without it is the mistake this schema
13278
+ * exists to make impossible.
13279
+ */
13280
+ attempts: number().int().nonnegative(),
13281
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13282
+ succeeded: number().int().nonnegative(),
13283
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13284
+ reasons: array(FailureReasonCountSchema).readonly()
13285
+ });
13286
+ method(_void(), array(FailureContributionSchema).readonly());
13287
+ /**
13180
13288
  * filesystem-browse — per-node capability for browsing the node's local
13181
13289
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13182
13290
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13698,6 +13806,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13698
13806
  kind: "mutation",
13699
13807
  auth: "admin"
13700
13808
  });
13809
+ var LoadContributionSchema = object({
13810
+ role: _enum([
13811
+ "decode",
13812
+ "transcode",
13813
+ "recording",
13814
+ "streaming",
13815
+ "detection"
13816
+ ]),
13817
+ /**
13818
+ * The NUMERIC device id — the same value every log line carries as
13819
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13820
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13821
+ * contributor that cannot name its camera must not emit the entry at all,
13822
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13823
+ * and would quietly turn one camera's cost into everybody's.
13824
+ */
13825
+ deviceId: number().int().positive().nullable(),
13826
+ attribution: _enum([
13827
+ "measured",
13828
+ "accounted",
13829
+ "unattributable"
13830
+ ]),
13831
+ /**
13832
+ * What ONE entry is, in the contributor's own words — `615/high`,
13833
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13834
+ * family and inventing a common one would lose the only information that
13835
+ * makes two entries for the same camera distinguishable.
13836
+ */
13837
+ unit: string(),
13838
+ /**
13839
+ * The OS process this cost lives in, when there is one. Present so a
13840
+ * consumer can (a) tell two generations of the same unit apart across a
13841
+ * restart, and (b) subtract claimed processes from the node's process
13842
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13843
+ * process of its own.
13844
+ */
13845
+ pid: number().int().positive().optional(),
13846
+ /**
13847
+ * When this generation started. The pid's incarnation marker: a consumer
13848
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13849
+ * window when this changes, because the counter restarted from zero in a new
13850
+ * process.
13851
+ */
13852
+ startedAtMs: number().optional(),
13853
+ /**
13854
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13855
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13856
+ * contribution is asked for.
13857
+ *
13858
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13859
+ * needs a sampler, and a new per-node sampler is the defect half of
13860
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13861
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13862
+ *
13863
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13864
+ * an entry with no process.
13865
+ */
13866
+ cpuSeconds: number().optional(),
13867
+ /** Resident bytes of this unit's process, same source and same rules. */
13868
+ rssBytes: number().optional()
13869
+ });
13870
+ method(_void(), array(LoadContributionSchema).readonly());
13701
13871
  /**
13702
13872
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13703
13873
  * through. It stores nothing.
@@ -13774,176 +13944,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13774
13944
  tags: record(string(), string()).optional()
13775
13945
  }), array(LogEntrySchema).readonly());
13776
13946
  /**
13777
- * `failure-contribution` — the capability an addon reports its OWN losses
13778
- * through, per camera, with the denominator attached. It stores nothing.
13779
- *
13780
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13781
- *
13782
- * `load-contribution` answers *what did this camera COST*. This answers *what
13783
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13784
- * copied: the contributor reports what it already knows, hub-main adds only
13785
- * `addonId`, nothing needs global knowledge, and there is no central list for
13786
- * somebody to forget to edit.
13787
- *
13788
- * They are not merged, because their invariants are opposites:
13789
- *
13790
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13791
- * claim a camera cost nothing, which is a measurement nobody made;
13792
- * - a `failure-contribution` zero is the **most valuable value on the
13793
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13794
- * and it is exactly what an absent entry cannot say.
13795
- *
13796
- * Putting a loss counter on a cost entry would also break the reconciliation
13797
- * that gives `load-contribution` its point: contributions are subtracted from
13798
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13799
- * has no process.
13800
- *
13801
- * ## Why not a log line, since the counters already exist
13802
- *
13803
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13804
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13805
- * ends in a log line, and a log line is the thing the operator asked to stop
13806
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13807
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13808
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13809
- * media blackout were both diagnosed. The counters stay; this is where they can
13810
- * be READ.
13811
- *
13812
- * ## The rate is served with its denominator or not at all
13813
- *
13814
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13815
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13816
- * than yesterday" and was **flat across twelve hours** once divided by the
13817
- * successes on the same path. A surface that publishes only the numerator
13818
- * reproduces that mistake on every read.
13819
- *
13820
- * ## Shape
13821
- *
13822
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13823
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13824
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13825
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13826
- * a forked runner's entries reach hub-main over transport that already exists.
13827
- * No new UDS message, no second registry (D3). The operator reads the assembled
13828
- * result through `system.getFailureContributions`.
13829
- */
13830
- var FailureReasonCountSchema = object({
13831
- /**
13832
- * Why the attempt did not land, in the contributor's own vocabulary —
13833
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13834
- * strings that already appear in this repo's logs and, where one exists, the
13835
- * same string the per-track `previewMissReason` records (D276): a second
13836
- * vocabulary for the same loss would make the row and the counter
13837
- * un-joinable.
13838
- */
13839
- reason: string(),
13840
- count: number().int().nonnegative()
13841
- });
13842
- var FailureContributionSchema = object({
13843
- /**
13844
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13845
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13846
- * `unit` free: the families are owned by different addons and a shared enum
13847
- * is a central list that rots invisibly.
13848
- */
13849
- family: string(),
13850
- /**
13851
- * The NUMERIC device id — the same value every log line carries as
13852
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13853
- * cannot name the camera must not emit the entry, because a fleet total
13854
- * cannot answer the only question anybody asks of this surface.
13855
- */
13856
- deviceId: number().int().positive(),
13857
- /**
13858
- * A second dimension inside the family: the model / step id for an inference
13859
- * timeout, so "which camera AND which model" is one read. Absent when the
13860
- * family has a single variant.
13861
- */
13862
- variant: string().optional(),
13863
- /**
13864
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13865
- * differencing two reads must drop the interval when it changes, because the
13866
- * counter restarted from zero in a respawned runner. Same discipline as
13867
- * `LoadContribution.startedAtMs`.
13868
- */
13869
- sinceMs: number(),
13870
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13871
- atMs: number(),
13872
- /**
13873
- * THE DENOMINATOR — every attempt on this path for this camera in the
13874
- * window. A failure count published without it is the mistake this schema
13875
- * exists to make impossible.
13876
- */
13877
- attempts: number().int().nonnegative(),
13878
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13879
- succeeded: number().int().nonnegative(),
13880
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13881
- reasons: array(FailureReasonCountSchema).readonly()
13882
- });
13883
- method(_void(), array(FailureContributionSchema).readonly());
13884
- var LoadContributionSchema = object({
13885
- role: _enum([
13886
- "decode",
13887
- "transcode",
13888
- "recording",
13889
- "streaming",
13890
- "detection"
13891
- ]),
13892
- /**
13893
- * The NUMERIC device id — the same value every log line carries as
13894
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13895
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13896
- * contributor that cannot name its camera must not emit the entry at all,
13897
- * because an unnamed per-camera entry is indistinguishable from a shared one
13898
- * and would quietly turn one camera's cost into everybody's.
13899
- */
13900
- deviceId: number().int().positive().nullable(),
13901
- attribution: _enum([
13902
- "measured",
13903
- "accounted",
13904
- "unattributable"
13905
- ]),
13906
- /**
13907
- * What ONE entry is, in the contributor's own words — `615/high`,
13908
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13909
- * family and inventing a common one would lose the only information that
13910
- * makes two entries for the same camera distinguishable.
13911
- */
13912
- unit: string(),
13913
- /**
13914
- * The OS process this cost lives in, when there is one. Present so a
13915
- * consumer can (a) tell two generations of the same unit apart across a
13916
- * restart, and (b) subtract claimed processes from the node's process
13917
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13918
- * process of its own.
13919
- */
13920
- pid: number().int().positive().optional(),
13921
- /**
13922
- * When this generation started. The pid's incarnation marker: a consumer
13923
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13924
- * window when this changes, because the counter restarted from zero in a new
13925
- * process.
13926
- */
13927
- startedAtMs: number().optional(),
13928
- /**
13929
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13930
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13931
- * contribution is asked for.
13932
- *
13933
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13934
- * needs a sampler, and a new per-node sampler is the defect half of
13935
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13936
- * by whoever already keeps a history; a rate cannot be un-averaged.
13937
- *
13938
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13939
- * an entry with no process.
13940
- */
13941
- cpuSeconds: number().optional(),
13942
- /** Resident bytes of this unit's process, same source and same rules. */
13943
- rssBytes: number().optional()
13944
- });
13945
- method(_void(), array(LoadContributionSchema).readonly());
13946
- /**
13947
13947
  * `login-method` — collection cap through which auth addons contribute
13948
13948
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13949
13949
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18681,12 +18681,53 @@ var MediaFileKindEnum = _enum([
18681
18681
  "keyFrameSmall",
18682
18682
  "thumbnailSmall"
18683
18683
  ]);
18684
+ /**
18685
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18686
+ * ARE — never the bytes themselves.
18687
+ *
18688
+ * ## Why `url` and not `base64`
18689
+ *
18690
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18691
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18692
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18693
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18694
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18695
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18696
+ *
18697
+ * `url` points at the `event-media` data plane
18698
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18699
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18700
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18701
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18702
+ * no less protected than they were inside a `view`-level cap response — see
18703
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18704
+ * (per-device scoping).
18705
+ *
18706
+ * The URL is built from the row's **stored** key, which is not always its
18707
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18708
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18709
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18710
+ *
18711
+ * ## `base64` is TRANSITIONAL and is going away
18712
+ *
18713
+ * It is still populated for one reason: the deployed viewer's track-detail
18714
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18715
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18716
+ * triangle — not as absence. Removing the field before that viewer ships is an
18717
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18718
+ * delete this line and the `withBytes` pass-through in
18719
+ * `analytics-query-facade.ts`; nothing else reads it.
18720
+ */
18684
18721
  var MediaFileSchema = object({
18685
18722
  key: string(),
18686
18723
  kind: MediaFileKindEnum,
18687
- base64: string(),
18688
18724
  sizeBytes: number(),
18689
18725
  timestamp: number()
18726
+ }).extend({
18727
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18728
+ url: string(),
18729
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18730
+ base64: string()
18690
18731
  });
18691
18732
  /**
18692
18733
  * One media row WITHOUT its bytes.
@@ -18698,7 +18739,9 @@ var MediaFileSchema = object({
18698
18739
  * blocks the whole view.
18699
18740
  *
18700
18741
  * `sizeBytes` is carried because it is what lets a client decide between the
18701
- * stored blob and a `?variant=thumb` rendering without fetching either.
18742
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18743
+ * `url` because a client that had to build the plane path itself is a second
18744
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18702
18745
  */
18703
18746
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18704
18747
  /**
@@ -19045,6 +19088,50 @@ var EventStoreFootprintSchema = object({
19045
19088
  totalBytes: number().int(),
19046
19089
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19047
19090
  });
19091
+ /** Event-media footprint for one {@link MediaFileKind}. */
19092
+ var EventMediaKindFootprintSchema = object({
19093
+ kind: MediaFileKindEnum,
19094
+ /** Media rows of this kind. */
19095
+ rows: number().int(),
19096
+ /** Bytes on disk held by those rows. */
19097
+ bytes: number().int()
19098
+ });
19099
+ /**
19100
+ * The media footprint broken down by KIND — the axis a deletion decision
19101
+ * actually turns on.
19102
+ *
19103
+ * A byte total says how much there is; it cannot say what is safe to remove.
19104
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19105
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19106
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19107
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19108
+ * nothing else, so sizing a deletion means summing per kind.
19109
+ *
19110
+ * ## Why `unaccounted*` exists
19111
+ *
19112
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19113
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19114
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19115
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19116
+ * retired code path, or by a version that knew a kind this one does not) would
19117
+ * otherwise vanish from the total silently, and an operator would delete
19118
+ * against a denominator smaller than the disk.
19119
+ *
19120
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19121
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19122
+ */
19123
+ var EventMediaKindBreakdownSchema = object({
19124
+ /** Every media row in scope, from one unfiltered aggregate. */
19125
+ totalRows: number().int(),
19126
+ /** Every media byte in scope, from that same aggregate. */
19127
+ totalBytes: number().int(),
19128
+ /** Per-kind footprint, ordered by bytes descending. */
19129
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19130
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19131
+ unaccountedRows: number().int(),
19132
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19133
+ unaccountedBytes: number().int()
19134
+ });
19048
19135
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19049
19136
  var EventPruneCountsSchema = object({
19050
19137
  motion: number().int(),
@@ -19248,6 +19335,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19248
19335
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19249
19336
  kind: "query",
19250
19337
  auth: "admin"
19338
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19339
+ kind: "query",
19340
+ auth: "admin"
19251
19341
  }), method(object({
19252
19342
  olderThanMs: number(),
19253
19343
  reason: OpsLogReasonSchema.optional()
@@ -19387,6 +19477,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19387
19477
  }), array(MediaFileSchema).readonly()), method(object({
19388
19478
  trackId: string(),
19389
19479
  deviceId: number()
19480
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19481
+ eventId: string(),
19482
+ deviceId: number()
19390
19483
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19391
19484
  kind: "mutation",
19392
19485
  auth: "admin"
@@ -21300,6 +21393,20 @@ method(object({
21300
21393
  error: string().optional()
21301
21394
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21302
21395
  providerId: string(),
21396
+ /**
21397
+ * The location this config is an UNSAVED edit of, when there is one.
21398
+ *
21399
+ * `listLocations` replaces every declared secret with the redaction
21400
+ * sentinel, so the edit modal's form state holds the sentinel for any
21401
+ * credential the operator did not retype — and posting that here
21402
+ * without a way to resolve it makes the provider try to authenticate
21403
+ * as `__camstack_redacted__` and report the operator's own working
21404
+ * password as wrong. Given this id, the orchestrator restores each
21405
+ * sentinel from the stored config (same rule as `upsertLocation`)
21406
+ * before dispatching. Omitted by the "Add location" wizard, where
21407
+ * every value was typed just now and nothing is stored yet.
21408
+ */
21409
+ locationId: string().optional(),
21303
21410
  config: record(string(), unknown())
21304
21411
  }), object({
21305
21412
  ok: boolean(),
@@ -24481,10 +24588,24 @@ var FaceClusterSchema = object({
24481
24588
  size: number().int(),
24482
24589
  cohesion: number()
24483
24590
  });
24591
+ /**
24592
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24593
+ * are — never the bytes.
24594
+ *
24595
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24596
+ * track/event contract) is still populated because a deployed viewer requires
24597
+ * the field to parse a row at all; this method has no such reader. Its ONE
24598
+ * caller is the admin UI's detail modal, which was building
24599
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24600
+ * dialog already rendering its key FRAME from the `event-media` plane.
24601
+ *
24602
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24603
+ * media key directly, so this needed no new plane and no new access decision.
24604
+ */
24484
24605
  var MediaFileLiteSchema$1 = object({
24485
24606
  key: string(),
24486
24607
  kind: string(),
24487
- base64: string(),
24608
+ url: string(),
24488
24609
  sizeBytes: number(),
24489
24610
  timestamp: number()
24490
24611
  });
@@ -27552,10 +27673,24 @@ var PlateInfoSchema = object({
27552
27673
  */
27553
27674
  cropUrl: string().optional()
27554
27675
  });
27676
+ /**
27677
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27678
+ * are — never the bytes.
27679
+ *
27680
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27681
+ * track/event contract) is still populated because a deployed viewer requires
27682
+ * the field to parse a row at all; this method has no such reader. Its ONE
27683
+ * caller is the admin UI's detail modal, which was building
27684
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27685
+ * dialog already rendering its key FRAME from the `event-media` plane.
27686
+ *
27687
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27688
+ * media key directly, so this needed no new plane and no new access decision.
27689
+ */
27555
27690
  var MediaFileLiteSchema = object({
27556
27691
  key: string(),
27557
27692
  kind: string(),
27558
- base64: string(),
27693
+ url: string(),
27559
27694
  sizeBytes: number(),
27560
27695
  timestamp: number()
27561
27696
  });
@@ -35802,6 +35937,12 @@ Object.freeze({
35802
35937
  addonId: null,
35803
35938
  access: "view"
35804
35939
  },
35940
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
35941
+ capName: "pipeline-analytics",
35942
+ capScope: "device",
35943
+ addonId: null,
35944
+ access: "view"
35945
+ },
35805
35946
  "pipelineAnalytics.getEventStoreFootprint": {
35806
35947
  capName: "pipeline-analytics",
35807
35948
  capScope: "device",
@@ -35898,6 +36039,12 @@ Object.freeze({
35898
36039
  addonId: null,
35899
36040
  access: "view"
35900
36041
  },
36042
+ "pipelineAnalytics.listEventMedia": {
36043
+ capName: "pipeline-analytics",
36044
+ capScope: "device",
36045
+ addonId: null,
36046
+ access: "view"
36047
+ },
35901
36048
  "pipelineAnalytics.listGroups": {
35902
36049
  capName: "pipeline-analytics",
35903
36050
  capScope: "device",
@@ -39461,6 +39608,11 @@ Object.freeze({
39461
39608
  form: "single",
39462
39609
  optional: false
39463
39610
  }],
39611
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
39612
+ name: "deviceId",
39613
+ form: "single",
39614
+ optional: true
39615
+ }],
39464
39616
  "pipelineAnalytics.getGroup": [{
39465
39617
  name: "deviceId",
39466
39618
  form: "single",
@@ -39521,6 +39673,11 @@ Object.freeze({
39521
39673
  form: "array",
39522
39674
  optional: false
39523
39675
  }],
39676
+ "pipelineAnalytics.listEventMedia": [{
39677
+ name: "deviceId",
39678
+ form: "single",
39679
+ optional: false
39680
+ }],
39524
39681
  "pipelineAnalytics.listGroups": [{
39525
39682
  name: "deviceIds",
39526
39683
  form: "array",
package/dist/addon.mjs CHANGED
@@ -13178,6 +13178,114 @@ method(object({
13178
13178
  height: number()
13179
13179
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13180
13180
  /**
13181
+ * `failure-contribution` — the capability an addon reports its OWN losses
13182
+ * through, per camera, with the denominator attached. It stores nothing.
13183
+ *
13184
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13185
+ *
13186
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13187
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13188
+ * copied: the contributor reports what it already knows, hub-main adds only
13189
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13190
+ * somebody to forget to edit.
13191
+ *
13192
+ * They are not merged, because their invariants are opposites:
13193
+ *
13194
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13195
+ * claim a camera cost nothing, which is a measurement nobody made;
13196
+ * - a `failure-contribution` zero is the **most valuable value on the
13197
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13198
+ * and it is exactly what an absent entry cannot say.
13199
+ *
13200
+ * Putting a loss counter on a cost entry would also break the reconciliation
13201
+ * that gives `load-contribution` its point: contributions are subtracted from
13202
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13203
+ * has no process.
13204
+ *
13205
+ * ## Why not a log line, since the counters already exist
13206
+ *
13207
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13208
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13209
+ * ends in a log line, and a log line is the thing the operator asked to stop
13210
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13211
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13212
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13213
+ * media blackout were both diagnosed. The counters stay; this is where they can
13214
+ * be READ.
13215
+ *
13216
+ * ## The rate is served with its denominator or not at all
13217
+ *
13218
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13219
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13220
+ * than yesterday" and was **flat across twelve hours** once divided by the
13221
+ * successes on the same path. A surface that publishes only the numerator
13222
+ * reproduces that mistake on every read.
13223
+ *
13224
+ * ## Shape
13225
+ *
13226
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13227
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13228
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13229
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13230
+ * a forked runner's entries reach hub-main over transport that already exists.
13231
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13232
+ * result through `system.getFailureContributions`.
13233
+ */
13234
+ var FailureReasonCountSchema = object({
13235
+ /**
13236
+ * Why the attempt did not land, in the contributor's own vocabulary —
13237
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13238
+ * strings that already appear in this repo's logs and, where one exists, the
13239
+ * same string the per-track `previewMissReason` records (D276): a second
13240
+ * vocabulary for the same loss would make the row and the counter
13241
+ * un-joinable.
13242
+ */
13243
+ reason: string(),
13244
+ count: number().int().nonnegative()
13245
+ });
13246
+ var FailureContributionSchema = object({
13247
+ /**
13248
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13249
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13250
+ * `unit` free: the families are owned by different addons and a shared enum
13251
+ * is a central list that rots invisibly.
13252
+ */
13253
+ family: string(),
13254
+ /**
13255
+ * The NUMERIC device id — the same value every log line carries as
13256
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13257
+ * cannot name the camera must not emit the entry, because a fleet total
13258
+ * cannot answer the only question anybody asks of this surface.
13259
+ */
13260
+ deviceId: number().int().positive(),
13261
+ /**
13262
+ * A second dimension inside the family: the model / step id for an inference
13263
+ * timeout, so "which camera AND which model" is one read. Absent when the
13264
+ * family has a single variant.
13265
+ */
13266
+ variant: string().optional(),
13267
+ /**
13268
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13269
+ * differencing two reads must drop the interval when it changes, because the
13270
+ * counter restarted from zero in a respawned runner. Same discipline as
13271
+ * `LoadContribution.startedAtMs`.
13272
+ */
13273
+ sinceMs: number(),
13274
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13275
+ atMs: number(),
13276
+ /**
13277
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13278
+ * window. A failure count published without it is the mistake this schema
13279
+ * exists to make impossible.
13280
+ */
13281
+ attempts: number().int().nonnegative(),
13282
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13283
+ succeeded: number().int().nonnegative(),
13284
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13285
+ reasons: array(FailureReasonCountSchema).readonly()
13286
+ });
13287
+ method(_void(), array(FailureContributionSchema).readonly());
13288
+ /**
13181
13289
  * filesystem-browse — per-node capability for browsing the node's local
13182
13290
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13183
13291
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13699,6 +13807,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13699
13807
  kind: "mutation",
13700
13808
  auth: "admin"
13701
13809
  });
13810
+ var LoadContributionSchema = object({
13811
+ role: _enum([
13812
+ "decode",
13813
+ "transcode",
13814
+ "recording",
13815
+ "streaming",
13816
+ "detection"
13817
+ ]),
13818
+ /**
13819
+ * The NUMERIC device id — the same value every log line carries as
13820
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13821
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13822
+ * contributor that cannot name its camera must not emit the entry at all,
13823
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13824
+ * and would quietly turn one camera's cost into everybody's.
13825
+ */
13826
+ deviceId: number().int().positive().nullable(),
13827
+ attribution: _enum([
13828
+ "measured",
13829
+ "accounted",
13830
+ "unattributable"
13831
+ ]),
13832
+ /**
13833
+ * What ONE entry is, in the contributor's own words — `615/high`,
13834
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13835
+ * family and inventing a common one would lose the only information that
13836
+ * makes two entries for the same camera distinguishable.
13837
+ */
13838
+ unit: string(),
13839
+ /**
13840
+ * The OS process this cost lives in, when there is one. Present so a
13841
+ * consumer can (a) tell two generations of the same unit apart across a
13842
+ * restart, and (b) subtract claimed processes from the node's process
13843
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13844
+ * process of its own.
13845
+ */
13846
+ pid: number().int().positive().optional(),
13847
+ /**
13848
+ * When this generation started. The pid's incarnation marker: a consumer
13849
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13850
+ * window when this changes, because the counter restarted from zero in a new
13851
+ * process.
13852
+ */
13853
+ startedAtMs: number().optional(),
13854
+ /**
13855
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13856
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13857
+ * contribution is asked for.
13858
+ *
13859
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13860
+ * needs a sampler, and a new per-node sampler is the defect half of
13861
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13862
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13863
+ *
13864
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13865
+ * an entry with no process.
13866
+ */
13867
+ cpuSeconds: number().optional(),
13868
+ /** Resident bytes of this unit's process, same source and same rules. */
13869
+ rssBytes: number().optional()
13870
+ });
13871
+ method(_void(), array(LoadContributionSchema).readonly());
13702
13872
  /**
13703
13873
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13704
13874
  * through. It stores nothing.
@@ -13775,176 +13945,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13775
13945
  tags: record(string(), string()).optional()
13776
13946
  }), array(LogEntrySchema).readonly());
13777
13947
  /**
13778
- * `failure-contribution` — the capability an addon reports its OWN losses
13779
- * through, per camera, with the denominator attached. It stores nothing.
13780
- *
13781
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13782
- *
13783
- * `load-contribution` answers *what did this camera COST*. This answers *what
13784
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13785
- * copied: the contributor reports what it already knows, hub-main adds only
13786
- * `addonId`, nothing needs global knowledge, and there is no central list for
13787
- * somebody to forget to edit.
13788
- *
13789
- * They are not merged, because their invariants are opposites:
13790
- *
13791
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13792
- * claim a camera cost nothing, which is a measurement nobody made;
13793
- * - a `failure-contribution` zero is the **most valuable value on the
13794
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13795
- * and it is exactly what an absent entry cannot say.
13796
- *
13797
- * Putting a loss counter on a cost entry would also break the reconciliation
13798
- * that gives `load-contribution` its point: contributions are subtracted from
13799
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13800
- * has no process.
13801
- *
13802
- * ## Why not a log line, since the counters already exist
13803
- *
13804
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13805
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13806
- * ends in a log line, and a log line is the thing the operator asked to stop
13807
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13808
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13809
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13810
- * media blackout were both diagnosed. The counters stay; this is where they can
13811
- * be READ.
13812
- *
13813
- * ## The rate is served with its denominator or not at all
13814
- *
13815
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13816
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13817
- * than yesterday" and was **flat across twelve hours** once divided by the
13818
- * successes on the same path. A surface that publishes only the numerator
13819
- * reproduces that mistake on every read.
13820
- *
13821
- * ## Shape
13822
- *
13823
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13824
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13825
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13826
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13827
- * a forked runner's entries reach hub-main over transport that already exists.
13828
- * No new UDS message, no second registry (D3). The operator reads the assembled
13829
- * result through `system.getFailureContributions`.
13830
- */
13831
- var FailureReasonCountSchema = object({
13832
- /**
13833
- * Why the attempt did not land, in the contributor's own vocabulary —
13834
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13835
- * strings that already appear in this repo's logs and, where one exists, the
13836
- * same string the per-track `previewMissReason` records (D276): a second
13837
- * vocabulary for the same loss would make the row and the counter
13838
- * un-joinable.
13839
- */
13840
- reason: string(),
13841
- count: number().int().nonnegative()
13842
- });
13843
- var FailureContributionSchema = object({
13844
- /**
13845
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13846
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13847
- * `unit` free: the families are owned by different addons and a shared enum
13848
- * is a central list that rots invisibly.
13849
- */
13850
- family: string(),
13851
- /**
13852
- * The NUMERIC device id — the same value every log line carries as
13853
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13854
- * cannot name the camera must not emit the entry, because a fleet total
13855
- * cannot answer the only question anybody asks of this surface.
13856
- */
13857
- deviceId: number().int().positive(),
13858
- /**
13859
- * A second dimension inside the family: the model / step id for an inference
13860
- * timeout, so "which camera AND which model" is one read. Absent when the
13861
- * family has a single variant.
13862
- */
13863
- variant: string().optional(),
13864
- /**
13865
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13866
- * differencing two reads must drop the interval when it changes, because the
13867
- * counter restarted from zero in a respawned runner. Same discipline as
13868
- * `LoadContribution.startedAtMs`.
13869
- */
13870
- sinceMs: number(),
13871
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13872
- atMs: number(),
13873
- /**
13874
- * THE DENOMINATOR — every attempt on this path for this camera in the
13875
- * window. A failure count published without it is the mistake this schema
13876
- * exists to make impossible.
13877
- */
13878
- attempts: number().int().nonnegative(),
13879
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13880
- succeeded: number().int().nonnegative(),
13881
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13882
- reasons: array(FailureReasonCountSchema).readonly()
13883
- });
13884
- method(_void(), array(FailureContributionSchema).readonly());
13885
- var LoadContributionSchema = object({
13886
- role: _enum([
13887
- "decode",
13888
- "transcode",
13889
- "recording",
13890
- "streaming",
13891
- "detection"
13892
- ]),
13893
- /**
13894
- * The NUMERIC device id — the same value every log line carries as
13895
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13896
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13897
- * contributor that cannot name its camera must not emit the entry at all,
13898
- * because an unnamed per-camera entry is indistinguishable from a shared one
13899
- * and would quietly turn one camera's cost into everybody's.
13900
- */
13901
- deviceId: number().int().positive().nullable(),
13902
- attribution: _enum([
13903
- "measured",
13904
- "accounted",
13905
- "unattributable"
13906
- ]),
13907
- /**
13908
- * What ONE entry is, in the contributor's own words — `615/high`,
13909
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13910
- * family and inventing a common one would lose the only information that
13911
- * makes two entries for the same camera distinguishable.
13912
- */
13913
- unit: string(),
13914
- /**
13915
- * The OS process this cost lives in, when there is one. Present so a
13916
- * consumer can (a) tell two generations of the same unit apart across a
13917
- * restart, and (b) subtract claimed processes from the node's process
13918
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13919
- * process of its own.
13920
- */
13921
- pid: number().int().positive().optional(),
13922
- /**
13923
- * When this generation started. The pid's incarnation marker: a consumer
13924
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13925
- * window when this changes, because the counter restarted from zero in a new
13926
- * process.
13927
- */
13928
- startedAtMs: number().optional(),
13929
- /**
13930
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13931
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13932
- * contribution is asked for.
13933
- *
13934
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13935
- * needs a sampler, and a new per-node sampler is the defect half of
13936
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13937
- * by whoever already keeps a history; a rate cannot be un-averaged.
13938
- *
13939
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13940
- * an entry with no process.
13941
- */
13942
- cpuSeconds: number().optional(),
13943
- /** Resident bytes of this unit's process, same source and same rules. */
13944
- rssBytes: number().optional()
13945
- });
13946
- method(_void(), array(LoadContributionSchema).readonly());
13947
- /**
13948
13948
  * `login-method` — collection cap through which auth addons contribute
13949
13949
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13950
13950
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18682,12 +18682,53 @@ var MediaFileKindEnum = _enum([
18682
18682
  "keyFrameSmall",
18683
18683
  "thumbnailSmall"
18684
18684
  ]);
18685
+ /**
18686
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18687
+ * ARE — never the bytes themselves.
18688
+ *
18689
+ * ## Why `url` and not `base64`
18690
+ *
18691
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18692
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18693
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18694
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18695
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18696
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18697
+ *
18698
+ * `url` points at the `event-media` data plane
18699
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18700
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18701
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18702
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18703
+ * no less protected than they were inside a `view`-level cap response — see
18704
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18705
+ * (per-device scoping).
18706
+ *
18707
+ * The URL is built from the row's **stored** key, which is not always its
18708
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18709
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18710
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18711
+ *
18712
+ * ## `base64` is TRANSITIONAL and is going away
18713
+ *
18714
+ * It is still populated for one reason: the deployed viewer's track-detail
18715
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18716
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18717
+ * triangle — not as absence. Removing the field before that viewer ships is an
18718
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18719
+ * delete this line and the `withBytes` pass-through in
18720
+ * `analytics-query-facade.ts`; nothing else reads it.
18721
+ */
18685
18722
  var MediaFileSchema = object({
18686
18723
  key: string(),
18687
18724
  kind: MediaFileKindEnum,
18688
- base64: string(),
18689
18725
  sizeBytes: number(),
18690
18726
  timestamp: number()
18727
+ }).extend({
18728
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18729
+ url: string(),
18730
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18731
+ base64: string()
18691
18732
  });
18692
18733
  /**
18693
18734
  * One media row WITHOUT its bytes.
@@ -18699,7 +18740,9 @@ var MediaFileSchema = object({
18699
18740
  * blocks the whole view.
18700
18741
  *
18701
18742
  * `sizeBytes` is carried because it is what lets a client decide between the
18702
- * stored blob and a `?variant=thumb` rendering without fetching either.
18743
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18744
+ * `url` because a client that had to build the plane path itself is a second
18745
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18703
18746
  */
18704
18747
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18705
18748
  /**
@@ -19046,6 +19089,50 @@ var EventStoreFootprintSchema = object({
19046
19089
  totalBytes: number().int(),
19047
19090
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19048
19091
  });
19092
+ /** Event-media footprint for one {@link MediaFileKind}. */
19093
+ var EventMediaKindFootprintSchema = object({
19094
+ kind: MediaFileKindEnum,
19095
+ /** Media rows of this kind. */
19096
+ rows: number().int(),
19097
+ /** Bytes on disk held by those rows. */
19098
+ bytes: number().int()
19099
+ });
19100
+ /**
19101
+ * The media footprint broken down by KIND — the axis a deletion decision
19102
+ * actually turns on.
19103
+ *
19104
+ * A byte total says how much there is; it cannot say what is safe to remove.
19105
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19106
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19107
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19108
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19109
+ * nothing else, so sizing a deletion means summing per kind.
19110
+ *
19111
+ * ## Why `unaccounted*` exists
19112
+ *
19113
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19114
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19115
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19116
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19117
+ * retired code path, or by a version that knew a kind this one does not) would
19118
+ * otherwise vanish from the total silently, and an operator would delete
19119
+ * against a denominator smaller than the disk.
19120
+ *
19121
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19122
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19123
+ */
19124
+ var EventMediaKindBreakdownSchema = object({
19125
+ /** Every media row in scope, from one unfiltered aggregate. */
19126
+ totalRows: number().int(),
19127
+ /** Every media byte in scope, from that same aggregate. */
19128
+ totalBytes: number().int(),
19129
+ /** Per-kind footprint, ordered by bytes descending. */
19130
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19131
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19132
+ unaccountedRows: number().int(),
19133
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19134
+ unaccountedBytes: number().int()
19135
+ });
19049
19136
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19050
19137
  var EventPruneCountsSchema = object({
19051
19138
  motion: number().int(),
@@ -19249,6 +19336,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19249
19336
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19250
19337
  kind: "query",
19251
19338
  auth: "admin"
19339
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19340
+ kind: "query",
19341
+ auth: "admin"
19252
19342
  }), method(object({
19253
19343
  olderThanMs: number(),
19254
19344
  reason: OpsLogReasonSchema.optional()
@@ -19388,6 +19478,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19388
19478
  }), array(MediaFileSchema).readonly()), method(object({
19389
19479
  trackId: string(),
19390
19480
  deviceId: number()
19481
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19482
+ eventId: string(),
19483
+ deviceId: number()
19391
19484
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19392
19485
  kind: "mutation",
19393
19486
  auth: "admin"
@@ -21301,6 +21394,20 @@ method(object({
21301
21394
  error: string().optional()
21302
21395
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21303
21396
  providerId: string(),
21397
+ /**
21398
+ * The location this config is an UNSAVED edit of, when there is one.
21399
+ *
21400
+ * `listLocations` replaces every declared secret with the redaction
21401
+ * sentinel, so the edit modal's form state holds the sentinel for any
21402
+ * credential the operator did not retype — and posting that here
21403
+ * without a way to resolve it makes the provider try to authenticate
21404
+ * as `__camstack_redacted__` and report the operator's own working
21405
+ * password as wrong. Given this id, the orchestrator restores each
21406
+ * sentinel from the stored config (same rule as `upsertLocation`)
21407
+ * before dispatching. Omitted by the "Add location" wizard, where
21408
+ * every value was typed just now and nothing is stored yet.
21409
+ */
21410
+ locationId: string().optional(),
21304
21411
  config: record(string(), unknown())
21305
21412
  }), object({
21306
21413
  ok: boolean(),
@@ -24482,10 +24589,24 @@ var FaceClusterSchema = object({
24482
24589
  size: number().int(),
24483
24590
  cohesion: number()
24484
24591
  });
24592
+ /**
24593
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24594
+ * are — never the bytes.
24595
+ *
24596
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24597
+ * track/event contract) is still populated because a deployed viewer requires
24598
+ * the field to parse a row at all; this method has no such reader. Its ONE
24599
+ * caller is the admin UI's detail modal, which was building
24600
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24601
+ * dialog already rendering its key FRAME from the `event-media` plane.
24602
+ *
24603
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24604
+ * media key directly, so this needed no new plane and no new access decision.
24605
+ */
24485
24606
  var MediaFileLiteSchema$1 = object({
24486
24607
  key: string(),
24487
24608
  kind: string(),
24488
- base64: string(),
24609
+ url: string(),
24489
24610
  sizeBytes: number(),
24490
24611
  timestamp: number()
24491
24612
  });
@@ -27553,10 +27674,24 @@ var PlateInfoSchema = object({
27553
27674
  */
27554
27675
  cropUrl: string().optional()
27555
27676
  });
27677
+ /**
27678
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27679
+ * are — never the bytes.
27680
+ *
27681
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27682
+ * track/event contract) is still populated because a deployed viewer requires
27683
+ * the field to parse a row at all; this method has no such reader. Its ONE
27684
+ * caller is the admin UI's detail modal, which was building
27685
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27686
+ * dialog already rendering its key FRAME from the `event-media` plane.
27687
+ *
27688
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27689
+ * media key directly, so this needed no new plane and no new access decision.
27690
+ */
27556
27691
  var MediaFileLiteSchema = object({
27557
27692
  key: string(),
27558
27693
  kind: string(),
27559
- base64: string(),
27694
+ url: string(),
27560
27695
  sizeBytes: number(),
27561
27696
  timestamp: number()
27562
27697
  });
@@ -35803,6 +35938,12 @@ Object.freeze({
35803
35938
  addonId: null,
35804
35939
  access: "view"
35805
35940
  },
35941
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
35942
+ capName: "pipeline-analytics",
35943
+ capScope: "device",
35944
+ addonId: null,
35945
+ access: "view"
35946
+ },
35806
35947
  "pipelineAnalytics.getEventStoreFootprint": {
35807
35948
  capName: "pipeline-analytics",
35808
35949
  capScope: "device",
@@ -35899,6 +36040,12 @@ Object.freeze({
35899
36040
  addonId: null,
35900
36041
  access: "view"
35901
36042
  },
36043
+ "pipelineAnalytics.listEventMedia": {
36044
+ capName: "pipeline-analytics",
36045
+ capScope: "device",
36046
+ addonId: null,
36047
+ access: "view"
36048
+ },
35902
36049
  "pipelineAnalytics.listGroups": {
35903
36050
  capName: "pipeline-analytics",
35904
36051
  capScope: "device",
@@ -39462,6 +39609,11 @@ Object.freeze({
39462
39609
  form: "single",
39463
39610
  optional: false
39464
39611
  }],
39612
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
39613
+ name: "deviceId",
39614
+ form: "single",
39615
+ optional: true
39616
+ }],
39465
39617
  "pipelineAnalytics.getGroup": [{
39466
39618
  name: "deviceId",
39467
39619
  form: "single",
@@ -39522,6 +39674,11 @@ Object.freeze({
39522
39674
  form: "array",
39523
39675
  optional: false
39524
39676
  }],
39677
+ "pipelineAnalytics.listEventMedia": [{
39678
+ name: "deviceId",
39679
+ form: "single",
39680
+ optional: false
39681
+ }],
39525
39682
  "pipelineAnalytics.listGroups": [{
39526
39683
  name: "deviceIds",
39527
39684
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-amcrest",
3
- "version": "0.2.47",
3
+ "version": "0.2.49",
4
4
  "description": "Amcrest/Dahua camera device provider addon for CamStack — Dahua CGI over HTTP(S) with digest auth (snapshot, RTSP catalog, PTZ, image/day-night config)",
5
5
  "keywords": [
6
6
  "camstack",