@camstack/addon-export-alexa 1.2.52 → 1.2.53

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.
@@ -13215,6 +13215,114 @@ method(object({
13215
13215
  height: number()
13216
13216
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13217
13217
  /**
13218
+ * `failure-contribution` — the capability an addon reports its OWN losses
13219
+ * through, per camera, with the denominator attached. It stores nothing.
13220
+ *
13221
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13222
+ *
13223
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13224
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13225
+ * copied: the contributor reports what it already knows, hub-main adds only
13226
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13227
+ * somebody to forget to edit.
13228
+ *
13229
+ * They are not merged, because their invariants are opposites:
13230
+ *
13231
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13232
+ * claim a camera cost nothing, which is a measurement nobody made;
13233
+ * - a `failure-contribution` zero is the **most valuable value on the
13234
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13235
+ * and it is exactly what an absent entry cannot say.
13236
+ *
13237
+ * Putting a loss counter on a cost entry would also break the reconciliation
13238
+ * that gives `load-contribution` its point: contributions are subtracted from
13239
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13240
+ * has no process.
13241
+ *
13242
+ * ## Why not a log line, since the counters already exist
13243
+ *
13244
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13245
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13246
+ * ends in a log line, and a log line is the thing the operator asked to stop
13247
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13248
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13249
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13250
+ * media blackout were both diagnosed. The counters stay; this is where they can
13251
+ * be READ.
13252
+ *
13253
+ * ## The rate is served with its denominator or not at all
13254
+ *
13255
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13256
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13257
+ * than yesterday" and was **flat across twelve hours** once divided by the
13258
+ * successes on the same path. A surface that publishes only the numerator
13259
+ * reproduces that mistake on every read.
13260
+ *
13261
+ * ## Shape
13262
+ *
13263
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13264
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13265
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13266
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13267
+ * a forked runner's entries reach hub-main over transport that already exists.
13268
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13269
+ * result through `system.getFailureContributions`.
13270
+ */
13271
+ var FailureReasonCountSchema = object({
13272
+ /**
13273
+ * Why the attempt did not land, in the contributor's own vocabulary —
13274
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13275
+ * strings that already appear in this repo's logs and, where one exists, the
13276
+ * same string the per-track `previewMissReason` records (D276): a second
13277
+ * vocabulary for the same loss would make the row and the counter
13278
+ * un-joinable.
13279
+ */
13280
+ reason: string(),
13281
+ count: number().int().nonnegative()
13282
+ });
13283
+ var FailureContributionSchema = object({
13284
+ /**
13285
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13286
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13287
+ * `unit` free: the families are owned by different addons and a shared enum
13288
+ * is a central list that rots invisibly.
13289
+ */
13290
+ family: string(),
13291
+ /**
13292
+ * The NUMERIC device id — the same value every log line carries as
13293
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13294
+ * cannot name the camera must not emit the entry, because a fleet total
13295
+ * cannot answer the only question anybody asks of this surface.
13296
+ */
13297
+ deviceId: number().int().positive(),
13298
+ /**
13299
+ * A second dimension inside the family: the model / step id for an inference
13300
+ * timeout, so "which camera AND which model" is one read. Absent when the
13301
+ * family has a single variant.
13302
+ */
13303
+ variant: string().optional(),
13304
+ /**
13305
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13306
+ * differencing two reads must drop the interval when it changes, because the
13307
+ * counter restarted from zero in a respawned runner. Same discipline as
13308
+ * `LoadContribution.startedAtMs`.
13309
+ */
13310
+ sinceMs: number(),
13311
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13312
+ atMs: number(),
13313
+ /**
13314
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13315
+ * window. A failure count published without it is the mistake this schema
13316
+ * exists to make impossible.
13317
+ */
13318
+ attempts: number().int().nonnegative(),
13319
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13320
+ succeeded: number().int().nonnegative(),
13321
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13322
+ reasons: array(FailureReasonCountSchema).readonly()
13323
+ });
13324
+ method(_void(), array(FailureContributionSchema).readonly());
13325
+ /**
13218
13326
  * filesystem-browse — per-node capability for browsing the node's local
13219
13327
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13220
13328
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13736,6 +13844,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13736
13844
  kind: "mutation",
13737
13845
  auth: "admin"
13738
13846
  });
13847
+ var LoadContributionSchema = object({
13848
+ role: _enum([
13849
+ "decode",
13850
+ "transcode",
13851
+ "recording",
13852
+ "streaming",
13853
+ "detection"
13854
+ ]),
13855
+ /**
13856
+ * The NUMERIC device id — the same value every log line carries as
13857
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13858
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13859
+ * contributor that cannot name its camera must not emit the entry at all,
13860
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13861
+ * and would quietly turn one camera's cost into everybody's.
13862
+ */
13863
+ deviceId: number().int().positive().nullable(),
13864
+ attribution: _enum([
13865
+ "measured",
13866
+ "accounted",
13867
+ "unattributable"
13868
+ ]),
13869
+ /**
13870
+ * What ONE entry is, in the contributor's own words — `615/high`,
13871
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13872
+ * family and inventing a common one would lose the only information that
13873
+ * makes two entries for the same camera distinguishable.
13874
+ */
13875
+ unit: string(),
13876
+ /**
13877
+ * The OS process this cost lives in, when there is one. Present so a
13878
+ * consumer can (a) tell two generations of the same unit apart across a
13879
+ * restart, and (b) subtract claimed processes from the node's process
13880
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13881
+ * process of its own.
13882
+ */
13883
+ pid: number().int().positive().optional(),
13884
+ /**
13885
+ * When this generation started. The pid's incarnation marker: a consumer
13886
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13887
+ * window when this changes, because the counter restarted from zero in a new
13888
+ * process.
13889
+ */
13890
+ startedAtMs: number().optional(),
13891
+ /**
13892
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13893
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13894
+ * contribution is asked for.
13895
+ *
13896
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13897
+ * needs a sampler, and a new per-node sampler is the defect half of
13898
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13899
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13900
+ *
13901
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13902
+ * an entry with no process.
13903
+ */
13904
+ cpuSeconds: number().optional(),
13905
+ /** Resident bytes of this unit's process, same source and same rules. */
13906
+ rssBytes: number().optional()
13907
+ });
13908
+ method(_void(), array(LoadContributionSchema).readonly());
13739
13909
  /**
13740
13910
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13741
13911
  * through. It stores nothing.
@@ -13812,176 +13982,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13812
13982
  tags: record(string(), string()).optional()
13813
13983
  }), array(LogEntrySchema).readonly());
13814
13984
  /**
13815
- * `failure-contribution` — the capability an addon reports its OWN losses
13816
- * through, per camera, with the denominator attached. It stores nothing.
13817
- *
13818
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13819
- *
13820
- * `load-contribution` answers *what did this camera COST*. This answers *what
13821
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13822
- * copied: the contributor reports what it already knows, hub-main adds only
13823
- * `addonId`, nothing needs global knowledge, and there is no central list for
13824
- * somebody to forget to edit.
13825
- *
13826
- * They are not merged, because their invariants are opposites:
13827
- *
13828
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13829
- * claim a camera cost nothing, which is a measurement nobody made;
13830
- * - a `failure-contribution` zero is the **most valuable value on the
13831
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13832
- * and it is exactly what an absent entry cannot say.
13833
- *
13834
- * Putting a loss counter on a cost entry would also break the reconciliation
13835
- * that gives `load-contribution` its point: contributions are subtracted from
13836
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13837
- * has no process.
13838
- *
13839
- * ## Why not a log line, since the counters already exist
13840
- *
13841
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13842
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13843
- * ends in a log line, and a log line is the thing the operator asked to stop
13844
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13845
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13846
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13847
- * media blackout were both diagnosed. The counters stay; this is where they can
13848
- * be READ.
13849
- *
13850
- * ## The rate is served with its denominator or not at all
13851
- *
13852
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13853
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13854
- * than yesterday" and was **flat across twelve hours** once divided by the
13855
- * successes on the same path. A surface that publishes only the numerator
13856
- * reproduces that mistake on every read.
13857
- *
13858
- * ## Shape
13859
- *
13860
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13861
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13862
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13863
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13864
- * a forked runner's entries reach hub-main over transport that already exists.
13865
- * No new UDS message, no second registry (D3). The operator reads the assembled
13866
- * result through `system.getFailureContributions`.
13867
- */
13868
- var FailureReasonCountSchema = object({
13869
- /**
13870
- * Why the attempt did not land, in the contributor's own vocabulary —
13871
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13872
- * strings that already appear in this repo's logs and, where one exists, the
13873
- * same string the per-track `previewMissReason` records (D276): a second
13874
- * vocabulary for the same loss would make the row and the counter
13875
- * un-joinable.
13876
- */
13877
- reason: string(),
13878
- count: number().int().nonnegative()
13879
- });
13880
- var FailureContributionSchema = object({
13881
- /**
13882
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13883
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13884
- * `unit` free: the families are owned by different addons and a shared enum
13885
- * is a central list that rots invisibly.
13886
- */
13887
- family: string(),
13888
- /**
13889
- * The NUMERIC device id — the same value every log line carries as
13890
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13891
- * cannot name the camera must not emit the entry, because a fleet total
13892
- * cannot answer the only question anybody asks of this surface.
13893
- */
13894
- deviceId: number().int().positive(),
13895
- /**
13896
- * A second dimension inside the family: the model / step id for an inference
13897
- * timeout, so "which camera AND which model" is one read. Absent when the
13898
- * family has a single variant.
13899
- */
13900
- variant: string().optional(),
13901
- /**
13902
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13903
- * differencing two reads must drop the interval when it changes, because the
13904
- * counter restarted from zero in a respawned runner. Same discipline as
13905
- * `LoadContribution.startedAtMs`.
13906
- */
13907
- sinceMs: number(),
13908
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13909
- atMs: number(),
13910
- /**
13911
- * THE DENOMINATOR — every attempt on this path for this camera in the
13912
- * window. A failure count published without it is the mistake this schema
13913
- * exists to make impossible.
13914
- */
13915
- attempts: number().int().nonnegative(),
13916
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13917
- succeeded: number().int().nonnegative(),
13918
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13919
- reasons: array(FailureReasonCountSchema).readonly()
13920
- });
13921
- method(_void(), array(FailureContributionSchema).readonly());
13922
- var LoadContributionSchema = object({
13923
- role: _enum([
13924
- "decode",
13925
- "transcode",
13926
- "recording",
13927
- "streaming",
13928
- "detection"
13929
- ]),
13930
- /**
13931
- * The NUMERIC device id — the same value every log line carries as
13932
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13933
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13934
- * contributor that cannot name its camera must not emit the entry at all,
13935
- * because an unnamed per-camera entry is indistinguishable from a shared one
13936
- * and would quietly turn one camera's cost into everybody's.
13937
- */
13938
- deviceId: number().int().positive().nullable(),
13939
- attribution: _enum([
13940
- "measured",
13941
- "accounted",
13942
- "unattributable"
13943
- ]),
13944
- /**
13945
- * What ONE entry is, in the contributor's own words — `615/high`,
13946
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13947
- * family and inventing a common one would lose the only information that
13948
- * makes two entries for the same camera distinguishable.
13949
- */
13950
- unit: string(),
13951
- /**
13952
- * The OS process this cost lives in, when there is one. Present so a
13953
- * consumer can (a) tell two generations of the same unit apart across a
13954
- * restart, and (b) subtract claimed processes from the node's process
13955
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13956
- * process of its own.
13957
- */
13958
- pid: number().int().positive().optional(),
13959
- /**
13960
- * When this generation started. The pid's incarnation marker: a consumer
13961
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13962
- * window when this changes, because the counter restarted from zero in a new
13963
- * process.
13964
- */
13965
- startedAtMs: number().optional(),
13966
- /**
13967
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13968
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13969
- * contribution is asked for.
13970
- *
13971
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13972
- * needs a sampler, and a new per-node sampler is the defect half of
13973
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13974
- * by whoever already keeps a history; a rate cannot be un-averaged.
13975
- *
13976
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13977
- * an entry with no process.
13978
- */
13979
- cpuSeconds: number().optional(),
13980
- /** Resident bytes of this unit's process, same source and same rules. */
13981
- rssBytes: number().optional()
13982
- });
13983
- method(_void(), array(LoadContributionSchema).readonly());
13984
- /**
13985
13985
  * `login-method` — collection cap through which auth addons contribute
13986
13986
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13987
13987
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18657,12 +18657,53 @@ var MediaFileKindEnum = _enum([
18657
18657
  "keyFrameSmall",
18658
18658
  "thumbnailSmall"
18659
18659
  ]);
18660
+ /**
18661
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18662
+ * ARE — never the bytes themselves.
18663
+ *
18664
+ * ## Why `url` and not `base64`
18665
+ *
18666
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18667
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18668
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18669
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18670
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18671
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18672
+ *
18673
+ * `url` points at the `event-media` data plane
18674
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18675
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18676
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18677
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18678
+ * no less protected than they were inside a `view`-level cap response — see
18679
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18680
+ * (per-device scoping).
18681
+ *
18682
+ * The URL is built from the row's **stored** key, which is not always its
18683
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18684
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18685
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18686
+ *
18687
+ * ## `base64` is TRANSITIONAL and is going away
18688
+ *
18689
+ * It is still populated for one reason: the deployed viewer's track-detail
18690
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18691
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18692
+ * triangle — not as absence. Removing the field before that viewer ships is an
18693
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18694
+ * delete this line and the `withBytes` pass-through in
18695
+ * `analytics-query-facade.ts`; nothing else reads it.
18696
+ */
18660
18697
  var MediaFileSchema = object({
18661
18698
  key: string(),
18662
18699
  kind: MediaFileKindEnum,
18663
- base64: string(),
18664
18700
  sizeBytes: number(),
18665
18701
  timestamp: number()
18702
+ }).extend({
18703
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18704
+ url: string(),
18705
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18706
+ base64: string()
18666
18707
  });
18667
18708
  /**
18668
18709
  * One media row WITHOUT its bytes.
@@ -18674,7 +18715,9 @@ var MediaFileSchema = object({
18674
18715
  * blocks the whole view.
18675
18716
  *
18676
18717
  * `sizeBytes` is carried because it is what lets a client decide between the
18677
- * stored blob and a `?variant=thumb` rendering without fetching either.
18718
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18719
+ * `url` because a client that had to build the plane path itself is a second
18720
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18678
18721
  */
18679
18722
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18680
18723
  /**
@@ -19363,6 +19406,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19363
19406
  }), array(MediaFileSchema).readonly()), method(object({
19364
19407
  trackId: string(),
19365
19408
  deviceId: number()
19409
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19410
+ eventId: string(),
19411
+ deviceId: number()
19366
19412
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19367
19413
  kind: "mutation",
19368
19414
  auth: "admin"
@@ -23623,10 +23669,24 @@ var FaceClusterSchema = object({
23623
23669
  size: number().int(),
23624
23670
  cohesion: number()
23625
23671
  });
23672
+ /**
23673
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23674
+ * are — never the bytes.
23675
+ *
23676
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23677
+ * track/event contract) is still populated because a deployed viewer requires
23678
+ * the field to parse a row at all; this method has no such reader. Its ONE
23679
+ * caller is the admin UI's detail modal, which was building
23680
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23681
+ * dialog already rendering its key FRAME from the `event-media` plane.
23682
+ *
23683
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23684
+ * media key directly, so this needed no new plane and no new access decision.
23685
+ */
23626
23686
  var MediaFileLiteSchema$1 = object({
23627
23687
  key: string(),
23628
23688
  kind: string(),
23629
- base64: string(),
23689
+ url: string(),
23630
23690
  sizeBytes: number(),
23631
23691
  timestamp: number()
23632
23692
  });
@@ -25878,10 +25938,24 @@ var PlateInfoSchema = object({
25878
25938
  */
25879
25939
  cropUrl: string().optional()
25880
25940
  });
25941
+ /**
25942
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25943
+ * are — never the bytes.
25944
+ *
25945
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25946
+ * track/event contract) is still populated because a deployed viewer requires
25947
+ * the field to parse a row at all; this method has no such reader. Its ONE
25948
+ * caller is the admin UI's detail modal, which was building
25949
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25950
+ * dialog already rendering its key FRAME from the `event-media` plane.
25951
+ *
25952
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25953
+ * media key directly, so this needed no new plane and no new access decision.
25954
+ */
25881
25955
  var MediaFileLiteSchema = object({
25882
25956
  key: string(),
25883
25957
  kind: string(),
25884
- base64: string(),
25958
+ url: string(),
25885
25959
  sizeBytes: number(),
25886
25960
  timestamp: number()
25887
25961
  });
@@ -31894,6 +31968,12 @@ Object.freeze({
31894
31968
  addonId: null,
31895
31969
  access: "view"
31896
31970
  },
31971
+ "pipelineAnalytics.listEventMedia": {
31972
+ capName: "pipeline-analytics",
31973
+ capScope: "device",
31974
+ addonId: null,
31975
+ access: "view"
31976
+ },
31897
31977
  "pipelineAnalytics.listGroups": {
31898
31978
  capName: "pipeline-analytics",
31899
31979
  capScope: "device",
@@ -35517,6 +35597,11 @@ Object.freeze({
35517
35597
  form: "array",
35518
35598
  optional: false
35519
35599
  }],
35600
+ "pipelineAnalytics.listEventMedia": [{
35601
+ name: "deviceId",
35602
+ form: "single",
35603
+ optional: false
35604
+ }],
35520
35605
  "pipelineAnalytics.listGroups": [{
35521
35606
  name: "deviceIds",
35522
35607
  form: "array",
@@ -13188,6 +13188,114 @@ method(object({
13188
13188
  height: number()
13189
13189
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13190
13190
  /**
13191
+ * `failure-contribution` — the capability an addon reports its OWN losses
13192
+ * through, per camera, with the denominator attached. It stores nothing.
13193
+ *
13194
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13195
+ *
13196
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13197
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13198
+ * copied: the contributor reports what it already knows, hub-main adds only
13199
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13200
+ * somebody to forget to edit.
13201
+ *
13202
+ * They are not merged, because their invariants are opposites:
13203
+ *
13204
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13205
+ * claim a camera cost nothing, which is a measurement nobody made;
13206
+ * - a `failure-contribution` zero is the **most valuable value on the
13207
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13208
+ * and it is exactly what an absent entry cannot say.
13209
+ *
13210
+ * Putting a loss counter on a cost entry would also break the reconciliation
13211
+ * that gives `load-contribution` its point: contributions are subtracted from
13212
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13213
+ * has no process.
13214
+ *
13215
+ * ## Why not a log line, since the counters already exist
13216
+ *
13217
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13218
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13219
+ * ends in a log line, and a log line is the thing the operator asked to stop
13220
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13221
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13222
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13223
+ * media blackout were both diagnosed. The counters stay; this is where they can
13224
+ * be READ.
13225
+ *
13226
+ * ## The rate is served with its denominator or not at all
13227
+ *
13228
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13229
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13230
+ * than yesterday" and was **flat across twelve hours** once divided by the
13231
+ * successes on the same path. A surface that publishes only the numerator
13232
+ * reproduces that mistake on every read.
13233
+ *
13234
+ * ## Shape
13235
+ *
13236
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13237
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13238
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13239
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13240
+ * a forked runner's entries reach hub-main over transport that already exists.
13241
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13242
+ * result through `system.getFailureContributions`.
13243
+ */
13244
+ var FailureReasonCountSchema = object({
13245
+ /**
13246
+ * Why the attempt did not land, in the contributor's own vocabulary —
13247
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13248
+ * strings that already appear in this repo's logs and, where one exists, the
13249
+ * same string the per-track `previewMissReason` records (D276): a second
13250
+ * vocabulary for the same loss would make the row and the counter
13251
+ * un-joinable.
13252
+ */
13253
+ reason: string(),
13254
+ count: number().int().nonnegative()
13255
+ });
13256
+ var FailureContributionSchema = object({
13257
+ /**
13258
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13259
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13260
+ * `unit` free: the families are owned by different addons and a shared enum
13261
+ * is a central list that rots invisibly.
13262
+ */
13263
+ family: string(),
13264
+ /**
13265
+ * The NUMERIC device id — the same value every log line carries as
13266
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13267
+ * cannot name the camera must not emit the entry, because a fleet total
13268
+ * cannot answer the only question anybody asks of this surface.
13269
+ */
13270
+ deviceId: number().int().positive(),
13271
+ /**
13272
+ * A second dimension inside the family: the model / step id for an inference
13273
+ * timeout, so "which camera AND which model" is one read. Absent when the
13274
+ * family has a single variant.
13275
+ */
13276
+ variant: string().optional(),
13277
+ /**
13278
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13279
+ * differencing two reads must drop the interval when it changes, because the
13280
+ * counter restarted from zero in a respawned runner. Same discipline as
13281
+ * `LoadContribution.startedAtMs`.
13282
+ */
13283
+ sinceMs: number(),
13284
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13285
+ atMs: number(),
13286
+ /**
13287
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13288
+ * window. A failure count published without it is the mistake this schema
13289
+ * exists to make impossible.
13290
+ */
13291
+ attempts: number().int().nonnegative(),
13292
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13293
+ succeeded: number().int().nonnegative(),
13294
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13295
+ reasons: array(FailureReasonCountSchema).readonly()
13296
+ });
13297
+ method(_void(), array(FailureContributionSchema).readonly());
13298
+ /**
13191
13299
  * filesystem-browse — per-node capability for browsing the node's local
13192
13300
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13193
13301
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13709,6 +13817,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13709
13817
  kind: "mutation",
13710
13818
  auth: "admin"
13711
13819
  });
13820
+ var LoadContributionSchema = object({
13821
+ role: _enum([
13822
+ "decode",
13823
+ "transcode",
13824
+ "recording",
13825
+ "streaming",
13826
+ "detection"
13827
+ ]),
13828
+ /**
13829
+ * The NUMERIC device id — the same value every log line carries as
13830
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13831
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13832
+ * contributor that cannot name its camera must not emit the entry at all,
13833
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13834
+ * and would quietly turn one camera's cost into everybody's.
13835
+ */
13836
+ deviceId: number().int().positive().nullable(),
13837
+ attribution: _enum([
13838
+ "measured",
13839
+ "accounted",
13840
+ "unattributable"
13841
+ ]),
13842
+ /**
13843
+ * What ONE entry is, in the contributor's own words — `615/high`,
13844
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13845
+ * family and inventing a common one would lose the only information that
13846
+ * makes two entries for the same camera distinguishable.
13847
+ */
13848
+ unit: string(),
13849
+ /**
13850
+ * The OS process this cost lives in, when there is one. Present so a
13851
+ * consumer can (a) tell two generations of the same unit apart across a
13852
+ * restart, and (b) subtract claimed processes from the node's process
13853
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13854
+ * process of its own.
13855
+ */
13856
+ pid: number().int().positive().optional(),
13857
+ /**
13858
+ * When this generation started. The pid's incarnation marker: a consumer
13859
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13860
+ * window when this changes, because the counter restarted from zero in a new
13861
+ * process.
13862
+ */
13863
+ startedAtMs: number().optional(),
13864
+ /**
13865
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13866
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13867
+ * contribution is asked for.
13868
+ *
13869
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13870
+ * needs a sampler, and a new per-node sampler is the defect half of
13871
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13872
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13873
+ *
13874
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13875
+ * an entry with no process.
13876
+ */
13877
+ cpuSeconds: number().optional(),
13878
+ /** Resident bytes of this unit's process, same source and same rules. */
13879
+ rssBytes: number().optional()
13880
+ });
13881
+ method(_void(), array(LoadContributionSchema).readonly());
13712
13882
  /**
13713
13883
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13714
13884
  * through. It stores nothing.
@@ -13785,176 +13955,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13785
13955
  tags: record(string(), string()).optional()
13786
13956
  }), array(LogEntrySchema).readonly());
13787
13957
  /**
13788
- * `failure-contribution` — the capability an addon reports its OWN losses
13789
- * through, per camera, with the denominator attached. It stores nothing.
13790
- *
13791
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13792
- *
13793
- * `load-contribution` answers *what did this camera COST*. This answers *what
13794
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13795
- * copied: the contributor reports what it already knows, hub-main adds only
13796
- * `addonId`, nothing needs global knowledge, and there is no central list for
13797
- * somebody to forget to edit.
13798
- *
13799
- * They are not merged, because their invariants are opposites:
13800
- *
13801
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13802
- * claim a camera cost nothing, which is a measurement nobody made;
13803
- * - a `failure-contribution` zero is the **most valuable value on the
13804
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13805
- * and it is exactly what an absent entry cannot say.
13806
- *
13807
- * Putting a loss counter on a cost entry would also break the reconciliation
13808
- * that gives `load-contribution` its point: contributions are subtracted from
13809
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13810
- * has no process.
13811
- *
13812
- * ## Why not a log line, since the counters already exist
13813
- *
13814
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13815
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13816
- * ends in a log line, and a log line is the thing the operator asked to stop
13817
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13818
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13819
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13820
- * media blackout were both diagnosed. The counters stay; this is where they can
13821
- * be READ.
13822
- *
13823
- * ## The rate is served with its denominator or not at all
13824
- *
13825
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13826
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13827
- * than yesterday" and was **flat across twelve hours** once divided by the
13828
- * successes on the same path. A surface that publishes only the numerator
13829
- * reproduces that mistake on every read.
13830
- *
13831
- * ## Shape
13832
- *
13833
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13834
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13835
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13836
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13837
- * a forked runner's entries reach hub-main over transport that already exists.
13838
- * No new UDS message, no second registry (D3). The operator reads the assembled
13839
- * result through `system.getFailureContributions`.
13840
- */
13841
- var FailureReasonCountSchema = object({
13842
- /**
13843
- * Why the attempt did not land, in the contributor's own vocabulary —
13844
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13845
- * strings that already appear in this repo's logs and, where one exists, the
13846
- * same string the per-track `previewMissReason` records (D276): a second
13847
- * vocabulary for the same loss would make the row and the counter
13848
- * un-joinable.
13849
- */
13850
- reason: string(),
13851
- count: number().int().nonnegative()
13852
- });
13853
- var FailureContributionSchema = object({
13854
- /**
13855
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13856
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13857
- * `unit` free: the families are owned by different addons and a shared enum
13858
- * is a central list that rots invisibly.
13859
- */
13860
- family: string(),
13861
- /**
13862
- * The NUMERIC device id — the same value every log line carries as
13863
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13864
- * cannot name the camera must not emit the entry, because a fleet total
13865
- * cannot answer the only question anybody asks of this surface.
13866
- */
13867
- deviceId: number().int().positive(),
13868
- /**
13869
- * A second dimension inside the family: the model / step id for an inference
13870
- * timeout, so "which camera AND which model" is one read. Absent when the
13871
- * family has a single variant.
13872
- */
13873
- variant: string().optional(),
13874
- /**
13875
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13876
- * differencing two reads must drop the interval when it changes, because the
13877
- * counter restarted from zero in a respawned runner. Same discipline as
13878
- * `LoadContribution.startedAtMs`.
13879
- */
13880
- sinceMs: number(),
13881
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13882
- atMs: number(),
13883
- /**
13884
- * THE DENOMINATOR — every attempt on this path for this camera in the
13885
- * window. A failure count published without it is the mistake this schema
13886
- * exists to make impossible.
13887
- */
13888
- attempts: number().int().nonnegative(),
13889
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13890
- succeeded: number().int().nonnegative(),
13891
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13892
- reasons: array(FailureReasonCountSchema).readonly()
13893
- });
13894
- method(_void(), array(FailureContributionSchema).readonly());
13895
- var LoadContributionSchema = object({
13896
- role: _enum([
13897
- "decode",
13898
- "transcode",
13899
- "recording",
13900
- "streaming",
13901
- "detection"
13902
- ]),
13903
- /**
13904
- * The NUMERIC device id — the same value every log line carries as
13905
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13906
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13907
- * contributor that cannot name its camera must not emit the entry at all,
13908
- * because an unnamed per-camera entry is indistinguishable from a shared one
13909
- * and would quietly turn one camera's cost into everybody's.
13910
- */
13911
- deviceId: number().int().positive().nullable(),
13912
- attribution: _enum([
13913
- "measured",
13914
- "accounted",
13915
- "unattributable"
13916
- ]),
13917
- /**
13918
- * What ONE entry is, in the contributor's own words — `615/high`,
13919
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13920
- * family and inventing a common one would lose the only information that
13921
- * makes two entries for the same camera distinguishable.
13922
- */
13923
- unit: string(),
13924
- /**
13925
- * The OS process this cost lives in, when there is one. Present so a
13926
- * consumer can (a) tell two generations of the same unit apart across a
13927
- * restart, and (b) subtract claimed processes from the node's process
13928
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13929
- * process of its own.
13930
- */
13931
- pid: number().int().positive().optional(),
13932
- /**
13933
- * When this generation started. The pid's incarnation marker: a consumer
13934
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13935
- * window when this changes, because the counter restarted from zero in a new
13936
- * process.
13937
- */
13938
- startedAtMs: number().optional(),
13939
- /**
13940
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13941
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13942
- * contribution is asked for.
13943
- *
13944
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13945
- * needs a sampler, and a new per-node sampler is the defect half of
13946
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13947
- * by whoever already keeps a history; a rate cannot be un-averaged.
13948
- *
13949
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13950
- * an entry with no process.
13951
- */
13952
- cpuSeconds: number().optional(),
13953
- /** Resident bytes of this unit's process, same source and same rules. */
13954
- rssBytes: number().optional()
13955
- });
13956
- method(_void(), array(LoadContributionSchema).readonly());
13957
- /**
13958
13958
  * `login-method` — collection cap through which auth addons contribute
13959
13959
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13960
13960
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18630,12 +18630,53 @@ var MediaFileKindEnum = _enum([
18630
18630
  "keyFrameSmall",
18631
18631
  "thumbnailSmall"
18632
18632
  ]);
18633
+ /**
18634
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18635
+ * ARE — never the bytes themselves.
18636
+ *
18637
+ * ## Why `url` and not `base64`
18638
+ *
18639
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18640
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18641
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18642
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18643
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18644
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18645
+ *
18646
+ * `url` points at the `event-media` data plane
18647
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18648
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18649
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18650
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18651
+ * no less protected than they were inside a `view`-level cap response — see
18652
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18653
+ * (per-device scoping).
18654
+ *
18655
+ * The URL is built from the row's **stored** key, which is not always its
18656
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18657
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18658
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18659
+ *
18660
+ * ## `base64` is TRANSITIONAL and is going away
18661
+ *
18662
+ * It is still populated for one reason: the deployed viewer's track-detail
18663
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18664
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18665
+ * triangle — not as absence. Removing the field before that viewer ships is an
18666
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18667
+ * delete this line and the `withBytes` pass-through in
18668
+ * `analytics-query-facade.ts`; nothing else reads it.
18669
+ */
18633
18670
  var MediaFileSchema = object({
18634
18671
  key: string(),
18635
18672
  kind: MediaFileKindEnum,
18636
- base64: string(),
18637
18673
  sizeBytes: number(),
18638
18674
  timestamp: number()
18675
+ }).extend({
18676
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18677
+ url: string(),
18678
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18679
+ base64: string()
18639
18680
  });
18640
18681
  /**
18641
18682
  * One media row WITHOUT its bytes.
@@ -18647,7 +18688,9 @@ var MediaFileSchema = object({
18647
18688
  * blocks the whole view.
18648
18689
  *
18649
18690
  * `sizeBytes` is carried because it is what lets a client decide between the
18650
- * stored blob and a `?variant=thumb` rendering without fetching either.
18691
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18692
+ * `url` because a client that had to build the plane path itself is a second
18693
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18651
18694
  */
18652
18695
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18653
18696
  /**
@@ -19336,6 +19379,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19336
19379
  }), array(MediaFileSchema).readonly()), method(object({
19337
19380
  trackId: string(),
19338
19381
  deviceId: number()
19382
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19383
+ eventId: string(),
19384
+ deviceId: number()
19339
19385
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19340
19386
  kind: "mutation",
19341
19387
  auth: "admin"
@@ -23596,10 +23642,24 @@ var FaceClusterSchema = object({
23596
23642
  size: number().int(),
23597
23643
  cohesion: number()
23598
23644
  });
23645
+ /**
23646
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23647
+ * are — never the bytes.
23648
+ *
23649
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23650
+ * track/event contract) is still populated because a deployed viewer requires
23651
+ * the field to parse a row at all; this method has no such reader. Its ONE
23652
+ * caller is the admin UI's detail modal, which was building
23653
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23654
+ * dialog already rendering its key FRAME from the `event-media` plane.
23655
+ *
23656
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23657
+ * media key directly, so this needed no new plane and no new access decision.
23658
+ */
23599
23659
  var MediaFileLiteSchema$1 = object({
23600
23660
  key: string(),
23601
23661
  kind: string(),
23602
- base64: string(),
23662
+ url: string(),
23603
23663
  sizeBytes: number(),
23604
23664
  timestamp: number()
23605
23665
  });
@@ -25851,10 +25911,24 @@ var PlateInfoSchema = object({
25851
25911
  */
25852
25912
  cropUrl: string().optional()
25853
25913
  });
25914
+ /**
25915
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25916
+ * are — never the bytes.
25917
+ *
25918
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25919
+ * track/event contract) is still populated because a deployed viewer requires
25920
+ * the field to parse a row at all; this method has no such reader. Its ONE
25921
+ * caller is the admin UI's detail modal, which was building
25922
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25923
+ * dialog already rendering its key FRAME from the `event-media` plane.
25924
+ *
25925
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25926
+ * media key directly, so this needed no new plane and no new access decision.
25927
+ */
25854
25928
  var MediaFileLiteSchema = object({
25855
25929
  key: string(),
25856
25930
  kind: string(),
25857
- base64: string(),
25931
+ url: string(),
25858
25932
  sizeBytes: number(),
25859
25933
  timestamp: number()
25860
25934
  });
@@ -31867,6 +31941,12 @@ Object.freeze({
31867
31941
  addonId: null,
31868
31942
  access: "view"
31869
31943
  },
31944
+ "pipelineAnalytics.listEventMedia": {
31945
+ capName: "pipeline-analytics",
31946
+ capScope: "device",
31947
+ addonId: null,
31948
+ access: "view"
31949
+ },
31870
31950
  "pipelineAnalytics.listGroups": {
31871
31951
  capName: "pipeline-analytics",
31872
31952
  capScope: "device",
@@ -35490,6 +35570,11 @@ Object.freeze({
35490
35570
  form: "array",
35491
35571
  optional: false
35492
35572
  }],
35573
+ "pipelineAnalytics.listEventMedia": [{
35574
+ name: "deviceId",
35575
+ form: "single",
35576
+ optional: false
35577
+ }],
35493
35578
  "pipelineAnalytics.listGroups": [{
35494
35579
  name: "deviceIds",
35495
35580
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-export-alexa",
3
- "version": "1.2.52",
3
+ "version": "1.2.53",
4
4
  "description": "Alexa Smart Home Skill export — hub-side OAuth provider + directive handler. The companion AWS Lambda forwards Alexa directives to this addon's /addon/export-alexa/directive endpoint.",
5
5
  "keywords": [
6
6
  "camstack",