@camstack/addon-provider-wyze 0.2.50 → 0.2.52

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
@@ -13233,6 +13233,114 @@ method(object({
13233
13233
  height: number()
13234
13234
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13235
13235
  /**
13236
+ * `failure-contribution` — the capability an addon reports its OWN losses
13237
+ * through, per camera, with the denominator attached. It stores nothing.
13238
+ *
13239
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13240
+ *
13241
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13242
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13243
+ * copied: the contributor reports what it already knows, hub-main adds only
13244
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13245
+ * somebody to forget to edit.
13246
+ *
13247
+ * They are not merged, because their invariants are opposites:
13248
+ *
13249
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13250
+ * claim a camera cost nothing, which is a measurement nobody made;
13251
+ * - a `failure-contribution` zero is the **most valuable value on the
13252
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13253
+ * and it is exactly what an absent entry cannot say.
13254
+ *
13255
+ * Putting a loss counter on a cost entry would also break the reconciliation
13256
+ * that gives `load-contribution` its point: contributions are subtracted from
13257
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13258
+ * has no process.
13259
+ *
13260
+ * ## Why not a log line, since the counters already exist
13261
+ *
13262
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13263
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13264
+ * ends in a log line, and a log line is the thing the operator asked to stop
13265
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13266
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13267
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13268
+ * media blackout were both diagnosed. The counters stay; this is where they can
13269
+ * be READ.
13270
+ *
13271
+ * ## The rate is served with its denominator or not at all
13272
+ *
13273
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13274
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13275
+ * than yesterday" and was **flat across twelve hours** once divided by the
13276
+ * successes on the same path. A surface that publishes only the numerator
13277
+ * reproduces that mistake on every read.
13278
+ *
13279
+ * ## Shape
13280
+ *
13281
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13282
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13283
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13284
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13285
+ * a forked runner's entries reach hub-main over transport that already exists.
13286
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13287
+ * result through `system.getFailureContributions`.
13288
+ */
13289
+ var FailureReasonCountSchema = object({
13290
+ /**
13291
+ * Why the attempt did not land, in the contributor's own vocabulary —
13292
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13293
+ * strings that already appear in this repo's logs and, where one exists, the
13294
+ * same string the per-track `previewMissReason` records (D276): a second
13295
+ * vocabulary for the same loss would make the row and the counter
13296
+ * un-joinable.
13297
+ */
13298
+ reason: string(),
13299
+ count: number().int().nonnegative()
13300
+ });
13301
+ var FailureContributionSchema = object({
13302
+ /**
13303
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13304
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13305
+ * `unit` free: the families are owned by different addons and a shared enum
13306
+ * is a central list that rots invisibly.
13307
+ */
13308
+ family: string(),
13309
+ /**
13310
+ * The NUMERIC device id — the same value every log line carries as
13311
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13312
+ * cannot name the camera must not emit the entry, because a fleet total
13313
+ * cannot answer the only question anybody asks of this surface.
13314
+ */
13315
+ deviceId: number().int().positive(),
13316
+ /**
13317
+ * A second dimension inside the family: the model / step id for an inference
13318
+ * timeout, so "which camera AND which model" is one read. Absent when the
13319
+ * family has a single variant.
13320
+ */
13321
+ variant: string().optional(),
13322
+ /**
13323
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13324
+ * differencing two reads must drop the interval when it changes, because the
13325
+ * counter restarted from zero in a respawned runner. Same discipline as
13326
+ * `LoadContribution.startedAtMs`.
13327
+ */
13328
+ sinceMs: number(),
13329
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13330
+ atMs: number(),
13331
+ /**
13332
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13333
+ * window. A failure count published without it is the mistake this schema
13334
+ * exists to make impossible.
13335
+ */
13336
+ attempts: number().int().nonnegative(),
13337
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13338
+ succeeded: number().int().nonnegative(),
13339
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13340
+ reasons: array(FailureReasonCountSchema).readonly()
13341
+ });
13342
+ method(_void(), array(FailureContributionSchema).readonly());
13343
+ /**
13236
13344
  * filesystem-browse — per-node capability for browsing the node's local
13237
13345
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13238
13346
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13754,6 +13862,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13754
13862
  kind: "mutation",
13755
13863
  auth: "admin"
13756
13864
  });
13865
+ var LoadContributionSchema = object({
13866
+ role: _enum([
13867
+ "decode",
13868
+ "transcode",
13869
+ "recording",
13870
+ "streaming",
13871
+ "detection"
13872
+ ]),
13873
+ /**
13874
+ * The NUMERIC device id — the same value every log line carries as
13875
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13876
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13877
+ * contributor that cannot name its camera must not emit the entry at all,
13878
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13879
+ * and would quietly turn one camera's cost into everybody's.
13880
+ */
13881
+ deviceId: number().int().positive().nullable(),
13882
+ attribution: _enum([
13883
+ "measured",
13884
+ "accounted",
13885
+ "unattributable"
13886
+ ]),
13887
+ /**
13888
+ * What ONE entry is, in the contributor's own words — `615/high`,
13889
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13890
+ * family and inventing a common one would lose the only information that
13891
+ * makes two entries for the same camera distinguishable.
13892
+ */
13893
+ unit: string(),
13894
+ /**
13895
+ * The OS process this cost lives in, when there is one. Present so a
13896
+ * consumer can (a) tell two generations of the same unit apart across a
13897
+ * restart, and (b) subtract claimed processes from the node's process
13898
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13899
+ * process of its own.
13900
+ */
13901
+ pid: number().int().positive().optional(),
13902
+ /**
13903
+ * When this generation started. The pid's incarnation marker: a consumer
13904
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13905
+ * window when this changes, because the counter restarted from zero in a new
13906
+ * process.
13907
+ */
13908
+ startedAtMs: number().optional(),
13909
+ /**
13910
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13911
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13912
+ * contribution is asked for.
13913
+ *
13914
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13915
+ * needs a sampler, and a new per-node sampler is the defect half of
13916
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13917
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13918
+ *
13919
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13920
+ * an entry with no process.
13921
+ */
13922
+ cpuSeconds: number().optional(),
13923
+ /** Resident bytes of this unit's process, same source and same rules. */
13924
+ rssBytes: number().optional()
13925
+ });
13926
+ method(_void(), array(LoadContributionSchema).readonly());
13757
13927
  /**
13758
13928
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13759
13929
  * through. It stores nothing.
@@ -13830,176 +14000,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13830
14000
  tags: record(string(), string()).optional()
13831
14001
  }), array(LogEntrySchema).readonly());
13832
14002
  /**
13833
- * `failure-contribution` — the capability an addon reports its OWN losses
13834
- * through, per camera, with the denominator attached. It stores nothing.
13835
- *
13836
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13837
- *
13838
- * `load-contribution` answers *what did this camera COST*. This answers *what
13839
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13840
- * copied: the contributor reports what it already knows, hub-main adds only
13841
- * `addonId`, nothing needs global knowledge, and there is no central list for
13842
- * somebody to forget to edit.
13843
- *
13844
- * They are not merged, because their invariants are opposites:
13845
- *
13846
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13847
- * claim a camera cost nothing, which is a measurement nobody made;
13848
- * - a `failure-contribution` zero is the **most valuable value on the
13849
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13850
- * and it is exactly what an absent entry cannot say.
13851
- *
13852
- * Putting a loss counter on a cost entry would also break the reconciliation
13853
- * that gives `load-contribution` its point: contributions are subtracted from
13854
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13855
- * has no process.
13856
- *
13857
- * ## Why not a log line, since the counters already exist
13858
- *
13859
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13860
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13861
- * ends in a log line, and a log line is the thing the operator asked to stop
13862
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13863
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13864
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13865
- * media blackout were both diagnosed. The counters stay; this is where they can
13866
- * be READ.
13867
- *
13868
- * ## The rate is served with its denominator or not at all
13869
- *
13870
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13871
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13872
- * than yesterday" and was **flat across twelve hours** once divided by the
13873
- * successes on the same path. A surface that publishes only the numerator
13874
- * reproduces that mistake on every read.
13875
- *
13876
- * ## Shape
13877
- *
13878
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13879
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13880
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13881
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13882
- * a forked runner's entries reach hub-main over transport that already exists.
13883
- * No new UDS message, no second registry (D3). The operator reads the assembled
13884
- * result through `system.getFailureContributions`.
13885
- */
13886
- var FailureReasonCountSchema = object({
13887
- /**
13888
- * Why the attempt did not land, in the contributor's own vocabulary —
13889
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13890
- * strings that already appear in this repo's logs and, where one exists, the
13891
- * same string the per-track `previewMissReason` records (D276): a second
13892
- * vocabulary for the same loss would make the row and the counter
13893
- * un-joinable.
13894
- */
13895
- reason: string(),
13896
- count: number().int().nonnegative()
13897
- });
13898
- var FailureContributionSchema = object({
13899
- /**
13900
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13901
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13902
- * `unit` free: the families are owned by different addons and a shared enum
13903
- * is a central list that rots invisibly.
13904
- */
13905
- family: string(),
13906
- /**
13907
- * The NUMERIC device id — the same value every log line carries as
13908
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13909
- * cannot name the camera must not emit the entry, because a fleet total
13910
- * cannot answer the only question anybody asks of this surface.
13911
- */
13912
- deviceId: number().int().positive(),
13913
- /**
13914
- * A second dimension inside the family: the model / step id for an inference
13915
- * timeout, so "which camera AND which model" is one read. Absent when the
13916
- * family has a single variant.
13917
- */
13918
- variant: string().optional(),
13919
- /**
13920
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13921
- * differencing two reads must drop the interval when it changes, because the
13922
- * counter restarted from zero in a respawned runner. Same discipline as
13923
- * `LoadContribution.startedAtMs`.
13924
- */
13925
- sinceMs: number(),
13926
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13927
- atMs: number(),
13928
- /**
13929
- * THE DENOMINATOR — every attempt on this path for this camera in the
13930
- * window. A failure count published without it is the mistake this schema
13931
- * exists to make impossible.
13932
- */
13933
- attempts: number().int().nonnegative(),
13934
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13935
- succeeded: number().int().nonnegative(),
13936
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13937
- reasons: array(FailureReasonCountSchema).readonly()
13938
- });
13939
- method(_void(), array(FailureContributionSchema).readonly());
13940
- var LoadContributionSchema = object({
13941
- role: _enum([
13942
- "decode",
13943
- "transcode",
13944
- "recording",
13945
- "streaming",
13946
- "detection"
13947
- ]),
13948
- /**
13949
- * The NUMERIC device id — the same value every log line carries as
13950
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13951
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13952
- * contributor that cannot name its camera must not emit the entry at all,
13953
- * because an unnamed per-camera entry is indistinguishable from a shared one
13954
- * and would quietly turn one camera's cost into everybody's.
13955
- */
13956
- deviceId: number().int().positive().nullable(),
13957
- attribution: _enum([
13958
- "measured",
13959
- "accounted",
13960
- "unattributable"
13961
- ]),
13962
- /**
13963
- * What ONE entry is, in the contributor's own words — `615/high`,
13964
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13965
- * family and inventing a common one would lose the only information that
13966
- * makes two entries for the same camera distinguishable.
13967
- */
13968
- unit: string(),
13969
- /**
13970
- * The OS process this cost lives in, when there is one. Present so a
13971
- * consumer can (a) tell two generations of the same unit apart across a
13972
- * restart, and (b) subtract claimed processes from the node's process
13973
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13974
- * process of its own.
13975
- */
13976
- pid: number().int().positive().optional(),
13977
- /**
13978
- * When this generation started. The pid's incarnation marker: a consumer
13979
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13980
- * window when this changes, because the counter restarted from zero in a new
13981
- * process.
13982
- */
13983
- startedAtMs: number().optional(),
13984
- /**
13985
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13986
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13987
- * contribution is asked for.
13988
- *
13989
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13990
- * needs a sampler, and a new per-node sampler is the defect half of
13991
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13992
- * by whoever already keeps a history; a rate cannot be un-averaged.
13993
- *
13994
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13995
- * an entry with no process.
13996
- */
13997
- cpuSeconds: number().optional(),
13998
- /** Resident bytes of this unit's process, same source and same rules. */
13999
- rssBytes: number().optional()
14000
- });
14001
- method(_void(), array(LoadContributionSchema).readonly());
14002
- /**
14003
14003
  * `login-method` — collection cap through which auth addons contribute
14004
14004
  * their pre-auth login surfaces to the login page. This is the SINGLE,
14005
14005
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18737,12 +18737,53 @@ var MediaFileKindEnum = _enum([
18737
18737
  "keyFrameSmall",
18738
18738
  "thumbnailSmall"
18739
18739
  ]);
18740
+ /**
18741
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18742
+ * ARE — never the bytes themselves.
18743
+ *
18744
+ * ## Why `url` and not `base64`
18745
+ *
18746
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18747
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18748
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18749
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18750
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18751
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18752
+ *
18753
+ * `url` points at the `event-media` data plane
18754
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18755
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18756
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18757
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18758
+ * no less protected than they were inside a `view`-level cap response — see
18759
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18760
+ * (per-device scoping).
18761
+ *
18762
+ * The URL is built from the row's **stored** key, which is not always its
18763
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18764
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18765
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18766
+ *
18767
+ * ## `base64` is TRANSITIONAL and is going away
18768
+ *
18769
+ * It is still populated for one reason: the deployed viewer's track-detail
18770
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18771
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18772
+ * triangle — not as absence. Removing the field before that viewer ships is an
18773
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18774
+ * delete this line and the `withBytes` pass-through in
18775
+ * `analytics-query-facade.ts`; nothing else reads it.
18776
+ */
18740
18777
  var MediaFileSchema = object({
18741
18778
  key: string(),
18742
18779
  kind: MediaFileKindEnum,
18743
- base64: string(),
18744
18780
  sizeBytes: number(),
18745
18781
  timestamp: number()
18782
+ }).extend({
18783
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18784
+ url: string(),
18785
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18786
+ base64: string()
18746
18787
  });
18747
18788
  /**
18748
18789
  * One media row WITHOUT its bytes.
@@ -18754,7 +18795,9 @@ var MediaFileSchema = object({
18754
18795
  * blocks the whole view.
18755
18796
  *
18756
18797
  * `sizeBytes` is carried because it is what lets a client decide between the
18757
- * stored blob and a `?variant=thumb` rendering without fetching either.
18798
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18799
+ * `url` because a client that had to build the plane path itself is a second
18800
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18758
18801
  */
18759
18802
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18760
18803
  /**
@@ -19101,6 +19144,50 @@ var EventStoreFootprintSchema = object({
19101
19144
  totalBytes: number().int(),
19102
19145
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19103
19146
  });
19147
+ /** Event-media footprint for one {@link MediaFileKind}. */
19148
+ var EventMediaKindFootprintSchema = object({
19149
+ kind: MediaFileKindEnum,
19150
+ /** Media rows of this kind. */
19151
+ rows: number().int(),
19152
+ /** Bytes on disk held by those rows. */
19153
+ bytes: number().int()
19154
+ });
19155
+ /**
19156
+ * The media footprint broken down by KIND — the axis a deletion decision
19157
+ * actually turns on.
19158
+ *
19159
+ * A byte total says how much there is; it cannot say what is safe to remove.
19160
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19161
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19162
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19163
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19164
+ * nothing else, so sizing a deletion means summing per kind.
19165
+ *
19166
+ * ## Why `unaccounted*` exists
19167
+ *
19168
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19169
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19170
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19171
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19172
+ * retired code path, or by a version that knew a kind this one does not) would
19173
+ * otherwise vanish from the total silently, and an operator would delete
19174
+ * against a denominator smaller than the disk.
19175
+ *
19176
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19177
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19178
+ */
19179
+ var EventMediaKindBreakdownSchema = object({
19180
+ /** Every media row in scope, from one unfiltered aggregate. */
19181
+ totalRows: number().int(),
19182
+ /** Every media byte in scope, from that same aggregate. */
19183
+ totalBytes: number().int(),
19184
+ /** Per-kind footprint, ordered by bytes descending. */
19185
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19186
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19187
+ unaccountedRows: number().int(),
19188
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19189
+ unaccountedBytes: number().int()
19190
+ });
19104
19191
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19105
19192
  var EventPruneCountsSchema = object({
19106
19193
  motion: number().int(),
@@ -19304,6 +19391,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19304
19391
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19305
19392
  kind: "query",
19306
19393
  auth: "admin"
19394
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19395
+ kind: "query",
19396
+ auth: "admin"
19307
19397
  }), method(object({
19308
19398
  olderThanMs: number(),
19309
19399
  reason: OpsLogReasonSchema.optional()
@@ -19443,6 +19533,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19443
19533
  }), array(MediaFileSchema).readonly()), method(object({
19444
19534
  trackId: string(),
19445
19535
  deviceId: number()
19536
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19537
+ eventId: string(),
19538
+ deviceId: number()
19446
19539
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19447
19540
  kind: "mutation",
19448
19541
  auth: "admin"
@@ -21356,6 +21449,20 @@ method(object({
21356
21449
  error: string().optional()
21357
21450
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21358
21451
  providerId: string(),
21452
+ /**
21453
+ * The location this config is an UNSAVED edit of, when there is one.
21454
+ *
21455
+ * `listLocations` replaces every declared secret with the redaction
21456
+ * sentinel, so the edit modal's form state holds the sentinel for any
21457
+ * credential the operator did not retype — and posting that here
21458
+ * without a way to resolve it makes the provider try to authenticate
21459
+ * as `__camstack_redacted__` and report the operator's own working
21460
+ * password as wrong. Given this id, the orchestrator restores each
21461
+ * sentinel from the stored config (same rule as `upsertLocation`)
21462
+ * before dispatching. Omitted by the "Add location" wizard, where
21463
+ * every value was typed just now and nothing is stored yet.
21464
+ */
21465
+ locationId: string().optional(),
21359
21466
  config: record(string(), unknown())
21360
21467
  }), object({
21361
21468
  ok: boolean(),
@@ -24545,10 +24652,24 @@ var FaceClusterSchema = object({
24545
24652
  size: number().int(),
24546
24653
  cohesion: number()
24547
24654
  });
24655
+ /**
24656
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24657
+ * are — never the bytes.
24658
+ *
24659
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24660
+ * track/event contract) is still populated because a deployed viewer requires
24661
+ * the field to parse a row at all; this method has no such reader. Its ONE
24662
+ * caller is the admin UI's detail modal, which was building
24663
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24664
+ * dialog already rendering its key FRAME from the `event-media` plane.
24665
+ *
24666
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24667
+ * media key directly, so this needed no new plane and no new access decision.
24668
+ */
24548
24669
  var MediaFileLiteSchema$1 = object({
24549
24670
  key: string(),
24550
24671
  kind: string(),
24551
- base64: string(),
24672
+ url: string(),
24552
24673
  sizeBytes: number(),
24553
24674
  timestamp: number()
24554
24675
  });
@@ -27570,10 +27691,24 @@ var PlateInfoSchema = object({
27570
27691
  */
27571
27692
  cropUrl: string().optional()
27572
27693
  });
27694
+ /**
27695
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27696
+ * are — never the bytes.
27697
+ *
27698
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27699
+ * track/event contract) is still populated because a deployed viewer requires
27700
+ * the field to parse a row at all; this method has no such reader. Its ONE
27701
+ * caller is the admin UI's detail modal, which was building
27702
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27703
+ * dialog already rendering its key FRAME from the `event-media` plane.
27704
+ *
27705
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27706
+ * media key directly, so this needed no new plane and no new access decision.
27707
+ */
27573
27708
  var MediaFileLiteSchema = object({
27574
27709
  key: string(),
27575
27710
  kind: string(),
27576
- base64: string(),
27711
+ url: string(),
27577
27712
  sizeBytes: number(),
27578
27713
  timestamp: number()
27579
27714
  });
@@ -35374,6 +35509,12 @@ Object.freeze({
35374
35509
  addonId: null,
35375
35510
  access: "view"
35376
35511
  },
35512
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
35513
+ capName: "pipeline-analytics",
35514
+ capScope: "device",
35515
+ addonId: null,
35516
+ access: "view"
35517
+ },
35377
35518
  "pipelineAnalytics.getEventStoreFootprint": {
35378
35519
  capName: "pipeline-analytics",
35379
35520
  capScope: "device",
@@ -35470,6 +35611,12 @@ Object.freeze({
35470
35611
  addonId: null,
35471
35612
  access: "view"
35472
35613
  },
35614
+ "pipelineAnalytics.listEventMedia": {
35615
+ capName: "pipeline-analytics",
35616
+ capScope: "device",
35617
+ addonId: null,
35618
+ access: "view"
35619
+ },
35473
35620
  "pipelineAnalytics.listGroups": {
35474
35621
  capName: "pipeline-analytics",
35475
35622
  capScope: "device",
@@ -39033,6 +39180,11 @@ Object.freeze({
39033
39180
  form: "single",
39034
39181
  optional: false
39035
39182
  }],
39183
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
39184
+ name: "deviceId",
39185
+ form: "single",
39186
+ optional: true
39187
+ }],
39036
39188
  "pipelineAnalytics.getGroup": [{
39037
39189
  name: "deviceId",
39038
39190
  form: "single",
@@ -39093,6 +39245,11 @@ Object.freeze({
39093
39245
  form: "array",
39094
39246
  optional: false
39095
39247
  }],
39248
+ "pipelineAnalytics.listEventMedia": [{
39249
+ name: "deviceId",
39250
+ form: "single",
39251
+ optional: false
39252
+ }],
39096
39253
  "pipelineAnalytics.listGroups": [{
39097
39254
  name: "deviceIds",
39098
39255
  form: "array",
package/dist/addon.mjs CHANGED
@@ -13212,6 +13212,114 @@ method(object({
13212
13212
  height: number()
13213
13213
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13214
13214
  /**
13215
+ * `failure-contribution` — the capability an addon reports its OWN losses
13216
+ * through, per camera, with the denominator attached. It stores nothing.
13217
+ *
13218
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13219
+ *
13220
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13221
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13222
+ * copied: the contributor reports what it already knows, hub-main adds only
13223
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13224
+ * somebody to forget to edit.
13225
+ *
13226
+ * They are not merged, because their invariants are opposites:
13227
+ *
13228
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13229
+ * claim a camera cost nothing, which is a measurement nobody made;
13230
+ * - a `failure-contribution` zero is the **most valuable value on the
13231
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13232
+ * and it is exactly what an absent entry cannot say.
13233
+ *
13234
+ * Putting a loss counter on a cost entry would also break the reconciliation
13235
+ * that gives `load-contribution` its point: contributions are subtracted from
13236
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13237
+ * has no process.
13238
+ *
13239
+ * ## Why not a log line, since the counters already exist
13240
+ *
13241
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13242
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13243
+ * ends in a log line, and a log line is the thing the operator asked to stop
13244
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13245
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13246
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13247
+ * media blackout were both diagnosed. The counters stay; this is where they can
13248
+ * be READ.
13249
+ *
13250
+ * ## The rate is served with its denominator or not at all
13251
+ *
13252
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13253
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13254
+ * than yesterday" and was **flat across twelve hours** once divided by the
13255
+ * successes on the same path. A surface that publishes only the numerator
13256
+ * reproduces that mistake on every read.
13257
+ *
13258
+ * ## Shape
13259
+ *
13260
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13261
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13262
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13263
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13264
+ * a forked runner's entries reach hub-main over transport that already exists.
13265
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13266
+ * result through `system.getFailureContributions`.
13267
+ */
13268
+ var FailureReasonCountSchema = object({
13269
+ /**
13270
+ * Why the attempt did not land, in the contributor's own vocabulary —
13271
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13272
+ * strings that already appear in this repo's logs and, where one exists, the
13273
+ * same string the per-track `previewMissReason` records (D276): a second
13274
+ * vocabulary for the same loss would make the row and the counter
13275
+ * un-joinable.
13276
+ */
13277
+ reason: string(),
13278
+ count: number().int().nonnegative()
13279
+ });
13280
+ var FailureContributionSchema = object({
13281
+ /**
13282
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13283
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13284
+ * `unit` free: the families are owned by different addons and a shared enum
13285
+ * is a central list that rots invisibly.
13286
+ */
13287
+ family: string(),
13288
+ /**
13289
+ * The NUMERIC device id — the same value every log line carries as
13290
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13291
+ * cannot name the camera must not emit the entry, because a fleet total
13292
+ * cannot answer the only question anybody asks of this surface.
13293
+ */
13294
+ deviceId: number().int().positive(),
13295
+ /**
13296
+ * A second dimension inside the family: the model / step id for an inference
13297
+ * timeout, so "which camera AND which model" is one read. Absent when the
13298
+ * family has a single variant.
13299
+ */
13300
+ variant: string().optional(),
13301
+ /**
13302
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13303
+ * differencing two reads must drop the interval when it changes, because the
13304
+ * counter restarted from zero in a respawned runner. Same discipline as
13305
+ * `LoadContribution.startedAtMs`.
13306
+ */
13307
+ sinceMs: number(),
13308
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13309
+ atMs: number(),
13310
+ /**
13311
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13312
+ * window. A failure count published without it is the mistake this schema
13313
+ * exists to make impossible.
13314
+ */
13315
+ attempts: number().int().nonnegative(),
13316
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13317
+ succeeded: number().int().nonnegative(),
13318
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13319
+ reasons: array(FailureReasonCountSchema).readonly()
13320
+ });
13321
+ method(_void(), array(FailureContributionSchema).readonly());
13322
+ /**
13215
13323
  * filesystem-browse — per-node capability for browsing the node's local
13216
13324
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13217
13325
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13733,6 +13841,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13733
13841
  kind: "mutation",
13734
13842
  auth: "admin"
13735
13843
  });
13844
+ var LoadContributionSchema = object({
13845
+ role: _enum([
13846
+ "decode",
13847
+ "transcode",
13848
+ "recording",
13849
+ "streaming",
13850
+ "detection"
13851
+ ]),
13852
+ /**
13853
+ * The NUMERIC device id — the same value every log line carries as
13854
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13855
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13856
+ * contributor that cannot name its camera must not emit the entry at all,
13857
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13858
+ * and would quietly turn one camera's cost into everybody's.
13859
+ */
13860
+ deviceId: number().int().positive().nullable(),
13861
+ attribution: _enum([
13862
+ "measured",
13863
+ "accounted",
13864
+ "unattributable"
13865
+ ]),
13866
+ /**
13867
+ * What ONE entry is, in the contributor's own words — `615/high`,
13868
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13869
+ * family and inventing a common one would lose the only information that
13870
+ * makes two entries for the same camera distinguishable.
13871
+ */
13872
+ unit: string(),
13873
+ /**
13874
+ * The OS process this cost lives in, when there is one. Present so a
13875
+ * consumer can (a) tell two generations of the same unit apart across a
13876
+ * restart, and (b) subtract claimed processes from the node's process
13877
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13878
+ * process of its own.
13879
+ */
13880
+ pid: number().int().positive().optional(),
13881
+ /**
13882
+ * When this generation started. The pid's incarnation marker: a consumer
13883
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13884
+ * window when this changes, because the counter restarted from zero in a new
13885
+ * process.
13886
+ */
13887
+ startedAtMs: number().optional(),
13888
+ /**
13889
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13890
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13891
+ * contribution is asked for.
13892
+ *
13893
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13894
+ * needs a sampler, and a new per-node sampler is the defect half of
13895
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13896
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13897
+ *
13898
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13899
+ * an entry with no process.
13900
+ */
13901
+ cpuSeconds: number().optional(),
13902
+ /** Resident bytes of this unit's process, same source and same rules. */
13903
+ rssBytes: number().optional()
13904
+ });
13905
+ method(_void(), array(LoadContributionSchema).readonly());
13736
13906
  /**
13737
13907
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13738
13908
  * through. It stores nothing.
@@ -13809,176 +13979,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13809
13979
  tags: record(string(), string()).optional()
13810
13980
  }), array(LogEntrySchema).readonly());
13811
13981
  /**
13812
- * `failure-contribution` — the capability an addon reports its OWN losses
13813
- * through, per camera, with the denominator attached. It stores nothing.
13814
- *
13815
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13816
- *
13817
- * `load-contribution` answers *what did this camera COST*. This answers *what
13818
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13819
- * copied: the contributor reports what it already knows, hub-main adds only
13820
- * `addonId`, nothing needs global knowledge, and there is no central list for
13821
- * somebody to forget to edit.
13822
- *
13823
- * They are not merged, because their invariants are opposites:
13824
- *
13825
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13826
- * claim a camera cost nothing, which is a measurement nobody made;
13827
- * - a `failure-contribution` zero is the **most valuable value on the
13828
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13829
- * and it is exactly what an absent entry cannot say.
13830
- *
13831
- * Putting a loss counter on a cost entry would also break the reconciliation
13832
- * that gives `load-contribution` its point: contributions are subtracted from
13833
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13834
- * has no process.
13835
- *
13836
- * ## Why not a log line, since the counters already exist
13837
- *
13838
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13839
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13840
- * ends in a log line, and a log line is the thing the operator asked to stop
13841
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13842
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13843
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13844
- * media blackout were both diagnosed. The counters stay; this is where they can
13845
- * be READ.
13846
- *
13847
- * ## The rate is served with its denominator or not at all
13848
- *
13849
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13850
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13851
- * than yesterday" and was **flat across twelve hours** once divided by the
13852
- * successes on the same path. A surface that publishes only the numerator
13853
- * reproduces that mistake on every read.
13854
- *
13855
- * ## Shape
13856
- *
13857
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13858
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13859
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13860
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13861
- * a forked runner's entries reach hub-main over transport that already exists.
13862
- * No new UDS message, no second registry (D3). The operator reads the assembled
13863
- * result through `system.getFailureContributions`.
13864
- */
13865
- var FailureReasonCountSchema = object({
13866
- /**
13867
- * Why the attempt did not land, in the contributor's own vocabulary —
13868
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13869
- * strings that already appear in this repo's logs and, where one exists, the
13870
- * same string the per-track `previewMissReason` records (D276): a second
13871
- * vocabulary for the same loss would make the row and the counter
13872
- * un-joinable.
13873
- */
13874
- reason: string(),
13875
- count: number().int().nonnegative()
13876
- });
13877
- var FailureContributionSchema = object({
13878
- /**
13879
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13880
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13881
- * `unit` free: the families are owned by different addons and a shared enum
13882
- * is a central list that rots invisibly.
13883
- */
13884
- family: string(),
13885
- /**
13886
- * The NUMERIC device id — the same value every log line carries as
13887
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13888
- * cannot name the camera must not emit the entry, because a fleet total
13889
- * cannot answer the only question anybody asks of this surface.
13890
- */
13891
- deviceId: number().int().positive(),
13892
- /**
13893
- * A second dimension inside the family: the model / step id for an inference
13894
- * timeout, so "which camera AND which model" is one read. Absent when the
13895
- * family has a single variant.
13896
- */
13897
- variant: string().optional(),
13898
- /**
13899
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13900
- * differencing two reads must drop the interval when it changes, because the
13901
- * counter restarted from zero in a respawned runner. Same discipline as
13902
- * `LoadContribution.startedAtMs`.
13903
- */
13904
- sinceMs: number(),
13905
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13906
- atMs: number(),
13907
- /**
13908
- * THE DENOMINATOR — every attempt on this path for this camera in the
13909
- * window. A failure count published without it is the mistake this schema
13910
- * exists to make impossible.
13911
- */
13912
- attempts: number().int().nonnegative(),
13913
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13914
- succeeded: number().int().nonnegative(),
13915
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13916
- reasons: array(FailureReasonCountSchema).readonly()
13917
- });
13918
- method(_void(), array(FailureContributionSchema).readonly());
13919
- var LoadContributionSchema = object({
13920
- role: _enum([
13921
- "decode",
13922
- "transcode",
13923
- "recording",
13924
- "streaming",
13925
- "detection"
13926
- ]),
13927
- /**
13928
- * The NUMERIC device id — the same value every log line carries as
13929
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13930
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13931
- * contributor that cannot name its camera must not emit the entry at all,
13932
- * because an unnamed per-camera entry is indistinguishable from a shared one
13933
- * and would quietly turn one camera's cost into everybody's.
13934
- */
13935
- deviceId: number().int().positive().nullable(),
13936
- attribution: _enum([
13937
- "measured",
13938
- "accounted",
13939
- "unattributable"
13940
- ]),
13941
- /**
13942
- * What ONE entry is, in the contributor's own words — `615/high`,
13943
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13944
- * family and inventing a common one would lose the only information that
13945
- * makes two entries for the same camera distinguishable.
13946
- */
13947
- unit: string(),
13948
- /**
13949
- * The OS process this cost lives in, when there is one. Present so a
13950
- * consumer can (a) tell two generations of the same unit apart across a
13951
- * restart, and (b) subtract claimed processes from the node's process
13952
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13953
- * process of its own.
13954
- */
13955
- pid: number().int().positive().optional(),
13956
- /**
13957
- * When this generation started. The pid's incarnation marker: a consumer
13958
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13959
- * window when this changes, because the counter restarted from zero in a new
13960
- * process.
13961
- */
13962
- startedAtMs: number().optional(),
13963
- /**
13964
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13965
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13966
- * contribution is asked for.
13967
- *
13968
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13969
- * needs a sampler, and a new per-node sampler is the defect half of
13970
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13971
- * by whoever already keeps a history; a rate cannot be un-averaged.
13972
- *
13973
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13974
- * an entry with no process.
13975
- */
13976
- cpuSeconds: number().optional(),
13977
- /** Resident bytes of this unit's process, same source and same rules. */
13978
- rssBytes: number().optional()
13979
- });
13980
- method(_void(), array(LoadContributionSchema).readonly());
13981
- /**
13982
13982
  * `login-method` — collection cap through which auth addons contribute
13983
13983
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13984
13984
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18716,12 +18716,53 @@ var MediaFileKindEnum = _enum([
18716
18716
  "keyFrameSmall",
18717
18717
  "thumbnailSmall"
18718
18718
  ]);
18719
+ /**
18720
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18721
+ * ARE — never the bytes themselves.
18722
+ *
18723
+ * ## Why `url` and not `base64`
18724
+ *
18725
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18726
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18727
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18728
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18729
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18730
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18731
+ *
18732
+ * `url` points at the `event-media` data plane
18733
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18734
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18735
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18736
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18737
+ * no less protected than they were inside a `view`-level cap response — see
18738
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18739
+ * (per-device scoping).
18740
+ *
18741
+ * The URL is built from the row's **stored** key, which is not always its
18742
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18743
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18744
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18745
+ *
18746
+ * ## `base64` is TRANSITIONAL and is going away
18747
+ *
18748
+ * It is still populated for one reason: the deployed viewer's track-detail
18749
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18750
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18751
+ * triangle — not as absence. Removing the field before that viewer ships is an
18752
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18753
+ * delete this line and the `withBytes` pass-through in
18754
+ * `analytics-query-facade.ts`; nothing else reads it.
18755
+ */
18719
18756
  var MediaFileSchema = object({
18720
18757
  key: string(),
18721
18758
  kind: MediaFileKindEnum,
18722
- base64: string(),
18723
18759
  sizeBytes: number(),
18724
18760
  timestamp: number()
18761
+ }).extend({
18762
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18763
+ url: string(),
18764
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18765
+ base64: string()
18725
18766
  });
18726
18767
  /**
18727
18768
  * One media row WITHOUT its bytes.
@@ -18733,7 +18774,9 @@ var MediaFileSchema = object({
18733
18774
  * blocks the whole view.
18734
18775
  *
18735
18776
  * `sizeBytes` is carried because it is what lets a client decide between the
18736
- * stored blob and a `?variant=thumb` rendering without fetching either.
18777
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18778
+ * `url` because a client that had to build the plane path itself is a second
18779
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18737
18780
  */
18738
18781
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18739
18782
  /**
@@ -19080,6 +19123,50 @@ var EventStoreFootprintSchema = object({
19080
19123
  totalBytes: number().int(),
19081
19124
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19082
19125
  });
19126
+ /** Event-media footprint for one {@link MediaFileKind}. */
19127
+ var EventMediaKindFootprintSchema = object({
19128
+ kind: MediaFileKindEnum,
19129
+ /** Media rows of this kind. */
19130
+ rows: number().int(),
19131
+ /** Bytes on disk held by those rows. */
19132
+ bytes: number().int()
19133
+ });
19134
+ /**
19135
+ * The media footprint broken down by KIND — the axis a deletion decision
19136
+ * actually turns on.
19137
+ *
19138
+ * A byte total says how much there is; it cannot say what is safe to remove.
19139
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19140
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19141
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19142
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19143
+ * nothing else, so sizing a deletion means summing per kind.
19144
+ *
19145
+ * ## Why `unaccounted*` exists
19146
+ *
19147
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19148
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19149
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19150
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19151
+ * retired code path, or by a version that knew a kind this one does not) would
19152
+ * otherwise vanish from the total silently, and an operator would delete
19153
+ * against a denominator smaller than the disk.
19154
+ *
19155
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19156
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19157
+ */
19158
+ var EventMediaKindBreakdownSchema = object({
19159
+ /** Every media row in scope, from one unfiltered aggregate. */
19160
+ totalRows: number().int(),
19161
+ /** Every media byte in scope, from that same aggregate. */
19162
+ totalBytes: number().int(),
19163
+ /** Per-kind footprint, ordered by bytes descending. */
19164
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19165
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19166
+ unaccountedRows: number().int(),
19167
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19168
+ unaccountedBytes: number().int()
19169
+ });
19083
19170
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19084
19171
  var EventPruneCountsSchema = object({
19085
19172
  motion: number().int(),
@@ -19283,6 +19370,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19283
19370
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19284
19371
  kind: "query",
19285
19372
  auth: "admin"
19373
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19374
+ kind: "query",
19375
+ auth: "admin"
19286
19376
  }), method(object({
19287
19377
  olderThanMs: number(),
19288
19378
  reason: OpsLogReasonSchema.optional()
@@ -19422,6 +19512,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19422
19512
  }), array(MediaFileSchema).readonly()), method(object({
19423
19513
  trackId: string(),
19424
19514
  deviceId: number()
19515
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19516
+ eventId: string(),
19517
+ deviceId: number()
19425
19518
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19426
19519
  kind: "mutation",
19427
19520
  auth: "admin"
@@ -21335,6 +21428,20 @@ method(object({
21335
21428
  error: string().optional()
21336
21429
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21337
21430
  providerId: string(),
21431
+ /**
21432
+ * The location this config is an UNSAVED edit of, when there is one.
21433
+ *
21434
+ * `listLocations` replaces every declared secret with the redaction
21435
+ * sentinel, so the edit modal's form state holds the sentinel for any
21436
+ * credential the operator did not retype — and posting that here
21437
+ * without a way to resolve it makes the provider try to authenticate
21438
+ * as `__camstack_redacted__` and report the operator's own working
21439
+ * password as wrong. Given this id, the orchestrator restores each
21440
+ * sentinel from the stored config (same rule as `upsertLocation`)
21441
+ * before dispatching. Omitted by the "Add location" wizard, where
21442
+ * every value was typed just now and nothing is stored yet.
21443
+ */
21444
+ locationId: string().optional(),
21338
21445
  config: record(string(), unknown())
21339
21446
  }), object({
21340
21447
  ok: boolean(),
@@ -24524,10 +24631,24 @@ var FaceClusterSchema = object({
24524
24631
  size: number().int(),
24525
24632
  cohesion: number()
24526
24633
  });
24634
+ /**
24635
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24636
+ * are — never the bytes.
24637
+ *
24638
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24639
+ * track/event contract) is still populated because a deployed viewer requires
24640
+ * the field to parse a row at all; this method has no such reader. Its ONE
24641
+ * caller is the admin UI's detail modal, which was building
24642
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24643
+ * dialog already rendering its key FRAME from the `event-media` plane.
24644
+ *
24645
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24646
+ * media key directly, so this needed no new plane and no new access decision.
24647
+ */
24527
24648
  var MediaFileLiteSchema$1 = object({
24528
24649
  key: string(),
24529
24650
  kind: string(),
24530
- base64: string(),
24651
+ url: string(),
24531
24652
  sizeBytes: number(),
24532
24653
  timestamp: number()
24533
24654
  });
@@ -27549,10 +27670,24 @@ var PlateInfoSchema = object({
27549
27670
  */
27550
27671
  cropUrl: string().optional()
27551
27672
  });
27673
+ /**
27674
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27675
+ * are — never the bytes.
27676
+ *
27677
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27678
+ * track/event contract) is still populated because a deployed viewer requires
27679
+ * the field to parse a row at all; this method has no such reader. Its ONE
27680
+ * caller is the admin UI's detail modal, which was building
27681
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27682
+ * dialog already rendering its key FRAME from the `event-media` plane.
27683
+ *
27684
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27685
+ * media key directly, so this needed no new plane and no new access decision.
27686
+ */
27552
27687
  var MediaFileLiteSchema = object({
27553
27688
  key: string(),
27554
27689
  kind: string(),
27555
- base64: string(),
27690
+ url: string(),
27556
27691
  sizeBytes: number(),
27557
27692
  timestamp: number()
27558
27693
  });
@@ -35353,6 +35488,12 @@ Object.freeze({
35353
35488
  addonId: null,
35354
35489
  access: "view"
35355
35490
  },
35491
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
35492
+ capName: "pipeline-analytics",
35493
+ capScope: "device",
35494
+ addonId: null,
35495
+ access: "view"
35496
+ },
35356
35497
  "pipelineAnalytics.getEventStoreFootprint": {
35357
35498
  capName: "pipeline-analytics",
35358
35499
  capScope: "device",
@@ -35449,6 +35590,12 @@ Object.freeze({
35449
35590
  addonId: null,
35450
35591
  access: "view"
35451
35592
  },
35593
+ "pipelineAnalytics.listEventMedia": {
35594
+ capName: "pipeline-analytics",
35595
+ capScope: "device",
35596
+ addonId: null,
35597
+ access: "view"
35598
+ },
35452
35599
  "pipelineAnalytics.listGroups": {
35453
35600
  capName: "pipeline-analytics",
35454
35601
  capScope: "device",
@@ -39012,6 +39159,11 @@ Object.freeze({
39012
39159
  form: "single",
39013
39160
  optional: false
39014
39161
  }],
39162
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
39163
+ name: "deviceId",
39164
+ form: "single",
39165
+ optional: true
39166
+ }],
39015
39167
  "pipelineAnalytics.getGroup": [{
39016
39168
  name: "deviceId",
39017
39169
  form: "single",
@@ -39072,6 +39224,11 @@ Object.freeze({
39072
39224
  form: "array",
39073
39225
  optional: false
39074
39226
  }],
39227
+ "pipelineAnalytics.listEventMedia": [{
39228
+ name: "deviceId",
39229
+ form: "single",
39230
+ optional: false
39231
+ }],
39075
39232
  "pipelineAnalytics.listGroups": [{
39076
39233
  name: "deviceIds",
39077
39234
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-wyze",
3
- "version": "0.2.50",
3
+ "version": "0.2.52",
4
4
  "description": "Wyze camera device-provider addon for CamStack — wraps the @apocaliss92/wyze-bridge-js P2P/DTLS client, feeding the stream-broker via the pull-rfc4571 lazy-publish path (a structural twin of addon-provider-reolink)",
5
5
  "keywords": [
6
6
  "camstack",