@camstack/addon-provider-velux 0.2.46 → 0.2.47

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
@@ -13184,6 +13184,114 @@ method(object({
13184
13184
  height: number()
13185
13185
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13186
13186
  /**
13187
+ * `failure-contribution` — the capability an addon reports its OWN losses
13188
+ * through, per camera, with the denominator attached. It stores nothing.
13189
+ *
13190
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13191
+ *
13192
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13193
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13194
+ * copied: the contributor reports what it already knows, hub-main adds only
13195
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13196
+ * somebody to forget to edit.
13197
+ *
13198
+ * They are not merged, because their invariants are opposites:
13199
+ *
13200
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13201
+ * claim a camera cost nothing, which is a measurement nobody made;
13202
+ * - a `failure-contribution` zero is the **most valuable value on the
13203
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13204
+ * and it is exactly what an absent entry cannot say.
13205
+ *
13206
+ * Putting a loss counter on a cost entry would also break the reconciliation
13207
+ * that gives `load-contribution` its point: contributions are subtracted from
13208
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13209
+ * has no process.
13210
+ *
13211
+ * ## Why not a log line, since the counters already exist
13212
+ *
13213
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13214
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13215
+ * ends in a log line, and a log line is the thing the operator asked to stop
13216
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13217
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13218
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13219
+ * media blackout were both diagnosed. The counters stay; this is where they can
13220
+ * be READ.
13221
+ *
13222
+ * ## The rate is served with its denominator or not at all
13223
+ *
13224
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13225
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13226
+ * than yesterday" and was **flat across twelve hours** once divided by the
13227
+ * successes on the same path. A surface that publishes only the numerator
13228
+ * reproduces that mistake on every read.
13229
+ *
13230
+ * ## Shape
13231
+ *
13232
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13233
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13234
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13235
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13236
+ * a forked runner's entries reach hub-main over transport that already exists.
13237
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13238
+ * result through `system.getFailureContributions`.
13239
+ */
13240
+ var FailureReasonCountSchema = object({
13241
+ /**
13242
+ * Why the attempt did not land, in the contributor's own vocabulary —
13243
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13244
+ * strings that already appear in this repo's logs and, where one exists, the
13245
+ * same string the per-track `previewMissReason` records (D276): a second
13246
+ * vocabulary for the same loss would make the row and the counter
13247
+ * un-joinable.
13248
+ */
13249
+ reason: string(),
13250
+ count: number().int().nonnegative()
13251
+ });
13252
+ var FailureContributionSchema = object({
13253
+ /**
13254
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13255
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13256
+ * `unit` free: the families are owned by different addons and a shared enum
13257
+ * is a central list that rots invisibly.
13258
+ */
13259
+ family: string(),
13260
+ /**
13261
+ * The NUMERIC device id — the same value every log line carries as
13262
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13263
+ * cannot name the camera must not emit the entry, because a fleet total
13264
+ * cannot answer the only question anybody asks of this surface.
13265
+ */
13266
+ deviceId: number().int().positive(),
13267
+ /**
13268
+ * A second dimension inside the family: the model / step id for an inference
13269
+ * timeout, so "which camera AND which model" is one read. Absent when the
13270
+ * family has a single variant.
13271
+ */
13272
+ variant: string().optional(),
13273
+ /**
13274
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13275
+ * differencing two reads must drop the interval when it changes, because the
13276
+ * counter restarted from zero in a respawned runner. Same discipline as
13277
+ * `LoadContribution.startedAtMs`.
13278
+ */
13279
+ sinceMs: number(),
13280
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13281
+ atMs: number(),
13282
+ /**
13283
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13284
+ * window. A failure count published without it is the mistake this schema
13285
+ * exists to make impossible.
13286
+ */
13287
+ attempts: number().int().nonnegative(),
13288
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13289
+ succeeded: number().int().nonnegative(),
13290
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13291
+ reasons: array(FailureReasonCountSchema).readonly()
13292
+ });
13293
+ method(_void(), array(FailureContributionSchema).readonly());
13294
+ /**
13187
13295
  * filesystem-browse — per-node capability for browsing the node's local
13188
13296
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13189
13297
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13705,6 +13813,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13705
13813
  kind: "mutation",
13706
13814
  auth: "admin"
13707
13815
  });
13816
+ var LoadContributionSchema = object({
13817
+ role: _enum([
13818
+ "decode",
13819
+ "transcode",
13820
+ "recording",
13821
+ "streaming",
13822
+ "detection"
13823
+ ]),
13824
+ /**
13825
+ * The NUMERIC device id — the same value every log line carries as
13826
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13827
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13828
+ * contributor that cannot name its camera must not emit the entry at all,
13829
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13830
+ * and would quietly turn one camera's cost into everybody's.
13831
+ */
13832
+ deviceId: number().int().positive().nullable(),
13833
+ attribution: _enum([
13834
+ "measured",
13835
+ "accounted",
13836
+ "unattributable"
13837
+ ]),
13838
+ /**
13839
+ * What ONE entry is, in the contributor's own words — `615/high`,
13840
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13841
+ * family and inventing a common one would lose the only information that
13842
+ * makes two entries for the same camera distinguishable.
13843
+ */
13844
+ unit: string(),
13845
+ /**
13846
+ * The OS process this cost lives in, when there is one. Present so a
13847
+ * consumer can (a) tell two generations of the same unit apart across a
13848
+ * restart, and (b) subtract claimed processes from the node's process
13849
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13850
+ * process of its own.
13851
+ */
13852
+ pid: number().int().positive().optional(),
13853
+ /**
13854
+ * When this generation started. The pid's incarnation marker: a consumer
13855
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13856
+ * window when this changes, because the counter restarted from zero in a new
13857
+ * process.
13858
+ */
13859
+ startedAtMs: number().optional(),
13860
+ /**
13861
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13862
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13863
+ * contribution is asked for.
13864
+ *
13865
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13866
+ * needs a sampler, and a new per-node sampler is the defect half of
13867
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13868
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13869
+ *
13870
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13871
+ * an entry with no process.
13872
+ */
13873
+ cpuSeconds: number().optional(),
13874
+ /** Resident bytes of this unit's process, same source and same rules. */
13875
+ rssBytes: number().optional()
13876
+ });
13877
+ method(_void(), array(LoadContributionSchema).readonly());
13708
13878
  /**
13709
13879
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13710
13880
  * through. It stores nothing.
@@ -13781,176 +13951,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13781
13951
  tags: record(string(), string()).optional()
13782
13952
  }), array(LogEntrySchema).readonly());
13783
13953
  /**
13784
- * `failure-contribution` — the capability an addon reports its OWN losses
13785
- * through, per camera, with the denominator attached. It stores nothing.
13786
- *
13787
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13788
- *
13789
- * `load-contribution` answers *what did this camera COST*. This answers *what
13790
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13791
- * copied: the contributor reports what it already knows, hub-main adds only
13792
- * `addonId`, nothing needs global knowledge, and there is no central list for
13793
- * somebody to forget to edit.
13794
- *
13795
- * They are not merged, because their invariants are opposites:
13796
- *
13797
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13798
- * claim a camera cost nothing, which is a measurement nobody made;
13799
- * - a `failure-contribution` zero is the **most valuable value on the
13800
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13801
- * and it is exactly what an absent entry cannot say.
13802
- *
13803
- * Putting a loss counter on a cost entry would also break the reconciliation
13804
- * that gives `load-contribution` its point: contributions are subtracted from
13805
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13806
- * has no process.
13807
- *
13808
- * ## Why not a log line, since the counters already exist
13809
- *
13810
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13811
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13812
- * ends in a log line, and a log line is the thing the operator asked to stop
13813
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13814
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13815
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13816
- * media blackout were both diagnosed. The counters stay; this is where they can
13817
- * be READ.
13818
- *
13819
- * ## The rate is served with its denominator or not at all
13820
- *
13821
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13822
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13823
- * than yesterday" and was **flat across twelve hours** once divided by the
13824
- * successes on the same path. A surface that publishes only the numerator
13825
- * reproduces that mistake on every read.
13826
- *
13827
- * ## Shape
13828
- *
13829
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13830
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13831
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13832
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13833
- * a forked runner's entries reach hub-main over transport that already exists.
13834
- * No new UDS message, no second registry (D3). The operator reads the assembled
13835
- * result through `system.getFailureContributions`.
13836
- */
13837
- var FailureReasonCountSchema = object({
13838
- /**
13839
- * Why the attempt did not land, in the contributor's own vocabulary —
13840
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13841
- * strings that already appear in this repo's logs and, where one exists, the
13842
- * same string the per-track `previewMissReason` records (D276): a second
13843
- * vocabulary for the same loss would make the row and the counter
13844
- * un-joinable.
13845
- */
13846
- reason: string(),
13847
- count: number().int().nonnegative()
13848
- });
13849
- var FailureContributionSchema = object({
13850
- /**
13851
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13852
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13853
- * `unit` free: the families are owned by different addons and a shared enum
13854
- * is a central list that rots invisibly.
13855
- */
13856
- family: string(),
13857
- /**
13858
- * The NUMERIC device id — the same value every log line carries as
13859
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13860
- * cannot name the camera must not emit the entry, because a fleet total
13861
- * cannot answer the only question anybody asks of this surface.
13862
- */
13863
- deviceId: number().int().positive(),
13864
- /**
13865
- * A second dimension inside the family: the model / step id for an inference
13866
- * timeout, so "which camera AND which model" is one read. Absent when the
13867
- * family has a single variant.
13868
- */
13869
- variant: string().optional(),
13870
- /**
13871
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13872
- * differencing two reads must drop the interval when it changes, because the
13873
- * counter restarted from zero in a respawned runner. Same discipline as
13874
- * `LoadContribution.startedAtMs`.
13875
- */
13876
- sinceMs: number(),
13877
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13878
- atMs: number(),
13879
- /**
13880
- * THE DENOMINATOR — every attempt on this path for this camera in the
13881
- * window. A failure count published without it is the mistake this schema
13882
- * exists to make impossible.
13883
- */
13884
- attempts: number().int().nonnegative(),
13885
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13886
- succeeded: number().int().nonnegative(),
13887
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13888
- reasons: array(FailureReasonCountSchema).readonly()
13889
- });
13890
- method(_void(), array(FailureContributionSchema).readonly());
13891
- var LoadContributionSchema = object({
13892
- role: _enum([
13893
- "decode",
13894
- "transcode",
13895
- "recording",
13896
- "streaming",
13897
- "detection"
13898
- ]),
13899
- /**
13900
- * The NUMERIC device id — the same value every log line carries as
13901
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13902
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13903
- * contributor that cannot name its camera must not emit the entry at all,
13904
- * because an unnamed per-camera entry is indistinguishable from a shared one
13905
- * and would quietly turn one camera's cost into everybody's.
13906
- */
13907
- deviceId: number().int().positive().nullable(),
13908
- attribution: _enum([
13909
- "measured",
13910
- "accounted",
13911
- "unattributable"
13912
- ]),
13913
- /**
13914
- * What ONE entry is, in the contributor's own words — `615/high`,
13915
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13916
- * family and inventing a common one would lose the only information that
13917
- * makes two entries for the same camera distinguishable.
13918
- */
13919
- unit: string(),
13920
- /**
13921
- * The OS process this cost lives in, when there is one. Present so a
13922
- * consumer can (a) tell two generations of the same unit apart across a
13923
- * restart, and (b) subtract claimed processes from the node's process
13924
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13925
- * process of its own.
13926
- */
13927
- pid: number().int().positive().optional(),
13928
- /**
13929
- * When this generation started. The pid's incarnation marker: a consumer
13930
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13931
- * window when this changes, because the counter restarted from zero in a new
13932
- * process.
13933
- */
13934
- startedAtMs: number().optional(),
13935
- /**
13936
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13937
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13938
- * contribution is asked for.
13939
- *
13940
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13941
- * needs a sampler, and a new per-node sampler is the defect half of
13942
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13943
- * by whoever already keeps a history; a rate cannot be un-averaged.
13944
- *
13945
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13946
- * an entry with no process.
13947
- */
13948
- cpuSeconds: number().optional(),
13949
- /** Resident bytes of this unit's process, same source and same rules. */
13950
- rssBytes: number().optional()
13951
- });
13952
- method(_void(), array(LoadContributionSchema).readonly());
13953
- /**
13954
13954
  * `login-method` — collection cap through which auth addons contribute
13955
13955
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13956
13956
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18688,12 +18688,53 @@ var MediaFileKindEnum = _enum([
18688
18688
  "keyFrameSmall",
18689
18689
  "thumbnailSmall"
18690
18690
  ]);
18691
+ /**
18692
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18693
+ * ARE — never the bytes themselves.
18694
+ *
18695
+ * ## Why `url` and not `base64`
18696
+ *
18697
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18698
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18699
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18700
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18701
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18702
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18703
+ *
18704
+ * `url` points at the `event-media` data plane
18705
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18706
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18707
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18708
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18709
+ * no less protected than they were inside a `view`-level cap response — see
18710
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18711
+ * (per-device scoping).
18712
+ *
18713
+ * The URL is built from the row's **stored** key, which is not always its
18714
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18715
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18716
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18717
+ *
18718
+ * ## `base64` is TRANSITIONAL and is going away
18719
+ *
18720
+ * It is still populated for one reason: the deployed viewer's track-detail
18721
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18722
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18723
+ * triangle — not as absence. Removing the field before that viewer ships is an
18724
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18725
+ * delete this line and the `withBytes` pass-through in
18726
+ * `analytics-query-facade.ts`; nothing else reads it.
18727
+ */
18691
18728
  var MediaFileSchema = object({
18692
18729
  key: string(),
18693
18730
  kind: MediaFileKindEnum,
18694
- base64: string(),
18695
18731
  sizeBytes: number(),
18696
18732
  timestamp: number()
18733
+ }).extend({
18734
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18735
+ url: string(),
18736
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18737
+ base64: string()
18697
18738
  });
18698
18739
  /**
18699
18740
  * One media row WITHOUT its bytes.
@@ -18705,7 +18746,9 @@ var MediaFileSchema = object({
18705
18746
  * blocks the whole view.
18706
18747
  *
18707
18748
  * `sizeBytes` is carried because it is what lets a client decide between the
18708
- * stored blob and a `?variant=thumb` rendering without fetching either.
18749
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18750
+ * `url` because a client that had to build the plane path itself is a second
18751
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18709
18752
  */
18710
18753
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18711
18754
  /**
@@ -19394,6 +19437,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19394
19437
  }), array(MediaFileSchema).readonly()), method(object({
19395
19438
  trackId: string(),
19396
19439
  deviceId: number()
19440
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19441
+ eventId: string(),
19442
+ deviceId: number()
19397
19443
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19398
19444
  kind: "mutation",
19399
19445
  auth: "admin"
@@ -24384,10 +24430,24 @@ var FaceClusterSchema = object({
24384
24430
  size: number().int(),
24385
24431
  cohesion: number()
24386
24432
  });
24433
+ /**
24434
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24435
+ * are — never the bytes.
24436
+ *
24437
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24438
+ * track/event contract) is still populated because a deployed viewer requires
24439
+ * the field to parse a row at all; this method has no such reader. Its ONE
24440
+ * caller is the admin UI's detail modal, which was building
24441
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24442
+ * dialog already rendering its key FRAME from the `event-media` plane.
24443
+ *
24444
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24445
+ * media key directly, so this needed no new plane and no new access decision.
24446
+ */
24387
24447
  var MediaFileLiteSchema$1 = object({
24388
24448
  key: string(),
24389
24449
  kind: string(),
24390
- base64: string(),
24450
+ url: string(),
24391
24451
  sizeBytes: number(),
24392
24452
  timestamp: number()
24393
24453
  });
@@ -27409,10 +27469,24 @@ var PlateInfoSchema = object({
27409
27469
  */
27410
27470
  cropUrl: string().optional()
27411
27471
  });
27472
+ /**
27473
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27474
+ * are — never the bytes.
27475
+ *
27476
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27477
+ * track/event contract) is still populated because a deployed viewer requires
27478
+ * the field to parse a row at all; this method has no such reader. Its ONE
27479
+ * caller is the admin UI's detail modal, which was building
27480
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27481
+ * dialog already rendering its key FRAME from the `event-media` plane.
27482
+ *
27483
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27484
+ * media key directly, so this needed no new plane and no new access decision.
27485
+ */
27412
27486
  var MediaFileLiteSchema = object({
27413
27487
  key: string(),
27414
27488
  kind: string(),
27415
- base64: string(),
27489
+ url: string(),
27416
27490
  sizeBytes: number(),
27417
27491
  timestamp: number()
27418
27492
  });
@@ -35309,6 +35383,12 @@ Object.freeze({
35309
35383
  addonId: null,
35310
35384
  access: "view"
35311
35385
  },
35386
+ "pipelineAnalytics.listEventMedia": {
35387
+ capName: "pipeline-analytics",
35388
+ capScope: "device",
35389
+ addonId: null,
35390
+ access: "view"
35391
+ },
35312
35392
  "pipelineAnalytics.listGroups": {
35313
35393
  capName: "pipeline-analytics",
35314
35394
  capScope: "device",
@@ -38932,6 +39012,11 @@ Object.freeze({
38932
39012
  form: "array",
38933
39013
  optional: false
38934
39014
  }],
39015
+ "pipelineAnalytics.listEventMedia": [{
39016
+ name: "deviceId",
39017
+ form: "single",
39018
+ optional: false
39019
+ }],
38935
39020
  "pipelineAnalytics.listGroups": [{
38936
39021
  name: "deviceIds",
38937
39022
  form: "array",
package/dist/addon.mjs CHANGED
@@ -13183,6 +13183,114 @@ method(object({
13183
13183
  height: number()
13184
13184
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13185
13185
  /**
13186
+ * `failure-contribution` — the capability an addon reports its OWN losses
13187
+ * through, per camera, with the denominator attached. It stores nothing.
13188
+ *
13189
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13190
+ *
13191
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13192
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13193
+ * copied: the contributor reports what it already knows, hub-main adds only
13194
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13195
+ * somebody to forget to edit.
13196
+ *
13197
+ * They are not merged, because their invariants are opposites:
13198
+ *
13199
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13200
+ * claim a camera cost nothing, which is a measurement nobody made;
13201
+ * - a `failure-contribution` zero is the **most valuable value on the
13202
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13203
+ * and it is exactly what an absent entry cannot say.
13204
+ *
13205
+ * Putting a loss counter on a cost entry would also break the reconciliation
13206
+ * that gives `load-contribution` its point: contributions are subtracted from
13207
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13208
+ * has no process.
13209
+ *
13210
+ * ## Why not a log line, since the counters already exist
13211
+ *
13212
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13213
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13214
+ * ends in a log line, and a log line is the thing the operator asked to stop
13215
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13216
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13217
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13218
+ * media blackout were both diagnosed. The counters stay; this is where they can
13219
+ * be READ.
13220
+ *
13221
+ * ## The rate is served with its denominator or not at all
13222
+ *
13223
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13224
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13225
+ * than yesterday" and was **flat across twelve hours** once divided by the
13226
+ * successes on the same path. A surface that publishes only the numerator
13227
+ * reproduces that mistake on every read.
13228
+ *
13229
+ * ## Shape
13230
+ *
13231
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13232
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13233
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13234
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13235
+ * a forked runner's entries reach hub-main over transport that already exists.
13236
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13237
+ * result through `system.getFailureContributions`.
13238
+ */
13239
+ var FailureReasonCountSchema = object({
13240
+ /**
13241
+ * Why the attempt did not land, in the contributor's own vocabulary —
13242
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13243
+ * strings that already appear in this repo's logs and, where one exists, the
13244
+ * same string the per-track `previewMissReason` records (D276): a second
13245
+ * vocabulary for the same loss would make the row and the counter
13246
+ * un-joinable.
13247
+ */
13248
+ reason: string(),
13249
+ count: number().int().nonnegative()
13250
+ });
13251
+ var FailureContributionSchema = object({
13252
+ /**
13253
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13254
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13255
+ * `unit` free: the families are owned by different addons and a shared enum
13256
+ * is a central list that rots invisibly.
13257
+ */
13258
+ family: string(),
13259
+ /**
13260
+ * The NUMERIC device id — the same value every log line carries as
13261
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13262
+ * cannot name the camera must not emit the entry, because a fleet total
13263
+ * cannot answer the only question anybody asks of this surface.
13264
+ */
13265
+ deviceId: number().int().positive(),
13266
+ /**
13267
+ * A second dimension inside the family: the model / step id for an inference
13268
+ * timeout, so "which camera AND which model" is one read. Absent when the
13269
+ * family has a single variant.
13270
+ */
13271
+ variant: string().optional(),
13272
+ /**
13273
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13274
+ * differencing two reads must drop the interval when it changes, because the
13275
+ * counter restarted from zero in a respawned runner. Same discipline as
13276
+ * `LoadContribution.startedAtMs`.
13277
+ */
13278
+ sinceMs: number(),
13279
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13280
+ atMs: number(),
13281
+ /**
13282
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13283
+ * window. A failure count published without it is the mistake this schema
13284
+ * exists to make impossible.
13285
+ */
13286
+ attempts: number().int().nonnegative(),
13287
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13288
+ succeeded: number().int().nonnegative(),
13289
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13290
+ reasons: array(FailureReasonCountSchema).readonly()
13291
+ });
13292
+ method(_void(), array(FailureContributionSchema).readonly());
13293
+ /**
13186
13294
  * filesystem-browse — per-node capability for browsing the node's local
13187
13295
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13188
13296
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13704,6 +13812,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13704
13812
  kind: "mutation",
13705
13813
  auth: "admin"
13706
13814
  });
13815
+ var LoadContributionSchema = object({
13816
+ role: _enum([
13817
+ "decode",
13818
+ "transcode",
13819
+ "recording",
13820
+ "streaming",
13821
+ "detection"
13822
+ ]),
13823
+ /**
13824
+ * The NUMERIC device id — the same value every log line carries as
13825
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13826
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13827
+ * contributor that cannot name its camera must not emit the entry at all,
13828
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13829
+ * and would quietly turn one camera's cost into everybody's.
13830
+ */
13831
+ deviceId: number().int().positive().nullable(),
13832
+ attribution: _enum([
13833
+ "measured",
13834
+ "accounted",
13835
+ "unattributable"
13836
+ ]),
13837
+ /**
13838
+ * What ONE entry is, in the contributor's own words — `615/high`,
13839
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13840
+ * family and inventing a common one would lose the only information that
13841
+ * makes two entries for the same camera distinguishable.
13842
+ */
13843
+ unit: string(),
13844
+ /**
13845
+ * The OS process this cost lives in, when there is one. Present so a
13846
+ * consumer can (a) tell two generations of the same unit apart across a
13847
+ * restart, and (b) subtract claimed processes from the node's process
13848
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13849
+ * process of its own.
13850
+ */
13851
+ pid: number().int().positive().optional(),
13852
+ /**
13853
+ * When this generation started. The pid's incarnation marker: a consumer
13854
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13855
+ * window when this changes, because the counter restarted from zero in a new
13856
+ * process.
13857
+ */
13858
+ startedAtMs: number().optional(),
13859
+ /**
13860
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13861
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13862
+ * contribution is asked for.
13863
+ *
13864
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13865
+ * needs a sampler, and a new per-node sampler is the defect half of
13866
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13867
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13868
+ *
13869
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13870
+ * an entry with no process.
13871
+ */
13872
+ cpuSeconds: number().optional(),
13873
+ /** Resident bytes of this unit's process, same source and same rules. */
13874
+ rssBytes: number().optional()
13875
+ });
13876
+ method(_void(), array(LoadContributionSchema).readonly());
13707
13877
  /**
13708
13878
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13709
13879
  * through. It stores nothing.
@@ -13780,176 +13950,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13780
13950
  tags: record(string(), string()).optional()
13781
13951
  }), array(LogEntrySchema).readonly());
13782
13952
  /**
13783
- * `failure-contribution` — the capability an addon reports its OWN losses
13784
- * through, per camera, with the denominator attached. It stores nothing.
13785
- *
13786
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13787
- *
13788
- * `load-contribution` answers *what did this camera COST*. This answers *what
13789
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13790
- * copied: the contributor reports what it already knows, hub-main adds only
13791
- * `addonId`, nothing needs global knowledge, and there is no central list for
13792
- * somebody to forget to edit.
13793
- *
13794
- * They are not merged, because their invariants are opposites:
13795
- *
13796
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13797
- * claim a camera cost nothing, which is a measurement nobody made;
13798
- * - a `failure-contribution` zero is the **most valuable value on the
13799
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13800
- * and it is exactly what an absent entry cannot say.
13801
- *
13802
- * Putting a loss counter on a cost entry would also break the reconciliation
13803
- * that gives `load-contribution` its point: contributions are subtracted from
13804
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13805
- * has no process.
13806
- *
13807
- * ## Why not a log line, since the counters already exist
13808
- *
13809
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13810
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13811
- * ends in a log line, and a log line is the thing the operator asked to stop
13812
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13813
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13814
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13815
- * media blackout were both diagnosed. The counters stay; this is where they can
13816
- * be READ.
13817
- *
13818
- * ## The rate is served with its denominator or not at all
13819
- *
13820
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13821
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13822
- * than yesterday" and was **flat across twelve hours** once divided by the
13823
- * successes on the same path. A surface that publishes only the numerator
13824
- * reproduces that mistake on every read.
13825
- *
13826
- * ## Shape
13827
- *
13828
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13829
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13830
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13831
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13832
- * a forked runner's entries reach hub-main over transport that already exists.
13833
- * No new UDS message, no second registry (D3). The operator reads the assembled
13834
- * result through `system.getFailureContributions`.
13835
- */
13836
- var FailureReasonCountSchema = object({
13837
- /**
13838
- * Why the attempt did not land, in the contributor's own vocabulary —
13839
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13840
- * strings that already appear in this repo's logs and, where one exists, the
13841
- * same string the per-track `previewMissReason` records (D276): a second
13842
- * vocabulary for the same loss would make the row and the counter
13843
- * un-joinable.
13844
- */
13845
- reason: string(),
13846
- count: number().int().nonnegative()
13847
- });
13848
- var FailureContributionSchema = object({
13849
- /**
13850
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13851
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13852
- * `unit` free: the families are owned by different addons and a shared enum
13853
- * is a central list that rots invisibly.
13854
- */
13855
- family: string(),
13856
- /**
13857
- * The NUMERIC device id — the same value every log line carries as
13858
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13859
- * cannot name the camera must not emit the entry, because a fleet total
13860
- * cannot answer the only question anybody asks of this surface.
13861
- */
13862
- deviceId: number().int().positive(),
13863
- /**
13864
- * A second dimension inside the family: the model / step id for an inference
13865
- * timeout, so "which camera AND which model" is one read. Absent when the
13866
- * family has a single variant.
13867
- */
13868
- variant: string().optional(),
13869
- /**
13870
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13871
- * differencing two reads must drop the interval when it changes, because the
13872
- * counter restarted from zero in a respawned runner. Same discipline as
13873
- * `LoadContribution.startedAtMs`.
13874
- */
13875
- sinceMs: number(),
13876
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13877
- atMs: number(),
13878
- /**
13879
- * THE DENOMINATOR — every attempt on this path for this camera in the
13880
- * window. A failure count published without it is the mistake this schema
13881
- * exists to make impossible.
13882
- */
13883
- attempts: number().int().nonnegative(),
13884
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13885
- succeeded: number().int().nonnegative(),
13886
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13887
- reasons: array(FailureReasonCountSchema).readonly()
13888
- });
13889
- method(_void(), array(FailureContributionSchema).readonly());
13890
- var LoadContributionSchema = object({
13891
- role: _enum([
13892
- "decode",
13893
- "transcode",
13894
- "recording",
13895
- "streaming",
13896
- "detection"
13897
- ]),
13898
- /**
13899
- * The NUMERIC device id — the same value every log line carries as
13900
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13901
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13902
- * contributor that cannot name its camera must not emit the entry at all,
13903
- * because an unnamed per-camera entry is indistinguishable from a shared one
13904
- * and would quietly turn one camera's cost into everybody's.
13905
- */
13906
- deviceId: number().int().positive().nullable(),
13907
- attribution: _enum([
13908
- "measured",
13909
- "accounted",
13910
- "unattributable"
13911
- ]),
13912
- /**
13913
- * What ONE entry is, in the contributor's own words — `615/high`,
13914
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13915
- * family and inventing a common one would lose the only information that
13916
- * makes two entries for the same camera distinguishable.
13917
- */
13918
- unit: string(),
13919
- /**
13920
- * The OS process this cost lives in, when there is one. Present so a
13921
- * consumer can (a) tell two generations of the same unit apart across a
13922
- * restart, and (b) subtract claimed processes from the node's process
13923
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13924
- * process of its own.
13925
- */
13926
- pid: number().int().positive().optional(),
13927
- /**
13928
- * When this generation started. The pid's incarnation marker: a consumer
13929
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13930
- * window when this changes, because the counter restarted from zero in a new
13931
- * process.
13932
- */
13933
- startedAtMs: number().optional(),
13934
- /**
13935
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13936
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13937
- * contribution is asked for.
13938
- *
13939
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13940
- * needs a sampler, and a new per-node sampler is the defect half of
13941
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13942
- * by whoever already keeps a history; a rate cannot be un-averaged.
13943
- *
13944
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13945
- * an entry with no process.
13946
- */
13947
- cpuSeconds: number().optional(),
13948
- /** Resident bytes of this unit's process, same source and same rules. */
13949
- rssBytes: number().optional()
13950
- });
13951
- method(_void(), array(LoadContributionSchema).readonly());
13952
- /**
13953
13953
  * `login-method` — collection cap through which auth addons contribute
13954
13954
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13955
13955
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18687,12 +18687,53 @@ var MediaFileKindEnum = _enum([
18687
18687
  "keyFrameSmall",
18688
18688
  "thumbnailSmall"
18689
18689
  ]);
18690
+ /**
18691
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18692
+ * ARE — never the bytes themselves.
18693
+ *
18694
+ * ## Why `url` and not `base64`
18695
+ *
18696
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18697
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18698
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18699
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18700
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18701
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18702
+ *
18703
+ * `url` points at the `event-media` data plane
18704
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18705
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18706
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18707
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18708
+ * no less protected than they were inside a `view`-level cap response — see
18709
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18710
+ * (per-device scoping).
18711
+ *
18712
+ * The URL is built from the row's **stored** key, which is not always its
18713
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18714
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18715
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18716
+ *
18717
+ * ## `base64` is TRANSITIONAL and is going away
18718
+ *
18719
+ * It is still populated for one reason: the deployed viewer's track-detail
18720
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18721
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18722
+ * triangle — not as absence. Removing the field before that viewer ships is an
18723
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18724
+ * delete this line and the `withBytes` pass-through in
18725
+ * `analytics-query-facade.ts`; nothing else reads it.
18726
+ */
18690
18727
  var MediaFileSchema = object({
18691
18728
  key: string(),
18692
18729
  kind: MediaFileKindEnum,
18693
- base64: string(),
18694
18730
  sizeBytes: number(),
18695
18731
  timestamp: number()
18732
+ }).extend({
18733
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18734
+ url: string(),
18735
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18736
+ base64: string()
18696
18737
  });
18697
18738
  /**
18698
18739
  * One media row WITHOUT its bytes.
@@ -18704,7 +18745,9 @@ var MediaFileSchema = object({
18704
18745
  * blocks the whole view.
18705
18746
  *
18706
18747
  * `sizeBytes` is carried because it is what lets a client decide between the
18707
- * stored blob and a `?variant=thumb` rendering without fetching either.
18748
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18749
+ * `url` because a client that had to build the plane path itself is a second
18750
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18708
18751
  */
18709
18752
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18710
18753
  /**
@@ -19393,6 +19436,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19393
19436
  }), array(MediaFileSchema).readonly()), method(object({
19394
19437
  trackId: string(),
19395
19438
  deviceId: number()
19439
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19440
+ eventId: string(),
19441
+ deviceId: number()
19396
19442
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19397
19443
  kind: "mutation",
19398
19444
  auth: "admin"
@@ -24383,10 +24429,24 @@ var FaceClusterSchema = object({
24383
24429
  size: number().int(),
24384
24430
  cohesion: number()
24385
24431
  });
24432
+ /**
24433
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24434
+ * are — never the bytes.
24435
+ *
24436
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24437
+ * track/event contract) is still populated because a deployed viewer requires
24438
+ * the field to parse a row at all; this method has no such reader. Its ONE
24439
+ * caller is the admin UI's detail modal, which was building
24440
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24441
+ * dialog already rendering its key FRAME from the `event-media` plane.
24442
+ *
24443
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24444
+ * media key directly, so this needed no new plane and no new access decision.
24445
+ */
24386
24446
  var MediaFileLiteSchema$1 = object({
24387
24447
  key: string(),
24388
24448
  kind: string(),
24389
- base64: string(),
24449
+ url: string(),
24390
24450
  sizeBytes: number(),
24391
24451
  timestamp: number()
24392
24452
  });
@@ -27408,10 +27468,24 @@ var PlateInfoSchema = object({
27408
27468
  */
27409
27469
  cropUrl: string().optional()
27410
27470
  });
27471
+ /**
27472
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27473
+ * are — never the bytes.
27474
+ *
27475
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27476
+ * track/event contract) is still populated because a deployed viewer requires
27477
+ * the field to parse a row at all; this method has no such reader. Its ONE
27478
+ * caller is the admin UI's detail modal, which was building
27479
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27480
+ * dialog already rendering its key FRAME from the `event-media` plane.
27481
+ *
27482
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27483
+ * media key directly, so this needed no new plane and no new access decision.
27484
+ */
27411
27485
  var MediaFileLiteSchema = object({
27412
27486
  key: string(),
27413
27487
  kind: string(),
27414
- base64: string(),
27488
+ url: string(),
27415
27489
  sizeBytes: number(),
27416
27490
  timestamp: number()
27417
27491
  });
@@ -35308,6 +35382,12 @@ Object.freeze({
35308
35382
  addonId: null,
35309
35383
  access: "view"
35310
35384
  },
35385
+ "pipelineAnalytics.listEventMedia": {
35386
+ capName: "pipeline-analytics",
35387
+ capScope: "device",
35388
+ addonId: null,
35389
+ access: "view"
35390
+ },
35311
35391
  "pipelineAnalytics.listGroups": {
35312
35392
  capName: "pipeline-analytics",
35313
35393
  capScope: "device",
@@ -38931,6 +39011,11 @@ Object.freeze({
38931
39011
  form: "array",
38932
39012
  optional: false
38933
39013
  }],
39014
+ "pipelineAnalytics.listEventMedia": [{
39015
+ name: "deviceId",
39016
+ form: "single",
39017
+ optional: false
39018
+ }],
38934
39019
  "pipelineAnalytics.listGroups": [{
38935
39020
  name: "deviceIds",
38936
39021
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-velux",
3
- "version": "0.2.46",
3
+ "version": "0.2.47",
4
4
  "description": "Velux KLF-200 device-provider addon for CamStack — local-gateway window + shutter covers (positional, no tilt)",
5
5
  "keywords": [
6
6
  "camstack",