@camstack/addon-export-hap 1.2.57 → 1.2.58

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
  /**
@@ -19853,6 +19896,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19853
19896
  }), array(MediaFileSchema).readonly()), method(object({
19854
19897
  trackId: string(),
19855
19898
  deviceId: number()
19899
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19900
+ eventId: string(),
19901
+ deviceId: number()
19856
19902
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19857
19903
  kind: "mutation",
19858
19904
  auth: "admin"
@@ -24133,10 +24179,24 @@ var FaceClusterSchema = object({
24133
24179
  size: number().int(),
24134
24180
  cohesion: number()
24135
24181
  });
24182
+ /**
24183
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24184
+ * are — never the bytes.
24185
+ *
24186
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24187
+ * track/event contract) is still populated because a deployed viewer requires
24188
+ * the field to parse a row at all; this method has no such reader. Its ONE
24189
+ * caller is the admin UI's detail modal, which was building
24190
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24191
+ * dialog already rendering its key FRAME from the `event-media` plane.
24192
+ *
24193
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24194
+ * media key directly, so this needed no new plane and no new access decision.
24195
+ */
24136
24196
  var MediaFileLiteSchema$1 = object({
24137
24197
  key: string(),
24138
24198
  kind: string(),
24139
- base64: string(),
24199
+ url: string(),
24140
24200
  sizeBytes: number(),
24141
24201
  timestamp: number()
24142
24202
  });
@@ -26416,10 +26476,24 @@ var PlateInfoSchema = object({
26416
26476
  */
26417
26477
  cropUrl: string().optional()
26418
26478
  });
26479
+ /**
26480
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
26481
+ * are — never the bytes.
26482
+ *
26483
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
26484
+ * track/event contract) is still populated because a deployed viewer requires
26485
+ * the field to parse a row at all; this method has no such reader. Its ONE
26486
+ * caller is the admin UI's detail modal, which was building
26487
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
26488
+ * dialog already rendering its key FRAME from the `event-media` plane.
26489
+ *
26490
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
26491
+ * media key directly, so this needed no new plane and no new access decision.
26492
+ */
26419
26493
  var MediaFileLiteSchema = object({
26420
26494
  key: string(),
26421
26495
  kind: string(),
26422
- base64: string(),
26496
+ url: string(),
26423
26497
  sizeBytes: number(),
26424
26498
  timestamp: number()
26425
26499
  });
@@ -32457,6 +32531,12 @@ Object.freeze({
32457
32531
  addonId: null,
32458
32532
  access: "view"
32459
32533
  },
32534
+ "pipelineAnalytics.listEventMedia": {
32535
+ capName: "pipeline-analytics",
32536
+ capScope: "device",
32537
+ addonId: null,
32538
+ access: "view"
32539
+ },
32460
32540
  "pipelineAnalytics.listGroups": {
32461
32541
  capName: "pipeline-analytics",
32462
32542
  capScope: "device",
@@ -36080,6 +36160,11 @@ Object.freeze({
36080
36160
  form: "array",
36081
36161
  optional: false
36082
36162
  }],
36163
+ "pipelineAnalytics.listEventMedia": [{
36164
+ name: "deviceId",
36165
+ form: "single",
36166
+ optional: false
36167
+ }],
36083
36168
  "pipelineAnalytics.listGroups": [{
36084
36169
  name: "deviceIds",
36085
36170
  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
  /**
@@ -19841,6 +19884,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19841
19884
  }), array(MediaFileSchema).readonly()), method(object({
19842
19885
  trackId: string(),
19843
19886
  deviceId: number()
19887
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19888
+ eventId: string(),
19889
+ deviceId: number()
19844
19890
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19845
19891
  kind: "mutation",
19846
19892
  auth: "admin"
@@ -24121,10 +24167,24 @@ var FaceClusterSchema = object({
24121
24167
  size: number().int(),
24122
24168
  cohesion: number()
24123
24169
  });
24170
+ /**
24171
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24172
+ * are — never the bytes.
24173
+ *
24174
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24175
+ * track/event contract) is still populated because a deployed viewer requires
24176
+ * the field to parse a row at all; this method has no such reader. Its ONE
24177
+ * caller is the admin UI's detail modal, which was building
24178
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24179
+ * dialog already rendering its key FRAME from the `event-media` plane.
24180
+ *
24181
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24182
+ * media key directly, so this needed no new plane and no new access decision.
24183
+ */
24124
24184
  var MediaFileLiteSchema$1 = object({
24125
24185
  key: string(),
24126
24186
  kind: string(),
24127
- base64: string(),
24187
+ url: string(),
24128
24188
  sizeBytes: number(),
24129
24189
  timestamp: number()
24130
24190
  });
@@ -26404,10 +26464,24 @@ var PlateInfoSchema = object({
26404
26464
  */
26405
26465
  cropUrl: string().optional()
26406
26466
  });
26467
+ /**
26468
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
26469
+ * are — never the bytes.
26470
+ *
26471
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
26472
+ * track/event contract) is still populated because a deployed viewer requires
26473
+ * the field to parse a row at all; this method has no such reader. Its ONE
26474
+ * caller is the admin UI's detail modal, which was building
26475
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
26476
+ * dialog already rendering its key FRAME from the `event-media` plane.
26477
+ *
26478
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
26479
+ * media key directly, so this needed no new plane and no new access decision.
26480
+ */
26407
26481
  var MediaFileLiteSchema = object({
26408
26482
  key: string(),
26409
26483
  kind: string(),
26410
- base64: string(),
26484
+ url: string(),
26411
26485
  sizeBytes: number(),
26412
26486
  timestamp: number()
26413
26487
  });
@@ -32445,6 +32519,12 @@ Object.freeze({
32445
32519
  addonId: null,
32446
32520
  access: "view"
32447
32521
  },
32522
+ "pipelineAnalytics.listEventMedia": {
32523
+ capName: "pipeline-analytics",
32524
+ capScope: "device",
32525
+ addonId: null,
32526
+ access: "view"
32527
+ },
32448
32528
  "pipelineAnalytics.listGroups": {
32449
32529
  capName: "pipeline-analytics",
32450
32530
  capScope: "device",
@@ -36068,6 +36148,11 @@ Object.freeze({
36068
36148
  form: "array",
36069
36149
  optional: false
36070
36150
  }],
36151
+ "pipelineAnalytics.listEventMedia": [{
36152
+ name: "deviceId",
36153
+ form: "single",
36154
+ optional: false
36155
+ }],
36071
36156
  "pipelineAnalytics.listGroups": [{
36072
36157
  name: "deviceIds",
36073
36158
  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.58",
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",