@camstack/addon-export-hap 1.2.57 → 1.2.59

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.
@@ -13721,6 +13721,114 @@ method(object({
13721
13721
  height: number()
13722
13722
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13723
13723
  /**
13724
+ * `failure-contribution` — the capability an addon reports its OWN losses
13725
+ * through, per camera, with the denominator attached. It stores nothing.
13726
+ *
13727
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13728
+ *
13729
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13730
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13731
+ * copied: the contributor reports what it already knows, hub-main adds only
13732
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13733
+ * somebody to forget to edit.
13734
+ *
13735
+ * They are not merged, because their invariants are opposites:
13736
+ *
13737
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13738
+ * claim a camera cost nothing, which is a measurement nobody made;
13739
+ * - a `failure-contribution` zero is the **most valuable value on the
13740
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13741
+ * and it is exactly what an absent entry cannot say.
13742
+ *
13743
+ * Putting a loss counter on a cost entry would also break the reconciliation
13744
+ * that gives `load-contribution` its point: contributions are subtracted from
13745
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13746
+ * has no process.
13747
+ *
13748
+ * ## Why not a log line, since the counters already exist
13749
+ *
13750
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13751
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13752
+ * ends in a log line, and a log line is the thing the operator asked to stop
13753
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13754
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13755
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13756
+ * media blackout were both diagnosed. The counters stay; this is where they can
13757
+ * be READ.
13758
+ *
13759
+ * ## The rate is served with its denominator or not at all
13760
+ *
13761
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13762
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13763
+ * than yesterday" and was **flat across twelve hours** once divided by the
13764
+ * successes on the same path. A surface that publishes only the numerator
13765
+ * reproduces that mistake on every read.
13766
+ *
13767
+ * ## Shape
13768
+ *
13769
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13770
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13771
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13772
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13773
+ * a forked runner's entries reach hub-main over transport that already exists.
13774
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13775
+ * result through `system.getFailureContributions`.
13776
+ */
13777
+ var FailureReasonCountSchema = object({
13778
+ /**
13779
+ * Why the attempt did not land, in the contributor's own vocabulary —
13780
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13781
+ * strings that already appear in this repo's logs and, where one exists, the
13782
+ * same string the per-track `previewMissReason` records (D276): a second
13783
+ * vocabulary for the same loss would make the row and the counter
13784
+ * un-joinable.
13785
+ */
13786
+ reason: string(),
13787
+ count: number().int().nonnegative()
13788
+ });
13789
+ var FailureContributionSchema = object({
13790
+ /**
13791
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13792
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13793
+ * `unit` free: the families are owned by different addons and a shared enum
13794
+ * is a central list that rots invisibly.
13795
+ */
13796
+ family: string(),
13797
+ /**
13798
+ * The NUMERIC device id — the same value every log line carries as
13799
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13800
+ * cannot name the camera must not emit the entry, because a fleet total
13801
+ * cannot answer the only question anybody asks of this surface.
13802
+ */
13803
+ deviceId: number().int().positive(),
13804
+ /**
13805
+ * A second dimension inside the family: the model / step id for an inference
13806
+ * timeout, so "which camera AND which model" is one read. Absent when the
13807
+ * family has a single variant.
13808
+ */
13809
+ variant: string().optional(),
13810
+ /**
13811
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13812
+ * differencing two reads must drop the interval when it changes, because the
13813
+ * counter restarted from zero in a respawned runner. Same discipline as
13814
+ * `LoadContribution.startedAtMs`.
13815
+ */
13816
+ sinceMs: number(),
13817
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13818
+ atMs: number(),
13819
+ /**
13820
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13821
+ * window. A failure count published without it is the mistake this schema
13822
+ * exists to make impossible.
13823
+ */
13824
+ attempts: number().int().nonnegative(),
13825
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13826
+ succeeded: number().int().nonnegative(),
13827
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13828
+ reasons: array(FailureReasonCountSchema).readonly()
13829
+ });
13830
+ method(_void(), array(FailureContributionSchema).readonly());
13831
+ /**
13724
13832
  * filesystem-browse — per-node capability for browsing the node's local
13725
13833
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13726
13834
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -14242,6 +14350,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
14242
14350
  kind: "mutation",
14243
14351
  auth: "admin"
14244
14352
  });
14353
+ var LoadContributionSchema = object({
14354
+ role: _enum([
14355
+ "decode",
14356
+ "transcode",
14357
+ "recording",
14358
+ "streaming",
14359
+ "detection"
14360
+ ]),
14361
+ /**
14362
+ * The NUMERIC device id — the same value every log line carries as
14363
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14364
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
14365
+ * contributor that cannot name its camera must not emit the entry at all,
14366
+ * because an unnamed per-camera entry is indistinguishable from a shared one
14367
+ * and would quietly turn one camera's cost into everybody's.
14368
+ */
14369
+ deviceId: number().int().positive().nullable(),
14370
+ attribution: _enum([
14371
+ "measured",
14372
+ "accounted",
14373
+ "unattributable"
14374
+ ]),
14375
+ /**
14376
+ * What ONE entry is, in the contributor's own words — `615/high`,
14377
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14378
+ * family and inventing a common one would lose the only information that
14379
+ * makes two entries for the same camera distinguishable.
14380
+ */
14381
+ unit: string(),
14382
+ /**
14383
+ * The OS process this cost lives in, when there is one. Present so a
14384
+ * consumer can (a) tell two generations of the same unit apart across a
14385
+ * restart, and (b) subtract claimed processes from the node's process
14386
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14387
+ * process of its own.
14388
+ */
14389
+ pid: number().int().positive().optional(),
14390
+ /**
14391
+ * When this generation started. The pid's incarnation marker: a consumer
14392
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14393
+ * window when this changes, because the counter restarted from zero in a new
14394
+ * process.
14395
+ */
14396
+ startedAtMs: number().optional(),
14397
+ /**
14398
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14399
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
14400
+ * contribution is asked for.
14401
+ *
14402
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
14403
+ * needs a sampler, and a new per-node sampler is the defect half of
14404
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14405
+ * by whoever already keeps a history; a rate cannot be un-averaged.
14406
+ *
14407
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14408
+ * an entry with no process.
14409
+ */
14410
+ cpuSeconds: number().optional(),
14411
+ /** Resident bytes of this unit's process, same source and same rules. */
14412
+ rssBytes: number().optional()
14413
+ });
14414
+ method(_void(), array(LoadContributionSchema).readonly());
14245
14415
  /**
14246
14416
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
14247
14417
  * through. It stores nothing.
@@ -14318,176 +14488,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
14318
14488
  tags: record(string(), string()).optional()
14319
14489
  }), array(LogEntrySchema).readonly());
14320
14490
  /**
14321
- * `failure-contribution` — the capability an addon reports its OWN losses
14322
- * through, per camera, with the denominator attached. It stores nothing.
14323
- *
14324
- * ## The twin of `load-contribution`, and why it is a twin and not a field
14325
- *
14326
- * `load-contribution` answers *what did this camera COST*. This answers *what
14327
- * did this camera LOSE*. The reporting discipline is identical and deliberately
14328
- * copied: the contributor reports what it already knows, hub-main adds only
14329
- * `addonId`, nothing needs global knowledge, and there is no central list for
14330
- * somebody to forget to edit.
14331
- *
14332
- * They are not merged, because their invariants are opposites:
14333
- *
14334
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
14335
- * claim a camera cost nothing, which is a measurement nobody made;
14336
- * - a `failure-contribution` zero is the **most valuable value on the
14337
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14338
- * and it is exactly what an absent entry cannot say.
14339
- *
14340
- * Putting a loss counter on a cost entry would also break the reconciliation
14341
- * that gives `load-contribution` its point: contributions are subtracted from
14342
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14343
- * has no process.
14344
- *
14345
- * ## Why not a log line, since the counters already exist
14346
- *
14347
- * Several of these paths already counted themselves — `CaptureScheduler`'s
14348
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14349
- * ends in a log line, and a log line is the thing the operator asked to stop
14350
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14351
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14352
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14353
- * media blackout were both diagnosed. The counters stay; this is where they can
14354
- * be READ.
14355
- *
14356
- * ## The rate is served with its denominator or not at all
14357
- *
14358
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
14359
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14360
- * than yesterday" and was **flat across twelve hours** once divided by the
14361
- * successes on the same path. A surface that publishes only the numerator
14362
- * reproduces that mistake on every read.
14363
- *
14364
- * ## Shape
14365
- *
14366
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14367
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14368
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14369
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14370
- * a forked runner's entries reach hub-main over transport that already exists.
14371
- * No new UDS message, no second registry (D3). The operator reads the assembled
14372
- * result through `system.getFailureContributions`.
14373
- */
14374
- var FailureReasonCountSchema = object({
14375
- /**
14376
- * Why the attempt did not land, in the contributor's own vocabulary —
14377
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14378
- * strings that already appear in this repo's logs and, where one exists, the
14379
- * same string the per-track `previewMissReason` records (D276): a second
14380
- * vocabulary for the same loss would make the row and the counter
14381
- * un-joinable.
14382
- */
14383
- reason: string(),
14384
- count: number().int().nonnegative()
14385
- });
14386
- var FailureContributionSchema = object({
14387
- /**
14388
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14389
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14390
- * `unit` free: the families are owned by different addons and a shared enum
14391
- * is a central list that rots invisibly.
14392
- */
14393
- family: string(),
14394
- /**
14395
- * The NUMERIC device id — the same value every log line carries as
14396
- * `tags.deviceId`. Never nullable and never absent: a contributor that
14397
- * cannot name the camera must not emit the entry, because a fleet total
14398
- * cannot answer the only question anybody asks of this surface.
14399
- */
14400
- deviceId: number().int().positive(),
14401
- /**
14402
- * A second dimension inside the family: the model / step id for an inference
14403
- * timeout, so "which camera AND which model" is one read. Absent when the
14404
- * family has a single variant.
14405
- */
14406
- variant: string().optional(),
14407
- /**
14408
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14409
- * differencing two reads must drop the interval when it changes, because the
14410
- * counter restarted from zero in a respawned runner. Same discipline as
14411
- * `LoadContribution.startedAtMs`.
14412
- */
14413
- sinceMs: number(),
14414
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14415
- atMs: number(),
14416
- /**
14417
- * THE DENOMINATOR — every attempt on this path for this camera in the
14418
- * window. A failure count published without it is the mistake this schema
14419
- * exists to make impossible.
14420
- */
14421
- attempts: number().int().nonnegative(),
14422
- /** Attempts that landed. `attempts - succeeded` is the loss. */
14423
- succeeded: number().int().nonnegative(),
14424
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
14425
- reasons: array(FailureReasonCountSchema).readonly()
14426
- });
14427
- method(_void(), array(FailureContributionSchema).readonly());
14428
- var LoadContributionSchema = object({
14429
- role: _enum([
14430
- "decode",
14431
- "transcode",
14432
- "recording",
14433
- "streaming",
14434
- "detection"
14435
- ]),
14436
- /**
14437
- * The NUMERIC device id — the same value every log line carries as
14438
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14439
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
14440
- * contributor that cannot name its camera must not emit the entry at all,
14441
- * because an unnamed per-camera entry is indistinguishable from a shared one
14442
- * and would quietly turn one camera's cost into everybody's.
14443
- */
14444
- deviceId: number().int().positive().nullable(),
14445
- attribution: _enum([
14446
- "measured",
14447
- "accounted",
14448
- "unattributable"
14449
- ]),
14450
- /**
14451
- * What ONE entry is, in the contributor's own words — `615/high`,
14452
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14453
- * family and inventing a common one would lose the only information that
14454
- * makes two entries for the same camera distinguishable.
14455
- */
14456
- unit: string(),
14457
- /**
14458
- * The OS process this cost lives in, when there is one. Present so a
14459
- * consumer can (a) tell two generations of the same unit apart across a
14460
- * restart, and (b) subtract claimed processes from the node's process
14461
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14462
- * process of its own.
14463
- */
14464
- pid: number().int().positive().optional(),
14465
- /**
14466
- * When this generation started. The pid's incarnation marker: a consumer
14467
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14468
- * window when this changes, because the counter restarted from zero in a new
14469
- * process.
14470
- */
14471
- startedAtMs: number().optional(),
14472
- /**
14473
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14474
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
14475
- * contribution is asked for.
14476
- *
14477
- * Cumulative and not a rate on purpose: a rate needs a window, a window
14478
- * needs a sampler, and a new per-node sampler is the defect half of
14479
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14480
- * by whoever already keeps a history; a rate cannot be un-averaged.
14481
- *
14482
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14483
- * an entry with no process.
14484
- */
14485
- cpuSeconds: number().optional(),
14486
- /** Resident bytes of this unit's process, same source and same rules. */
14487
- rssBytes: number().optional()
14488
- });
14489
- method(_void(), array(LoadContributionSchema).readonly());
14490
- /**
14491
14491
  * `login-method` — collection cap through which auth addons contribute
14492
14492
  * their pre-auth login surfaces to the login page. This is the SINGLE,
14493
14493
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -19147,12 +19147,53 @@ var MediaFileKindEnum = _enum([
19147
19147
  "keyFrameSmall",
19148
19148
  "thumbnailSmall"
19149
19149
  ]);
19150
+ /**
19151
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
19152
+ * ARE — never the bytes themselves.
19153
+ *
19154
+ * ## Why `url` and not `base64`
19155
+ *
19156
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
19157
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
19158
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
19159
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
19160
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
19161
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
19162
+ *
19163
+ * `url` points at the `event-media` data plane
19164
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
19165
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
19166
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
19167
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
19168
+ * no less protected than they were inside a `view`-level cap response — see
19169
+ * `data-plane-access.ts` for the rule and the one gap it does not close
19170
+ * (per-device scoping).
19171
+ *
19172
+ * The URL is built from the row's **stored** key, which is not always its
19173
+ * published `kind`: a track's face/plate crop is stored as `crop` under
19174
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
19175
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
19176
+ *
19177
+ * ## `base64` is TRANSITIONAL and is going away
19178
+ *
19179
+ * It is still populated for one reason: the deployed viewer's track-detail
19180
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
19181
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
19182
+ * triangle — not as absence. Removing the field before that viewer ships is an
19183
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
19184
+ * delete this line and the `withBytes` pass-through in
19185
+ * `analytics-query-facade.ts`; nothing else reads it.
19186
+ */
19150
19187
  var MediaFileSchema = object({
19151
19188
  key: string(),
19152
19189
  kind: MediaFileKindEnum,
19153
- base64: string(),
19154
19190
  sizeBytes: number(),
19155
19191
  timestamp: number()
19192
+ }).extend({
19193
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
19194
+ url: string(),
19195
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
19196
+ base64: string()
19156
19197
  });
19157
19198
  /**
19158
19199
  * One media row WITHOUT its bytes.
@@ -19164,7 +19205,9 @@ var MediaFileSchema = object({
19164
19205
  * blocks the whole view.
19165
19206
  *
19166
19207
  * `sizeBytes` is carried because it is what lets a client decide between the
19167
- * stored blob and a `?variant=thumb` rendering without fetching either.
19208
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
19209
+ * `url` because a client that had to build the plane path itself is a second
19210
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
19168
19211
  */
19169
19212
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
19170
19213
  /**
@@ -19511,6 +19554,50 @@ var EventStoreFootprintSchema = object({
19511
19554
  totalBytes: number().int(),
19512
19555
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19513
19556
  });
19557
+ /** Event-media footprint for one {@link MediaFileKind}. */
19558
+ var EventMediaKindFootprintSchema = object({
19559
+ kind: MediaFileKindEnum,
19560
+ /** Media rows of this kind. */
19561
+ rows: number().int(),
19562
+ /** Bytes on disk held by those rows. */
19563
+ bytes: number().int()
19564
+ });
19565
+ /**
19566
+ * The media footprint broken down by KIND — the axis a deletion decision
19567
+ * actually turns on.
19568
+ *
19569
+ * A byte total says how much there is; it cannot say what is safe to remove.
19570
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19571
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19572
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19573
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19574
+ * nothing else, so sizing a deletion means summing per kind.
19575
+ *
19576
+ * ## Why `unaccounted*` exists
19577
+ *
19578
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19579
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19580
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19581
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19582
+ * retired code path, or by a version that knew a kind this one does not) would
19583
+ * otherwise vanish from the total silently, and an operator would delete
19584
+ * against a denominator smaller than the disk.
19585
+ *
19586
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19587
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19588
+ */
19589
+ var EventMediaKindBreakdownSchema = object({
19590
+ /** Every media row in scope, from one unfiltered aggregate. */
19591
+ totalRows: number().int(),
19592
+ /** Every media byte in scope, from that same aggregate. */
19593
+ totalBytes: number().int(),
19594
+ /** Per-kind footprint, ordered by bytes descending. */
19595
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19596
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19597
+ unaccountedRows: number().int(),
19598
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19599
+ unaccountedBytes: number().int()
19600
+ });
19514
19601
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19515
19602
  var EventPruneCountsSchema = object({
19516
19603
  motion: number().int(),
@@ -19714,6 +19801,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19714
19801
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19715
19802
  kind: "query",
19716
19803
  auth: "admin"
19804
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19805
+ kind: "query",
19806
+ auth: "admin"
19717
19807
  }), method(object({
19718
19808
  olderThanMs: number(),
19719
19809
  reason: OpsLogReasonSchema.optional()
@@ -19853,6 +19943,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19853
19943
  }), array(MediaFileSchema).readonly()), method(object({
19854
19944
  trackId: string(),
19855
19945
  deviceId: number()
19946
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19947
+ eventId: string(),
19948
+ deviceId: number()
19856
19949
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19857
19950
  kind: "mutation",
19858
19951
  auth: "admin"
@@ -21662,6 +21755,20 @@ method(object({
21662
21755
  error: string().optional()
21663
21756
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21664
21757
  providerId: string(),
21758
+ /**
21759
+ * The location this config is an UNSAVED edit of, when there is one.
21760
+ *
21761
+ * `listLocations` replaces every declared secret with the redaction
21762
+ * sentinel, so the edit modal's form state holds the sentinel for any
21763
+ * credential the operator did not retype — and posting that here
21764
+ * without a way to resolve it makes the provider try to authenticate
21765
+ * as `__camstack_redacted__` and report the operator's own working
21766
+ * password as wrong. Given this id, the orchestrator restores each
21767
+ * sentinel from the stored config (same rule as `upsertLocation`)
21768
+ * before dispatching. Omitted by the "Add location" wizard, where
21769
+ * every value was typed just now and nothing is stored yet.
21770
+ */
21771
+ locationId: string().optional(),
21665
21772
  config: record(string(), unknown())
21666
21773
  }), object({
21667
21774
  ok: boolean(),
@@ -24133,10 +24240,24 @@ var FaceClusterSchema = object({
24133
24240
  size: number().int(),
24134
24241
  cohesion: number()
24135
24242
  });
24243
+ /**
24244
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24245
+ * are — never the bytes.
24246
+ *
24247
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24248
+ * track/event contract) is still populated because a deployed viewer requires
24249
+ * the field to parse a row at all; this method has no such reader. Its ONE
24250
+ * caller is the admin UI's detail modal, which was building
24251
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24252
+ * dialog already rendering its key FRAME from the `event-media` plane.
24253
+ *
24254
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24255
+ * media key directly, so this needed no new plane and no new access decision.
24256
+ */
24136
24257
  var MediaFileLiteSchema$1 = object({
24137
24258
  key: string(),
24138
24259
  kind: string(),
24139
- base64: string(),
24260
+ url: string(),
24140
24261
  sizeBytes: number(),
24141
24262
  timestamp: number()
24142
24263
  });
@@ -26416,10 +26537,24 @@ var PlateInfoSchema = object({
26416
26537
  */
26417
26538
  cropUrl: string().optional()
26418
26539
  });
26540
+ /**
26541
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
26542
+ * are — never the bytes.
26543
+ *
26544
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
26545
+ * track/event contract) is still populated because a deployed viewer requires
26546
+ * the field to parse a row at all; this method has no such reader. Its ONE
26547
+ * caller is the admin UI's detail modal, which was building
26548
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
26549
+ * dialog already rendering its key FRAME from the `event-media` plane.
26550
+ *
26551
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
26552
+ * media key directly, so this needed no new plane and no new access decision.
26553
+ */
26419
26554
  var MediaFileLiteSchema = object({
26420
26555
  key: string(),
26421
26556
  kind: string(),
26422
- base64: string(),
26557
+ url: string(),
26423
26558
  sizeBytes: number(),
26424
26559
  timestamp: number()
26425
26560
  });
@@ -32361,6 +32496,12 @@ Object.freeze({
32361
32496
  addonId: null,
32362
32497
  access: "view"
32363
32498
  },
32499
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
32500
+ capName: "pipeline-analytics",
32501
+ capScope: "device",
32502
+ addonId: null,
32503
+ access: "view"
32504
+ },
32364
32505
  "pipelineAnalytics.getEventStoreFootprint": {
32365
32506
  capName: "pipeline-analytics",
32366
32507
  capScope: "device",
@@ -32457,6 +32598,12 @@ Object.freeze({
32457
32598
  addonId: null,
32458
32599
  access: "view"
32459
32600
  },
32601
+ "pipelineAnalytics.listEventMedia": {
32602
+ capName: "pipeline-analytics",
32603
+ capScope: "device",
32604
+ addonId: null,
32605
+ access: "view"
32606
+ },
32460
32607
  "pipelineAnalytics.listGroups": {
32461
32608
  capName: "pipeline-analytics",
32462
32609
  capScope: "device",
@@ -36020,6 +36167,11 @@ Object.freeze({
36020
36167
  form: "single",
36021
36168
  optional: false
36022
36169
  }],
36170
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
36171
+ name: "deviceId",
36172
+ form: "single",
36173
+ optional: true
36174
+ }],
36023
36175
  "pipelineAnalytics.getGroup": [{
36024
36176
  name: "deviceId",
36025
36177
  form: "single",
@@ -36080,6 +36232,11 @@ Object.freeze({
36080
36232
  form: "array",
36081
36233
  optional: false
36082
36234
  }],
36235
+ "pipelineAnalytics.listEventMedia": [{
36236
+ name: "deviceId",
36237
+ form: "single",
36238
+ optional: false
36239
+ }],
36083
36240
  "pipelineAnalytics.listGroups": [{
36084
36241
  name: "deviceIds",
36085
36242
  form: "array",
@@ -13709,6 +13709,114 @@ method(object({
13709
13709
  height: number()
13710
13710
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13711
13711
  /**
13712
+ * `failure-contribution` — the capability an addon reports its OWN losses
13713
+ * through, per camera, with the denominator attached. It stores nothing.
13714
+ *
13715
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13716
+ *
13717
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13718
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13719
+ * copied: the contributor reports what it already knows, hub-main adds only
13720
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13721
+ * somebody to forget to edit.
13722
+ *
13723
+ * They are not merged, because their invariants are opposites:
13724
+ *
13725
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13726
+ * claim a camera cost nothing, which is a measurement nobody made;
13727
+ * - a `failure-contribution` zero is the **most valuable value on the
13728
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13729
+ * and it is exactly what an absent entry cannot say.
13730
+ *
13731
+ * Putting a loss counter on a cost entry would also break the reconciliation
13732
+ * that gives `load-contribution` its point: contributions are subtracted from
13733
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13734
+ * has no process.
13735
+ *
13736
+ * ## Why not a log line, since the counters already exist
13737
+ *
13738
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13739
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13740
+ * ends in a log line, and a log line is the thing the operator asked to stop
13741
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13742
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13743
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13744
+ * media blackout were both diagnosed. The counters stay; this is where they can
13745
+ * be READ.
13746
+ *
13747
+ * ## The rate is served with its denominator or not at all
13748
+ *
13749
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13750
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13751
+ * than yesterday" and was **flat across twelve hours** once divided by the
13752
+ * successes on the same path. A surface that publishes only the numerator
13753
+ * reproduces that mistake on every read.
13754
+ *
13755
+ * ## Shape
13756
+ *
13757
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13758
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13759
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13760
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13761
+ * a forked runner's entries reach hub-main over transport that already exists.
13762
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13763
+ * result through `system.getFailureContributions`.
13764
+ */
13765
+ var FailureReasonCountSchema = object({
13766
+ /**
13767
+ * Why the attempt did not land, in the contributor's own vocabulary —
13768
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13769
+ * strings that already appear in this repo's logs and, where one exists, the
13770
+ * same string the per-track `previewMissReason` records (D276): a second
13771
+ * vocabulary for the same loss would make the row and the counter
13772
+ * un-joinable.
13773
+ */
13774
+ reason: string(),
13775
+ count: number().int().nonnegative()
13776
+ });
13777
+ var FailureContributionSchema = object({
13778
+ /**
13779
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13780
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13781
+ * `unit` free: the families are owned by different addons and a shared enum
13782
+ * is a central list that rots invisibly.
13783
+ */
13784
+ family: string(),
13785
+ /**
13786
+ * The NUMERIC device id — the same value every log line carries as
13787
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13788
+ * cannot name the camera must not emit the entry, because a fleet total
13789
+ * cannot answer the only question anybody asks of this surface.
13790
+ */
13791
+ deviceId: number().int().positive(),
13792
+ /**
13793
+ * A second dimension inside the family: the model / step id for an inference
13794
+ * timeout, so "which camera AND which model" is one read. Absent when the
13795
+ * family has a single variant.
13796
+ */
13797
+ variant: string().optional(),
13798
+ /**
13799
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13800
+ * differencing two reads must drop the interval when it changes, because the
13801
+ * counter restarted from zero in a respawned runner. Same discipline as
13802
+ * `LoadContribution.startedAtMs`.
13803
+ */
13804
+ sinceMs: number(),
13805
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13806
+ atMs: number(),
13807
+ /**
13808
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13809
+ * window. A failure count published without it is the mistake this schema
13810
+ * exists to make impossible.
13811
+ */
13812
+ attempts: number().int().nonnegative(),
13813
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13814
+ succeeded: number().int().nonnegative(),
13815
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13816
+ reasons: array(FailureReasonCountSchema).readonly()
13817
+ });
13818
+ method(_void(), array(FailureContributionSchema).readonly());
13819
+ /**
13712
13820
  * filesystem-browse — per-node capability for browsing the node's local
13713
13821
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13714
13822
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -14230,6 +14338,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
14230
14338
  kind: "mutation",
14231
14339
  auth: "admin"
14232
14340
  });
14341
+ var LoadContributionSchema = object({
14342
+ role: _enum([
14343
+ "decode",
14344
+ "transcode",
14345
+ "recording",
14346
+ "streaming",
14347
+ "detection"
14348
+ ]),
14349
+ /**
14350
+ * The NUMERIC device id — the same value every log line carries as
14351
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14352
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
14353
+ * contributor that cannot name its camera must not emit the entry at all,
14354
+ * because an unnamed per-camera entry is indistinguishable from a shared one
14355
+ * and would quietly turn one camera's cost into everybody's.
14356
+ */
14357
+ deviceId: number().int().positive().nullable(),
14358
+ attribution: _enum([
14359
+ "measured",
14360
+ "accounted",
14361
+ "unattributable"
14362
+ ]),
14363
+ /**
14364
+ * What ONE entry is, in the contributor's own words — `615/high`,
14365
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14366
+ * family and inventing a common one would lose the only information that
14367
+ * makes two entries for the same camera distinguishable.
14368
+ */
14369
+ unit: string(),
14370
+ /**
14371
+ * The OS process this cost lives in, when there is one. Present so a
14372
+ * consumer can (a) tell two generations of the same unit apart across a
14373
+ * restart, and (b) subtract claimed processes from the node's process
14374
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14375
+ * process of its own.
14376
+ */
14377
+ pid: number().int().positive().optional(),
14378
+ /**
14379
+ * When this generation started. The pid's incarnation marker: a consumer
14380
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14381
+ * window when this changes, because the counter restarted from zero in a new
14382
+ * process.
14383
+ */
14384
+ startedAtMs: number().optional(),
14385
+ /**
14386
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14387
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
14388
+ * contribution is asked for.
14389
+ *
14390
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
14391
+ * needs a sampler, and a new per-node sampler is the defect half of
14392
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14393
+ * by whoever already keeps a history; a rate cannot be un-averaged.
14394
+ *
14395
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14396
+ * an entry with no process.
14397
+ */
14398
+ cpuSeconds: number().optional(),
14399
+ /** Resident bytes of this unit's process, same source and same rules. */
14400
+ rssBytes: number().optional()
14401
+ });
14402
+ method(_void(), array(LoadContributionSchema).readonly());
14233
14403
  /**
14234
14404
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
14235
14405
  * through. It stores nothing.
@@ -14306,176 +14476,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
14306
14476
  tags: record(string(), string()).optional()
14307
14477
  }), array(LogEntrySchema).readonly());
14308
14478
  /**
14309
- * `failure-contribution` — the capability an addon reports its OWN losses
14310
- * through, per camera, with the denominator attached. It stores nothing.
14311
- *
14312
- * ## The twin of `load-contribution`, and why it is a twin and not a field
14313
- *
14314
- * `load-contribution` answers *what did this camera COST*. This answers *what
14315
- * did this camera LOSE*. The reporting discipline is identical and deliberately
14316
- * copied: the contributor reports what it already knows, hub-main adds only
14317
- * `addonId`, nothing needs global knowledge, and there is no central list for
14318
- * somebody to forget to edit.
14319
- *
14320
- * They are not merged, because their invariants are opposites:
14321
- *
14322
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
14323
- * claim a camera cost nothing, which is a measurement nobody made;
14324
- * - a `failure-contribution` zero is the **most valuable value on the
14325
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14326
- * and it is exactly what an absent entry cannot say.
14327
- *
14328
- * Putting a loss counter on a cost entry would also break the reconciliation
14329
- * that gives `load-contribution` its point: contributions are subtracted from
14330
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14331
- * has no process.
14332
- *
14333
- * ## Why not a log line, since the counters already exist
14334
- *
14335
- * Several of these paths already counted themselves — `CaptureScheduler`'s
14336
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14337
- * ends in a log line, and a log line is the thing the operator asked to stop
14338
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14339
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14340
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14341
- * media blackout were both diagnosed. The counters stay; this is where they can
14342
- * be READ.
14343
- *
14344
- * ## The rate is served with its denominator or not at all
14345
- *
14346
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
14347
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14348
- * than yesterday" and was **flat across twelve hours** once divided by the
14349
- * successes on the same path. A surface that publishes only the numerator
14350
- * reproduces that mistake on every read.
14351
- *
14352
- * ## Shape
14353
- *
14354
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14355
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14356
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14357
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14358
- * a forked runner's entries reach hub-main over transport that already exists.
14359
- * No new UDS message, no second registry (D3). The operator reads the assembled
14360
- * result through `system.getFailureContributions`.
14361
- */
14362
- var FailureReasonCountSchema = object({
14363
- /**
14364
- * Why the attempt did not land, in the contributor's own vocabulary —
14365
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14366
- * strings that already appear in this repo's logs and, where one exists, the
14367
- * same string the per-track `previewMissReason` records (D276): a second
14368
- * vocabulary for the same loss would make the row and the counter
14369
- * un-joinable.
14370
- */
14371
- reason: string(),
14372
- count: number().int().nonnegative()
14373
- });
14374
- var FailureContributionSchema = object({
14375
- /**
14376
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14377
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14378
- * `unit` free: the families are owned by different addons and a shared enum
14379
- * is a central list that rots invisibly.
14380
- */
14381
- family: string(),
14382
- /**
14383
- * The NUMERIC device id — the same value every log line carries as
14384
- * `tags.deviceId`. Never nullable and never absent: a contributor that
14385
- * cannot name the camera must not emit the entry, because a fleet total
14386
- * cannot answer the only question anybody asks of this surface.
14387
- */
14388
- deviceId: number().int().positive(),
14389
- /**
14390
- * A second dimension inside the family: the model / step id for an inference
14391
- * timeout, so "which camera AND which model" is one read. Absent when the
14392
- * family has a single variant.
14393
- */
14394
- variant: string().optional(),
14395
- /**
14396
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14397
- * differencing two reads must drop the interval when it changes, because the
14398
- * counter restarted from zero in a respawned runner. Same discipline as
14399
- * `LoadContribution.startedAtMs`.
14400
- */
14401
- sinceMs: number(),
14402
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14403
- atMs: number(),
14404
- /**
14405
- * THE DENOMINATOR — every attempt on this path for this camera in the
14406
- * window. A failure count published without it is the mistake this schema
14407
- * exists to make impossible.
14408
- */
14409
- attempts: number().int().nonnegative(),
14410
- /** Attempts that landed. `attempts - succeeded` is the loss. */
14411
- succeeded: number().int().nonnegative(),
14412
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
14413
- reasons: array(FailureReasonCountSchema).readonly()
14414
- });
14415
- method(_void(), array(FailureContributionSchema).readonly());
14416
- var LoadContributionSchema = object({
14417
- role: _enum([
14418
- "decode",
14419
- "transcode",
14420
- "recording",
14421
- "streaming",
14422
- "detection"
14423
- ]),
14424
- /**
14425
- * The NUMERIC device id — the same value every log line carries as
14426
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14427
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
14428
- * contributor that cannot name its camera must not emit the entry at all,
14429
- * because an unnamed per-camera entry is indistinguishable from a shared one
14430
- * and would quietly turn one camera's cost into everybody's.
14431
- */
14432
- deviceId: number().int().positive().nullable(),
14433
- attribution: _enum([
14434
- "measured",
14435
- "accounted",
14436
- "unattributable"
14437
- ]),
14438
- /**
14439
- * What ONE entry is, in the contributor's own words — `615/high`,
14440
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14441
- * family and inventing a common one would lose the only information that
14442
- * makes two entries for the same camera distinguishable.
14443
- */
14444
- unit: string(),
14445
- /**
14446
- * The OS process this cost lives in, when there is one. Present so a
14447
- * consumer can (a) tell two generations of the same unit apart across a
14448
- * restart, and (b) subtract claimed processes from the node's process
14449
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14450
- * process of its own.
14451
- */
14452
- pid: number().int().positive().optional(),
14453
- /**
14454
- * When this generation started. The pid's incarnation marker: a consumer
14455
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14456
- * window when this changes, because the counter restarted from zero in a new
14457
- * process.
14458
- */
14459
- startedAtMs: number().optional(),
14460
- /**
14461
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14462
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
14463
- * contribution is asked for.
14464
- *
14465
- * Cumulative and not a rate on purpose: a rate needs a window, a window
14466
- * needs a sampler, and a new per-node sampler is the defect half of
14467
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14468
- * by whoever already keeps a history; a rate cannot be un-averaged.
14469
- *
14470
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14471
- * an entry with no process.
14472
- */
14473
- cpuSeconds: number().optional(),
14474
- /** Resident bytes of this unit's process, same source and same rules. */
14475
- rssBytes: number().optional()
14476
- });
14477
- method(_void(), array(LoadContributionSchema).readonly());
14478
- /**
14479
14479
  * `login-method` — collection cap through which auth addons contribute
14480
14480
  * their pre-auth login surfaces to the login page. This is the SINGLE,
14481
14481
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -19135,12 +19135,53 @@ var MediaFileKindEnum = _enum([
19135
19135
  "keyFrameSmall",
19136
19136
  "thumbnailSmall"
19137
19137
  ]);
19138
+ /**
19139
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
19140
+ * ARE — never the bytes themselves.
19141
+ *
19142
+ * ## Why `url` and not `base64`
19143
+ *
19144
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
19145
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
19146
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
19147
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
19148
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
19149
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
19150
+ *
19151
+ * `url` points at the `event-media` data plane
19152
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
19153
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
19154
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
19155
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
19156
+ * no less protected than they were inside a `view`-level cap response — see
19157
+ * `data-plane-access.ts` for the rule and the one gap it does not close
19158
+ * (per-device scoping).
19159
+ *
19160
+ * The URL is built from the row's **stored** key, which is not always its
19161
+ * published `kind`: a track's face/plate crop is stored as `crop` under
19162
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
19163
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
19164
+ *
19165
+ * ## `base64` is TRANSITIONAL and is going away
19166
+ *
19167
+ * It is still populated for one reason: the deployed viewer's track-detail
19168
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
19169
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
19170
+ * triangle — not as absence. Removing the field before that viewer ships is an
19171
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
19172
+ * delete this line and the `withBytes` pass-through in
19173
+ * `analytics-query-facade.ts`; nothing else reads it.
19174
+ */
19138
19175
  var MediaFileSchema = object({
19139
19176
  key: string(),
19140
19177
  kind: MediaFileKindEnum,
19141
- base64: string(),
19142
19178
  sizeBytes: number(),
19143
19179
  timestamp: number()
19180
+ }).extend({
19181
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
19182
+ url: string(),
19183
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
19184
+ base64: string()
19144
19185
  });
19145
19186
  /**
19146
19187
  * One media row WITHOUT its bytes.
@@ -19152,7 +19193,9 @@ var MediaFileSchema = object({
19152
19193
  * blocks the whole view.
19153
19194
  *
19154
19195
  * `sizeBytes` is carried because it is what lets a client decide between the
19155
- * stored blob and a `?variant=thumb` rendering without fetching either.
19196
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
19197
+ * `url` because a client that had to build the plane path itself is a second
19198
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
19156
19199
  */
19157
19200
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
19158
19201
  /**
@@ -19499,6 +19542,50 @@ var EventStoreFootprintSchema = object({
19499
19542
  totalBytes: number().int(),
19500
19543
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19501
19544
  });
19545
+ /** Event-media footprint for one {@link MediaFileKind}. */
19546
+ var EventMediaKindFootprintSchema = object({
19547
+ kind: MediaFileKindEnum,
19548
+ /** Media rows of this kind. */
19549
+ rows: number().int(),
19550
+ /** Bytes on disk held by those rows. */
19551
+ bytes: number().int()
19552
+ });
19553
+ /**
19554
+ * The media footprint broken down by KIND — the axis a deletion decision
19555
+ * actually turns on.
19556
+ *
19557
+ * A byte total says how much there is; it cannot say what is safe to remove.
19558
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19559
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19560
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19561
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19562
+ * nothing else, so sizing a deletion means summing per kind.
19563
+ *
19564
+ * ## Why `unaccounted*` exists
19565
+ *
19566
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19567
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19568
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19569
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19570
+ * retired code path, or by a version that knew a kind this one does not) would
19571
+ * otherwise vanish from the total silently, and an operator would delete
19572
+ * against a denominator smaller than the disk.
19573
+ *
19574
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19575
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19576
+ */
19577
+ var EventMediaKindBreakdownSchema = object({
19578
+ /** Every media row in scope, from one unfiltered aggregate. */
19579
+ totalRows: number().int(),
19580
+ /** Every media byte in scope, from that same aggregate. */
19581
+ totalBytes: number().int(),
19582
+ /** Per-kind footprint, ordered by bytes descending. */
19583
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19584
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19585
+ unaccountedRows: number().int(),
19586
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19587
+ unaccountedBytes: number().int()
19588
+ });
19502
19589
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19503
19590
  var EventPruneCountsSchema = object({
19504
19591
  motion: number().int(),
@@ -19702,6 +19789,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19702
19789
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19703
19790
  kind: "query",
19704
19791
  auth: "admin"
19792
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19793
+ kind: "query",
19794
+ auth: "admin"
19705
19795
  }), method(object({
19706
19796
  olderThanMs: number(),
19707
19797
  reason: OpsLogReasonSchema.optional()
@@ -19841,6 +19931,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19841
19931
  }), array(MediaFileSchema).readonly()), method(object({
19842
19932
  trackId: string(),
19843
19933
  deviceId: number()
19934
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19935
+ eventId: string(),
19936
+ deviceId: number()
19844
19937
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19845
19938
  kind: "mutation",
19846
19939
  auth: "admin"
@@ -21650,6 +21743,20 @@ method(object({
21650
21743
  error: string().optional()
21651
21744
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21652
21745
  providerId: string(),
21746
+ /**
21747
+ * The location this config is an UNSAVED edit of, when there is one.
21748
+ *
21749
+ * `listLocations` replaces every declared secret with the redaction
21750
+ * sentinel, so the edit modal's form state holds the sentinel for any
21751
+ * credential the operator did not retype — and posting that here
21752
+ * without a way to resolve it makes the provider try to authenticate
21753
+ * as `__camstack_redacted__` and report the operator's own working
21754
+ * password as wrong. Given this id, the orchestrator restores each
21755
+ * sentinel from the stored config (same rule as `upsertLocation`)
21756
+ * before dispatching. Omitted by the "Add location" wizard, where
21757
+ * every value was typed just now and nothing is stored yet.
21758
+ */
21759
+ locationId: string().optional(),
21653
21760
  config: record(string(), unknown())
21654
21761
  }), object({
21655
21762
  ok: boolean(),
@@ -24121,10 +24228,24 @@ var FaceClusterSchema = object({
24121
24228
  size: number().int(),
24122
24229
  cohesion: number()
24123
24230
  });
24231
+ /**
24232
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24233
+ * are — never the bytes.
24234
+ *
24235
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24236
+ * track/event contract) is still populated because a deployed viewer requires
24237
+ * the field to parse a row at all; this method has no such reader. Its ONE
24238
+ * caller is the admin UI's detail modal, which was building
24239
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24240
+ * dialog already rendering its key FRAME from the `event-media` plane.
24241
+ *
24242
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24243
+ * media key directly, so this needed no new plane and no new access decision.
24244
+ */
24124
24245
  var MediaFileLiteSchema$1 = object({
24125
24246
  key: string(),
24126
24247
  kind: string(),
24127
- base64: string(),
24248
+ url: string(),
24128
24249
  sizeBytes: number(),
24129
24250
  timestamp: number()
24130
24251
  });
@@ -26404,10 +26525,24 @@ var PlateInfoSchema = object({
26404
26525
  */
26405
26526
  cropUrl: string().optional()
26406
26527
  });
26528
+ /**
26529
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
26530
+ * are — never the bytes.
26531
+ *
26532
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
26533
+ * track/event contract) is still populated because a deployed viewer requires
26534
+ * the field to parse a row at all; this method has no such reader. Its ONE
26535
+ * caller is the admin UI's detail modal, which was building
26536
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
26537
+ * dialog already rendering its key FRAME from the `event-media` plane.
26538
+ *
26539
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
26540
+ * media key directly, so this needed no new plane and no new access decision.
26541
+ */
26407
26542
  var MediaFileLiteSchema = object({
26408
26543
  key: string(),
26409
26544
  kind: string(),
26410
- base64: string(),
26545
+ url: string(),
26411
26546
  sizeBytes: number(),
26412
26547
  timestamp: number()
26413
26548
  });
@@ -32349,6 +32484,12 @@ Object.freeze({
32349
32484
  addonId: null,
32350
32485
  access: "view"
32351
32486
  },
32487
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
32488
+ capName: "pipeline-analytics",
32489
+ capScope: "device",
32490
+ addonId: null,
32491
+ access: "view"
32492
+ },
32352
32493
  "pipelineAnalytics.getEventStoreFootprint": {
32353
32494
  capName: "pipeline-analytics",
32354
32495
  capScope: "device",
@@ -32445,6 +32586,12 @@ Object.freeze({
32445
32586
  addonId: null,
32446
32587
  access: "view"
32447
32588
  },
32589
+ "pipelineAnalytics.listEventMedia": {
32590
+ capName: "pipeline-analytics",
32591
+ capScope: "device",
32592
+ addonId: null,
32593
+ access: "view"
32594
+ },
32448
32595
  "pipelineAnalytics.listGroups": {
32449
32596
  capName: "pipeline-analytics",
32450
32597
  capScope: "device",
@@ -36008,6 +36155,11 @@ Object.freeze({
36008
36155
  form: "single",
36009
36156
  optional: false
36010
36157
  }],
36158
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
36159
+ name: "deviceId",
36160
+ form: "single",
36161
+ optional: true
36162
+ }],
36011
36163
  "pipelineAnalytics.getGroup": [{
36012
36164
  name: "deviceId",
36013
36165
  form: "single",
@@ -36068,6 +36220,11 @@ Object.freeze({
36068
36220
  form: "array",
36069
36221
  optional: false
36070
36222
  }],
36223
+ "pipelineAnalytics.listEventMedia": [{
36224
+ name: "deviceId",
36225
+ form: "single",
36226
+ optional: false
36227
+ }],
36071
36228
  "pipelineAnalytics.listGroups": [{
36072
36229
  name: "deviceIds",
36073
36230
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-export-hap",
3
- "version": "1.2.57",
3
+ "version": "1.2.59",
4
4
  "description": "HomeKit (HAP) exporter for CamStack devices. Publishes each exposed device as its own HomeKit accessory: cameras and doorbells with SRTP streaming, HomeKit Secure Video, motion, two-way audio, PTZ and battery; switches, lights, locks and sensors through a capability→service table.",
5
5
  "keywords": [
6
6
  "camstack",