@camstack/addon-provider-amcrest 0.2.47 → 0.2.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +259 -174
  2. package/dist/addon.mjs +259 -174
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -13177,6 +13177,114 @@ method(object({
13177
13177
  height: number()
13178
13178
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13179
13179
  /**
13180
+ * `failure-contribution` — the capability an addon reports its OWN losses
13181
+ * through, per camera, with the denominator attached. It stores nothing.
13182
+ *
13183
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13184
+ *
13185
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13186
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13187
+ * copied: the contributor reports what it already knows, hub-main adds only
13188
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13189
+ * somebody to forget to edit.
13190
+ *
13191
+ * They are not merged, because their invariants are opposites:
13192
+ *
13193
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13194
+ * claim a camera cost nothing, which is a measurement nobody made;
13195
+ * - a `failure-contribution` zero is the **most valuable value on the
13196
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13197
+ * and it is exactly what an absent entry cannot say.
13198
+ *
13199
+ * Putting a loss counter on a cost entry would also break the reconciliation
13200
+ * that gives `load-contribution` its point: contributions are subtracted from
13201
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13202
+ * has no process.
13203
+ *
13204
+ * ## Why not a log line, since the counters already exist
13205
+ *
13206
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13207
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13208
+ * ends in a log line, and a log line is the thing the operator asked to stop
13209
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13210
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13211
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13212
+ * media blackout were both diagnosed. The counters stay; this is where they can
13213
+ * be READ.
13214
+ *
13215
+ * ## The rate is served with its denominator or not at all
13216
+ *
13217
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13218
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13219
+ * than yesterday" and was **flat across twelve hours** once divided by the
13220
+ * successes on the same path. A surface that publishes only the numerator
13221
+ * reproduces that mistake on every read.
13222
+ *
13223
+ * ## Shape
13224
+ *
13225
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13226
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13227
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13228
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13229
+ * a forked runner's entries reach hub-main over transport that already exists.
13230
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13231
+ * result through `system.getFailureContributions`.
13232
+ */
13233
+ var FailureReasonCountSchema = object({
13234
+ /**
13235
+ * Why the attempt did not land, in the contributor's own vocabulary —
13236
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13237
+ * strings that already appear in this repo's logs and, where one exists, the
13238
+ * same string the per-track `previewMissReason` records (D276): a second
13239
+ * vocabulary for the same loss would make the row and the counter
13240
+ * un-joinable.
13241
+ */
13242
+ reason: string(),
13243
+ count: number().int().nonnegative()
13244
+ });
13245
+ var FailureContributionSchema = object({
13246
+ /**
13247
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13248
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13249
+ * `unit` free: the families are owned by different addons and a shared enum
13250
+ * is a central list that rots invisibly.
13251
+ */
13252
+ family: string(),
13253
+ /**
13254
+ * The NUMERIC device id — the same value every log line carries as
13255
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13256
+ * cannot name the camera must not emit the entry, because a fleet total
13257
+ * cannot answer the only question anybody asks of this surface.
13258
+ */
13259
+ deviceId: number().int().positive(),
13260
+ /**
13261
+ * A second dimension inside the family: the model / step id for an inference
13262
+ * timeout, so "which camera AND which model" is one read. Absent when the
13263
+ * family has a single variant.
13264
+ */
13265
+ variant: string().optional(),
13266
+ /**
13267
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13268
+ * differencing two reads must drop the interval when it changes, because the
13269
+ * counter restarted from zero in a respawned runner. Same discipline as
13270
+ * `LoadContribution.startedAtMs`.
13271
+ */
13272
+ sinceMs: number(),
13273
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13274
+ atMs: number(),
13275
+ /**
13276
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13277
+ * window. A failure count published without it is the mistake this schema
13278
+ * exists to make impossible.
13279
+ */
13280
+ attempts: number().int().nonnegative(),
13281
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13282
+ succeeded: number().int().nonnegative(),
13283
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13284
+ reasons: array(FailureReasonCountSchema).readonly()
13285
+ });
13286
+ method(_void(), array(FailureContributionSchema).readonly());
13287
+ /**
13180
13288
  * filesystem-browse — per-node capability for browsing the node's local
13181
13289
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13182
13290
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13698,6 +13806,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13698
13806
  kind: "mutation",
13699
13807
  auth: "admin"
13700
13808
  });
13809
+ var LoadContributionSchema = object({
13810
+ role: _enum([
13811
+ "decode",
13812
+ "transcode",
13813
+ "recording",
13814
+ "streaming",
13815
+ "detection"
13816
+ ]),
13817
+ /**
13818
+ * The NUMERIC device id — the same value every log line carries as
13819
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13820
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13821
+ * contributor that cannot name its camera must not emit the entry at all,
13822
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13823
+ * and would quietly turn one camera's cost into everybody's.
13824
+ */
13825
+ deviceId: number().int().positive().nullable(),
13826
+ attribution: _enum([
13827
+ "measured",
13828
+ "accounted",
13829
+ "unattributable"
13830
+ ]),
13831
+ /**
13832
+ * What ONE entry is, in the contributor's own words — `615/high`,
13833
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13834
+ * family and inventing a common one would lose the only information that
13835
+ * makes two entries for the same camera distinguishable.
13836
+ */
13837
+ unit: string(),
13838
+ /**
13839
+ * The OS process this cost lives in, when there is one. Present so a
13840
+ * consumer can (a) tell two generations of the same unit apart across a
13841
+ * restart, and (b) subtract claimed processes from the node's process
13842
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13843
+ * process of its own.
13844
+ */
13845
+ pid: number().int().positive().optional(),
13846
+ /**
13847
+ * When this generation started. The pid's incarnation marker: a consumer
13848
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13849
+ * window when this changes, because the counter restarted from zero in a new
13850
+ * process.
13851
+ */
13852
+ startedAtMs: number().optional(),
13853
+ /**
13854
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13855
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13856
+ * contribution is asked for.
13857
+ *
13858
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13859
+ * needs a sampler, and a new per-node sampler is the defect half of
13860
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13861
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13862
+ *
13863
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13864
+ * an entry with no process.
13865
+ */
13866
+ cpuSeconds: number().optional(),
13867
+ /** Resident bytes of this unit's process, same source and same rules. */
13868
+ rssBytes: number().optional()
13869
+ });
13870
+ method(_void(), array(LoadContributionSchema).readonly());
13701
13871
  /**
13702
13872
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13703
13873
  * through. It stores nothing.
@@ -13774,176 +13944,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13774
13944
  tags: record(string(), string()).optional()
13775
13945
  }), array(LogEntrySchema).readonly());
13776
13946
  /**
13777
- * `failure-contribution` — the capability an addon reports its OWN losses
13778
- * through, per camera, with the denominator attached. It stores nothing.
13779
- *
13780
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13781
- *
13782
- * `load-contribution` answers *what did this camera COST*. This answers *what
13783
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13784
- * copied: the contributor reports what it already knows, hub-main adds only
13785
- * `addonId`, nothing needs global knowledge, and there is no central list for
13786
- * somebody to forget to edit.
13787
- *
13788
- * They are not merged, because their invariants are opposites:
13789
- *
13790
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13791
- * claim a camera cost nothing, which is a measurement nobody made;
13792
- * - a `failure-contribution` zero is the **most valuable value on the
13793
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13794
- * and it is exactly what an absent entry cannot say.
13795
- *
13796
- * Putting a loss counter on a cost entry would also break the reconciliation
13797
- * that gives `load-contribution` its point: contributions are subtracted from
13798
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13799
- * has no process.
13800
- *
13801
- * ## Why not a log line, since the counters already exist
13802
- *
13803
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13804
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13805
- * ends in a log line, and a log line is the thing the operator asked to stop
13806
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13807
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13808
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13809
- * media blackout were both diagnosed. The counters stay; this is where they can
13810
- * be READ.
13811
- *
13812
- * ## The rate is served with its denominator or not at all
13813
- *
13814
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13815
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13816
- * than yesterday" and was **flat across twelve hours** once divided by the
13817
- * successes on the same path. A surface that publishes only the numerator
13818
- * reproduces that mistake on every read.
13819
- *
13820
- * ## Shape
13821
- *
13822
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13823
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13824
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13825
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13826
- * a forked runner's entries reach hub-main over transport that already exists.
13827
- * No new UDS message, no second registry (D3). The operator reads the assembled
13828
- * result through `system.getFailureContributions`.
13829
- */
13830
- var FailureReasonCountSchema = object({
13831
- /**
13832
- * Why the attempt did not land, in the contributor's own vocabulary —
13833
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13834
- * strings that already appear in this repo's logs and, where one exists, the
13835
- * same string the per-track `previewMissReason` records (D276): a second
13836
- * vocabulary for the same loss would make the row and the counter
13837
- * un-joinable.
13838
- */
13839
- reason: string(),
13840
- count: number().int().nonnegative()
13841
- });
13842
- var FailureContributionSchema = object({
13843
- /**
13844
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13845
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13846
- * `unit` free: the families are owned by different addons and a shared enum
13847
- * is a central list that rots invisibly.
13848
- */
13849
- family: string(),
13850
- /**
13851
- * The NUMERIC device id — the same value every log line carries as
13852
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13853
- * cannot name the camera must not emit the entry, because a fleet total
13854
- * cannot answer the only question anybody asks of this surface.
13855
- */
13856
- deviceId: number().int().positive(),
13857
- /**
13858
- * A second dimension inside the family: the model / step id for an inference
13859
- * timeout, so "which camera AND which model" is one read. Absent when the
13860
- * family has a single variant.
13861
- */
13862
- variant: string().optional(),
13863
- /**
13864
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13865
- * differencing two reads must drop the interval when it changes, because the
13866
- * counter restarted from zero in a respawned runner. Same discipline as
13867
- * `LoadContribution.startedAtMs`.
13868
- */
13869
- sinceMs: number(),
13870
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13871
- atMs: number(),
13872
- /**
13873
- * THE DENOMINATOR — every attempt on this path for this camera in the
13874
- * window. A failure count published without it is the mistake this schema
13875
- * exists to make impossible.
13876
- */
13877
- attempts: number().int().nonnegative(),
13878
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13879
- succeeded: number().int().nonnegative(),
13880
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13881
- reasons: array(FailureReasonCountSchema).readonly()
13882
- });
13883
- method(_void(), array(FailureContributionSchema).readonly());
13884
- var LoadContributionSchema = object({
13885
- role: _enum([
13886
- "decode",
13887
- "transcode",
13888
- "recording",
13889
- "streaming",
13890
- "detection"
13891
- ]),
13892
- /**
13893
- * The NUMERIC device id — the same value every log line carries as
13894
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13895
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13896
- * contributor that cannot name its camera must not emit the entry at all,
13897
- * because an unnamed per-camera entry is indistinguishable from a shared one
13898
- * and would quietly turn one camera's cost into everybody's.
13899
- */
13900
- deviceId: number().int().positive().nullable(),
13901
- attribution: _enum([
13902
- "measured",
13903
- "accounted",
13904
- "unattributable"
13905
- ]),
13906
- /**
13907
- * What ONE entry is, in the contributor's own words — `615/high`,
13908
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13909
- * family and inventing a common one would lose the only information that
13910
- * makes two entries for the same camera distinguishable.
13911
- */
13912
- unit: string(),
13913
- /**
13914
- * The OS process this cost lives in, when there is one. Present so a
13915
- * consumer can (a) tell two generations of the same unit apart across a
13916
- * restart, and (b) subtract claimed processes from the node's process
13917
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13918
- * process of its own.
13919
- */
13920
- pid: number().int().positive().optional(),
13921
- /**
13922
- * When this generation started. The pid's incarnation marker: a consumer
13923
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13924
- * window when this changes, because the counter restarted from zero in a new
13925
- * process.
13926
- */
13927
- startedAtMs: number().optional(),
13928
- /**
13929
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13930
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13931
- * contribution is asked for.
13932
- *
13933
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13934
- * needs a sampler, and a new per-node sampler is the defect half of
13935
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13936
- * by whoever already keeps a history; a rate cannot be un-averaged.
13937
- *
13938
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13939
- * an entry with no process.
13940
- */
13941
- cpuSeconds: number().optional(),
13942
- /** Resident bytes of this unit's process, same source and same rules. */
13943
- rssBytes: number().optional()
13944
- });
13945
- method(_void(), array(LoadContributionSchema).readonly());
13946
- /**
13947
13947
  * `login-method` — collection cap through which auth addons contribute
13948
13948
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13949
13949
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18681,12 +18681,53 @@ var MediaFileKindEnum = _enum([
18681
18681
  "keyFrameSmall",
18682
18682
  "thumbnailSmall"
18683
18683
  ]);
18684
+ /**
18685
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18686
+ * ARE — never the bytes themselves.
18687
+ *
18688
+ * ## Why `url` and not `base64`
18689
+ *
18690
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18691
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18692
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18693
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18694
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18695
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18696
+ *
18697
+ * `url` points at the `event-media` data plane
18698
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18699
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18700
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18701
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18702
+ * no less protected than they were inside a `view`-level cap response — see
18703
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18704
+ * (per-device scoping).
18705
+ *
18706
+ * The URL is built from the row's **stored** key, which is not always its
18707
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18708
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18709
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18710
+ *
18711
+ * ## `base64` is TRANSITIONAL and is going away
18712
+ *
18713
+ * It is still populated for one reason: the deployed viewer's track-detail
18714
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18715
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18716
+ * triangle — not as absence. Removing the field before that viewer ships is an
18717
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18718
+ * delete this line and the `withBytes` pass-through in
18719
+ * `analytics-query-facade.ts`; nothing else reads it.
18720
+ */
18684
18721
  var MediaFileSchema = object({
18685
18722
  key: string(),
18686
18723
  kind: MediaFileKindEnum,
18687
- base64: string(),
18688
18724
  sizeBytes: number(),
18689
18725
  timestamp: number()
18726
+ }).extend({
18727
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18728
+ url: string(),
18729
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18730
+ base64: string()
18690
18731
  });
18691
18732
  /**
18692
18733
  * One media row WITHOUT its bytes.
@@ -18698,7 +18739,9 @@ var MediaFileSchema = object({
18698
18739
  * blocks the whole view.
18699
18740
  *
18700
18741
  * `sizeBytes` is carried because it is what lets a client decide between the
18701
- * stored blob and a `?variant=thumb` rendering without fetching either.
18742
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18743
+ * `url` because a client that had to build the plane path itself is a second
18744
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18702
18745
  */
18703
18746
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18704
18747
  /**
@@ -19387,6 +19430,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19387
19430
  }), array(MediaFileSchema).readonly()), method(object({
19388
19431
  trackId: string(),
19389
19432
  deviceId: number()
19433
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19434
+ eventId: string(),
19435
+ deviceId: number()
19390
19436
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19391
19437
  kind: "mutation",
19392
19438
  auth: "admin"
@@ -24481,10 +24527,24 @@ var FaceClusterSchema = object({
24481
24527
  size: number().int(),
24482
24528
  cohesion: number()
24483
24529
  });
24530
+ /**
24531
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24532
+ * are — never the bytes.
24533
+ *
24534
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24535
+ * track/event contract) is still populated because a deployed viewer requires
24536
+ * the field to parse a row at all; this method has no such reader. Its ONE
24537
+ * caller is the admin UI's detail modal, which was building
24538
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24539
+ * dialog already rendering its key FRAME from the `event-media` plane.
24540
+ *
24541
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24542
+ * media key directly, so this needed no new plane and no new access decision.
24543
+ */
24484
24544
  var MediaFileLiteSchema$1 = object({
24485
24545
  key: string(),
24486
24546
  kind: string(),
24487
- base64: string(),
24547
+ url: string(),
24488
24548
  sizeBytes: number(),
24489
24549
  timestamp: number()
24490
24550
  });
@@ -27552,10 +27612,24 @@ var PlateInfoSchema = object({
27552
27612
  */
27553
27613
  cropUrl: string().optional()
27554
27614
  });
27615
+ /**
27616
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27617
+ * are — never the bytes.
27618
+ *
27619
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27620
+ * track/event contract) is still populated because a deployed viewer requires
27621
+ * the field to parse a row at all; this method has no such reader. Its ONE
27622
+ * caller is the admin UI's detail modal, which was building
27623
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27624
+ * dialog already rendering its key FRAME from the `event-media` plane.
27625
+ *
27626
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27627
+ * media key directly, so this needed no new plane and no new access decision.
27628
+ */
27555
27629
  var MediaFileLiteSchema = object({
27556
27630
  key: string(),
27557
27631
  kind: string(),
27558
- base64: string(),
27632
+ url: string(),
27559
27633
  sizeBytes: number(),
27560
27634
  timestamp: number()
27561
27635
  });
@@ -35898,6 +35972,12 @@ Object.freeze({
35898
35972
  addonId: null,
35899
35973
  access: "view"
35900
35974
  },
35975
+ "pipelineAnalytics.listEventMedia": {
35976
+ capName: "pipeline-analytics",
35977
+ capScope: "device",
35978
+ addonId: null,
35979
+ access: "view"
35980
+ },
35901
35981
  "pipelineAnalytics.listGroups": {
35902
35982
  capName: "pipeline-analytics",
35903
35983
  capScope: "device",
@@ -39521,6 +39601,11 @@ Object.freeze({
39521
39601
  form: "array",
39522
39602
  optional: false
39523
39603
  }],
39604
+ "pipelineAnalytics.listEventMedia": [{
39605
+ name: "deviceId",
39606
+ form: "single",
39607
+ optional: false
39608
+ }],
39524
39609
  "pipelineAnalytics.listGroups": [{
39525
39610
  name: "deviceIds",
39526
39611
  form: "array",
package/dist/addon.mjs CHANGED
@@ -13178,6 +13178,114 @@ method(object({
13178
13178
  height: number()
13179
13179
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13180
13180
  /**
13181
+ * `failure-contribution` — the capability an addon reports its OWN losses
13182
+ * through, per camera, with the denominator attached. It stores nothing.
13183
+ *
13184
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13185
+ *
13186
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13187
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13188
+ * copied: the contributor reports what it already knows, hub-main adds only
13189
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13190
+ * somebody to forget to edit.
13191
+ *
13192
+ * They are not merged, because their invariants are opposites:
13193
+ *
13194
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13195
+ * claim a camera cost nothing, which is a measurement nobody made;
13196
+ * - a `failure-contribution` zero is the **most valuable value on the
13197
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13198
+ * and it is exactly what an absent entry cannot say.
13199
+ *
13200
+ * Putting a loss counter on a cost entry would also break the reconciliation
13201
+ * that gives `load-contribution` its point: contributions are subtracted from
13202
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13203
+ * has no process.
13204
+ *
13205
+ * ## Why not a log line, since the counters already exist
13206
+ *
13207
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13208
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13209
+ * ends in a log line, and a log line is the thing the operator asked to stop
13210
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13211
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13212
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13213
+ * media blackout were both diagnosed. The counters stay; this is where they can
13214
+ * be READ.
13215
+ *
13216
+ * ## The rate is served with its denominator or not at all
13217
+ *
13218
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13219
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13220
+ * than yesterday" and was **flat across twelve hours** once divided by the
13221
+ * successes on the same path. A surface that publishes only the numerator
13222
+ * reproduces that mistake on every read.
13223
+ *
13224
+ * ## Shape
13225
+ *
13226
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13227
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13228
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13229
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13230
+ * a forked runner's entries reach hub-main over transport that already exists.
13231
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13232
+ * result through `system.getFailureContributions`.
13233
+ */
13234
+ var FailureReasonCountSchema = object({
13235
+ /**
13236
+ * Why the attempt did not land, in the contributor's own vocabulary —
13237
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13238
+ * strings that already appear in this repo's logs and, where one exists, the
13239
+ * same string the per-track `previewMissReason` records (D276): a second
13240
+ * vocabulary for the same loss would make the row and the counter
13241
+ * un-joinable.
13242
+ */
13243
+ reason: string(),
13244
+ count: number().int().nonnegative()
13245
+ });
13246
+ var FailureContributionSchema = object({
13247
+ /**
13248
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13249
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13250
+ * `unit` free: the families are owned by different addons and a shared enum
13251
+ * is a central list that rots invisibly.
13252
+ */
13253
+ family: string(),
13254
+ /**
13255
+ * The NUMERIC device id — the same value every log line carries as
13256
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13257
+ * cannot name the camera must not emit the entry, because a fleet total
13258
+ * cannot answer the only question anybody asks of this surface.
13259
+ */
13260
+ deviceId: number().int().positive(),
13261
+ /**
13262
+ * A second dimension inside the family: the model / step id for an inference
13263
+ * timeout, so "which camera AND which model" is one read. Absent when the
13264
+ * family has a single variant.
13265
+ */
13266
+ variant: string().optional(),
13267
+ /**
13268
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13269
+ * differencing two reads must drop the interval when it changes, because the
13270
+ * counter restarted from zero in a respawned runner. Same discipline as
13271
+ * `LoadContribution.startedAtMs`.
13272
+ */
13273
+ sinceMs: number(),
13274
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13275
+ atMs: number(),
13276
+ /**
13277
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13278
+ * window. A failure count published without it is the mistake this schema
13279
+ * exists to make impossible.
13280
+ */
13281
+ attempts: number().int().nonnegative(),
13282
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13283
+ succeeded: number().int().nonnegative(),
13284
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13285
+ reasons: array(FailureReasonCountSchema).readonly()
13286
+ });
13287
+ method(_void(), array(FailureContributionSchema).readonly());
13288
+ /**
13181
13289
  * filesystem-browse — per-node capability for browsing the node's local
13182
13290
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13183
13291
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13699,6 +13807,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13699
13807
  kind: "mutation",
13700
13808
  auth: "admin"
13701
13809
  });
13810
+ var LoadContributionSchema = object({
13811
+ role: _enum([
13812
+ "decode",
13813
+ "transcode",
13814
+ "recording",
13815
+ "streaming",
13816
+ "detection"
13817
+ ]),
13818
+ /**
13819
+ * The NUMERIC device id — the same value every log line carries as
13820
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13821
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13822
+ * contributor that cannot name its camera must not emit the entry at all,
13823
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13824
+ * and would quietly turn one camera's cost into everybody's.
13825
+ */
13826
+ deviceId: number().int().positive().nullable(),
13827
+ attribution: _enum([
13828
+ "measured",
13829
+ "accounted",
13830
+ "unattributable"
13831
+ ]),
13832
+ /**
13833
+ * What ONE entry is, in the contributor's own words — `615/high`,
13834
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13835
+ * family and inventing a common one would lose the only information that
13836
+ * makes two entries for the same camera distinguishable.
13837
+ */
13838
+ unit: string(),
13839
+ /**
13840
+ * The OS process this cost lives in, when there is one. Present so a
13841
+ * consumer can (a) tell two generations of the same unit apart across a
13842
+ * restart, and (b) subtract claimed processes from the node's process
13843
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13844
+ * process of its own.
13845
+ */
13846
+ pid: number().int().positive().optional(),
13847
+ /**
13848
+ * When this generation started. The pid's incarnation marker: a consumer
13849
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13850
+ * window when this changes, because the counter restarted from zero in a new
13851
+ * process.
13852
+ */
13853
+ startedAtMs: number().optional(),
13854
+ /**
13855
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13856
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13857
+ * contribution is asked for.
13858
+ *
13859
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13860
+ * needs a sampler, and a new per-node sampler is the defect half of
13861
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13862
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13863
+ *
13864
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13865
+ * an entry with no process.
13866
+ */
13867
+ cpuSeconds: number().optional(),
13868
+ /** Resident bytes of this unit's process, same source and same rules. */
13869
+ rssBytes: number().optional()
13870
+ });
13871
+ method(_void(), array(LoadContributionSchema).readonly());
13702
13872
  /**
13703
13873
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13704
13874
  * through. It stores nothing.
@@ -13775,176 +13945,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13775
13945
  tags: record(string(), string()).optional()
13776
13946
  }), array(LogEntrySchema).readonly());
13777
13947
  /**
13778
- * `failure-contribution` — the capability an addon reports its OWN losses
13779
- * through, per camera, with the denominator attached. It stores nothing.
13780
- *
13781
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13782
- *
13783
- * `load-contribution` answers *what did this camera COST*. This answers *what
13784
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13785
- * copied: the contributor reports what it already knows, hub-main adds only
13786
- * `addonId`, nothing needs global knowledge, and there is no central list for
13787
- * somebody to forget to edit.
13788
- *
13789
- * They are not merged, because their invariants are opposites:
13790
- *
13791
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13792
- * claim a camera cost nothing, which is a measurement nobody made;
13793
- * - a `failure-contribution` zero is the **most valuable value on the
13794
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13795
- * and it is exactly what an absent entry cannot say.
13796
- *
13797
- * Putting a loss counter on a cost entry would also break the reconciliation
13798
- * that gives `load-contribution` its point: contributions are subtracted from
13799
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13800
- * has no process.
13801
- *
13802
- * ## Why not a log line, since the counters already exist
13803
- *
13804
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13805
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13806
- * ends in a log line, and a log line is the thing the operator asked to stop
13807
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13808
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13809
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13810
- * media blackout were both diagnosed. The counters stay; this is where they can
13811
- * be READ.
13812
- *
13813
- * ## The rate is served with its denominator or not at all
13814
- *
13815
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13816
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13817
- * than yesterday" and was **flat across twelve hours** once divided by the
13818
- * successes on the same path. A surface that publishes only the numerator
13819
- * reproduces that mistake on every read.
13820
- *
13821
- * ## Shape
13822
- *
13823
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13824
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13825
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13826
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13827
- * a forked runner's entries reach hub-main over transport that already exists.
13828
- * No new UDS message, no second registry (D3). The operator reads the assembled
13829
- * result through `system.getFailureContributions`.
13830
- */
13831
- var FailureReasonCountSchema = object({
13832
- /**
13833
- * Why the attempt did not land, in the contributor's own vocabulary —
13834
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13835
- * strings that already appear in this repo's logs and, where one exists, the
13836
- * same string the per-track `previewMissReason` records (D276): a second
13837
- * vocabulary for the same loss would make the row and the counter
13838
- * un-joinable.
13839
- */
13840
- reason: string(),
13841
- count: number().int().nonnegative()
13842
- });
13843
- var FailureContributionSchema = object({
13844
- /**
13845
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13846
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13847
- * `unit` free: the families are owned by different addons and a shared enum
13848
- * is a central list that rots invisibly.
13849
- */
13850
- family: string(),
13851
- /**
13852
- * The NUMERIC device id — the same value every log line carries as
13853
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13854
- * cannot name the camera must not emit the entry, because a fleet total
13855
- * cannot answer the only question anybody asks of this surface.
13856
- */
13857
- deviceId: number().int().positive(),
13858
- /**
13859
- * A second dimension inside the family: the model / step id for an inference
13860
- * timeout, so "which camera AND which model" is one read. Absent when the
13861
- * family has a single variant.
13862
- */
13863
- variant: string().optional(),
13864
- /**
13865
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13866
- * differencing two reads must drop the interval when it changes, because the
13867
- * counter restarted from zero in a respawned runner. Same discipline as
13868
- * `LoadContribution.startedAtMs`.
13869
- */
13870
- sinceMs: number(),
13871
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13872
- atMs: number(),
13873
- /**
13874
- * THE DENOMINATOR — every attempt on this path for this camera in the
13875
- * window. A failure count published without it is the mistake this schema
13876
- * exists to make impossible.
13877
- */
13878
- attempts: number().int().nonnegative(),
13879
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13880
- succeeded: number().int().nonnegative(),
13881
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13882
- reasons: array(FailureReasonCountSchema).readonly()
13883
- });
13884
- method(_void(), array(FailureContributionSchema).readonly());
13885
- var LoadContributionSchema = object({
13886
- role: _enum([
13887
- "decode",
13888
- "transcode",
13889
- "recording",
13890
- "streaming",
13891
- "detection"
13892
- ]),
13893
- /**
13894
- * The NUMERIC device id — the same value every log line carries as
13895
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13896
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13897
- * contributor that cannot name its camera must not emit the entry at all,
13898
- * because an unnamed per-camera entry is indistinguishable from a shared one
13899
- * and would quietly turn one camera's cost into everybody's.
13900
- */
13901
- deviceId: number().int().positive().nullable(),
13902
- attribution: _enum([
13903
- "measured",
13904
- "accounted",
13905
- "unattributable"
13906
- ]),
13907
- /**
13908
- * What ONE entry is, in the contributor's own words — `615/high`,
13909
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13910
- * family and inventing a common one would lose the only information that
13911
- * makes two entries for the same camera distinguishable.
13912
- */
13913
- unit: string(),
13914
- /**
13915
- * The OS process this cost lives in, when there is one. Present so a
13916
- * consumer can (a) tell two generations of the same unit apart across a
13917
- * restart, and (b) subtract claimed processes from the node's process
13918
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13919
- * process of its own.
13920
- */
13921
- pid: number().int().positive().optional(),
13922
- /**
13923
- * When this generation started. The pid's incarnation marker: a consumer
13924
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13925
- * window when this changes, because the counter restarted from zero in a new
13926
- * process.
13927
- */
13928
- startedAtMs: number().optional(),
13929
- /**
13930
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13931
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13932
- * contribution is asked for.
13933
- *
13934
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13935
- * needs a sampler, and a new per-node sampler is the defect half of
13936
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13937
- * by whoever already keeps a history; a rate cannot be un-averaged.
13938
- *
13939
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13940
- * an entry with no process.
13941
- */
13942
- cpuSeconds: number().optional(),
13943
- /** Resident bytes of this unit's process, same source and same rules. */
13944
- rssBytes: number().optional()
13945
- });
13946
- method(_void(), array(LoadContributionSchema).readonly());
13947
- /**
13948
13948
  * `login-method` — collection cap through which auth addons contribute
13949
13949
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13950
13950
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18682,12 +18682,53 @@ var MediaFileKindEnum = _enum([
18682
18682
  "keyFrameSmall",
18683
18683
  "thumbnailSmall"
18684
18684
  ]);
18685
+ /**
18686
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18687
+ * ARE — never the bytes themselves.
18688
+ *
18689
+ * ## Why `url` and not `base64`
18690
+ *
18691
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18692
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18693
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18694
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18695
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18696
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18697
+ *
18698
+ * `url` points at the `event-media` data plane
18699
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18700
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18701
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18702
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18703
+ * no less protected than they were inside a `view`-level cap response — see
18704
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18705
+ * (per-device scoping).
18706
+ *
18707
+ * The URL is built from the row's **stored** key, which is not always its
18708
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18709
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18710
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18711
+ *
18712
+ * ## `base64` is TRANSITIONAL and is going away
18713
+ *
18714
+ * It is still populated for one reason: the deployed viewer's track-detail
18715
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18716
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18717
+ * triangle — not as absence. Removing the field before that viewer ships is an
18718
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18719
+ * delete this line and the `withBytes` pass-through in
18720
+ * `analytics-query-facade.ts`; nothing else reads it.
18721
+ */
18685
18722
  var MediaFileSchema = object({
18686
18723
  key: string(),
18687
18724
  kind: MediaFileKindEnum,
18688
- base64: string(),
18689
18725
  sizeBytes: number(),
18690
18726
  timestamp: number()
18727
+ }).extend({
18728
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18729
+ url: string(),
18730
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18731
+ base64: string()
18691
18732
  });
18692
18733
  /**
18693
18734
  * One media row WITHOUT its bytes.
@@ -18699,7 +18740,9 @@ var MediaFileSchema = object({
18699
18740
  * blocks the whole view.
18700
18741
  *
18701
18742
  * `sizeBytes` is carried because it is what lets a client decide between the
18702
- * stored blob and a `?variant=thumb` rendering without fetching either.
18743
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18744
+ * `url` because a client that had to build the plane path itself is a second
18745
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18703
18746
  */
18704
18747
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18705
18748
  /**
@@ -19388,6 +19431,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19388
19431
  }), array(MediaFileSchema).readonly()), method(object({
19389
19432
  trackId: string(),
19390
19433
  deviceId: number()
19434
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19435
+ eventId: string(),
19436
+ deviceId: number()
19391
19437
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19392
19438
  kind: "mutation",
19393
19439
  auth: "admin"
@@ -24482,10 +24528,24 @@ var FaceClusterSchema = object({
24482
24528
  size: number().int(),
24483
24529
  cohesion: number()
24484
24530
  });
24531
+ /**
24532
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24533
+ * are — never the bytes.
24534
+ *
24535
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24536
+ * track/event contract) is still populated because a deployed viewer requires
24537
+ * the field to parse a row at all; this method has no such reader. Its ONE
24538
+ * caller is the admin UI's detail modal, which was building
24539
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24540
+ * dialog already rendering its key FRAME from the `event-media` plane.
24541
+ *
24542
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24543
+ * media key directly, so this needed no new plane and no new access decision.
24544
+ */
24485
24545
  var MediaFileLiteSchema$1 = object({
24486
24546
  key: string(),
24487
24547
  kind: string(),
24488
- base64: string(),
24548
+ url: string(),
24489
24549
  sizeBytes: number(),
24490
24550
  timestamp: number()
24491
24551
  });
@@ -27553,10 +27613,24 @@ var PlateInfoSchema = object({
27553
27613
  */
27554
27614
  cropUrl: string().optional()
27555
27615
  });
27616
+ /**
27617
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27618
+ * are — never the bytes.
27619
+ *
27620
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27621
+ * track/event contract) is still populated because a deployed viewer requires
27622
+ * the field to parse a row at all; this method has no such reader. Its ONE
27623
+ * caller is the admin UI's detail modal, which was building
27624
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27625
+ * dialog already rendering its key FRAME from the `event-media` plane.
27626
+ *
27627
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27628
+ * media key directly, so this needed no new plane and no new access decision.
27629
+ */
27556
27630
  var MediaFileLiteSchema = object({
27557
27631
  key: string(),
27558
27632
  kind: string(),
27559
- base64: string(),
27633
+ url: string(),
27560
27634
  sizeBytes: number(),
27561
27635
  timestamp: number()
27562
27636
  });
@@ -35899,6 +35973,12 @@ Object.freeze({
35899
35973
  addonId: null,
35900
35974
  access: "view"
35901
35975
  },
35976
+ "pipelineAnalytics.listEventMedia": {
35977
+ capName: "pipeline-analytics",
35978
+ capScope: "device",
35979
+ addonId: null,
35980
+ access: "view"
35981
+ },
35902
35982
  "pipelineAnalytics.listGroups": {
35903
35983
  capName: "pipeline-analytics",
35904
35984
  capScope: "device",
@@ -39522,6 +39602,11 @@ Object.freeze({
39522
39602
  form: "array",
39523
39603
  optional: false
39524
39604
  }],
39605
+ "pipelineAnalytics.listEventMedia": [{
39606
+ name: "deviceId",
39607
+ form: "single",
39608
+ optional: false
39609
+ }],
39525
39610
  "pipelineAnalytics.listGroups": [{
39526
39611
  name: "deviceIds",
39527
39612
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-amcrest",
3
- "version": "0.2.47",
3
+ "version": "0.2.48",
4
4
  "description": "Amcrest/Dahua camera device provider addon for CamStack — Dahua CGI over HTTP(S) with digest auth (snapshot, RTSP catalog, PTZ, image/day-night config)",
5
5
  "keywords": [
6
6
  "camstack",