@camstack/addon-provider-unifi 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"
@@ -24401,10 +24447,24 @@ var FaceClusterSchema = object({
24401
24447
  size: number().int(),
24402
24448
  cohesion: number()
24403
24449
  });
24450
+ /**
24451
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24452
+ * are — never the bytes.
24453
+ *
24454
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24455
+ * track/event contract) is still populated because a deployed viewer requires
24456
+ * the field to parse a row at all; this method has no such reader. Its ONE
24457
+ * caller is the admin UI's detail modal, which was building
24458
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24459
+ * dialog already rendering its key FRAME from the `event-media` plane.
24460
+ *
24461
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24462
+ * media key directly, so this needed no new plane and no new access decision.
24463
+ */
24404
24464
  var MediaFileLiteSchema$1 = object({
24405
24465
  key: string(),
24406
24466
  kind: string(),
24407
- base64: string(),
24467
+ url: string(),
24408
24468
  sizeBytes: number(),
24409
24469
  timestamp: number()
24410
24470
  });
@@ -27426,10 +27486,24 @@ var PlateInfoSchema = object({
27426
27486
  */
27427
27487
  cropUrl: string().optional()
27428
27488
  });
27489
+ /**
27490
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27491
+ * are — never the bytes.
27492
+ *
27493
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27494
+ * track/event contract) is still populated because a deployed viewer requires
27495
+ * the field to parse a row at all; this method has no such reader. Its ONE
27496
+ * caller is the admin UI's detail modal, which was building
27497
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27498
+ * dialog already rendering its key FRAME from the `event-media` plane.
27499
+ *
27500
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27501
+ * media key directly, so this needed no new plane and no new access decision.
27502
+ */
27429
27503
  var MediaFileLiteSchema = object({
27430
27504
  key: string(),
27431
27505
  kind: string(),
27432
- base64: string(),
27506
+ url: string(),
27433
27507
  sizeBytes: number(),
27434
27508
  timestamp: number()
27435
27509
  });
@@ -35326,6 +35400,12 @@ Object.freeze({
35326
35400
  addonId: null,
35327
35401
  access: "view"
35328
35402
  },
35403
+ "pipelineAnalytics.listEventMedia": {
35404
+ capName: "pipeline-analytics",
35405
+ capScope: "device",
35406
+ addonId: null,
35407
+ access: "view"
35408
+ },
35329
35409
  "pipelineAnalytics.listGroups": {
35330
35410
  capName: "pipeline-analytics",
35331
35411
  capScope: "device",
@@ -38949,6 +39029,11 @@ Object.freeze({
38949
39029
  form: "array",
38950
39030
  optional: false
38951
39031
  }],
39032
+ "pipelineAnalytics.listEventMedia": [{
39033
+ name: "deviceId",
39034
+ form: "single",
39035
+ optional: false
39036
+ }],
38952
39037
  "pipelineAnalytics.listGroups": [{
38953
39038
  name: "deviceIds",
38954
39039
  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"
@@ -24400,10 +24446,24 @@ var FaceClusterSchema = object({
24400
24446
  size: number().int(),
24401
24447
  cohesion: number()
24402
24448
  });
24449
+ /**
24450
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24451
+ * are — never the bytes.
24452
+ *
24453
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24454
+ * track/event contract) is still populated because a deployed viewer requires
24455
+ * the field to parse a row at all; this method has no such reader. Its ONE
24456
+ * caller is the admin UI's detail modal, which was building
24457
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24458
+ * dialog already rendering its key FRAME from the `event-media` plane.
24459
+ *
24460
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24461
+ * media key directly, so this needed no new plane and no new access decision.
24462
+ */
24403
24463
  var MediaFileLiteSchema$1 = object({
24404
24464
  key: string(),
24405
24465
  kind: string(),
24406
- base64: string(),
24466
+ url: string(),
24407
24467
  sizeBytes: number(),
24408
24468
  timestamp: number()
24409
24469
  });
@@ -27425,10 +27485,24 @@ var PlateInfoSchema = object({
27425
27485
  */
27426
27486
  cropUrl: string().optional()
27427
27487
  });
27488
+ /**
27489
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27490
+ * are — never the bytes.
27491
+ *
27492
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27493
+ * track/event contract) is still populated because a deployed viewer requires
27494
+ * the field to parse a row at all; this method has no such reader. Its ONE
27495
+ * caller is the admin UI's detail modal, which was building
27496
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27497
+ * dialog already rendering its key FRAME from the `event-media` plane.
27498
+ *
27499
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27500
+ * media key directly, so this needed no new plane and no new access decision.
27501
+ */
27428
27502
  var MediaFileLiteSchema = object({
27429
27503
  key: string(),
27430
27504
  kind: string(),
27431
- base64: string(),
27505
+ url: string(),
27432
27506
  sizeBytes: number(),
27433
27507
  timestamp: number()
27434
27508
  });
@@ -35325,6 +35399,12 @@ Object.freeze({
35325
35399
  addonId: null,
35326
35400
  access: "view"
35327
35401
  },
35402
+ "pipelineAnalytics.listEventMedia": {
35403
+ capName: "pipeline-analytics",
35404
+ capScope: "device",
35405
+ addonId: null,
35406
+ access: "view"
35407
+ },
35328
35408
  "pipelineAnalytics.listGroups": {
35329
35409
  capName: "pipeline-analytics",
35330
35410
  capScope: "device",
@@ -38948,6 +39028,11 @@ Object.freeze({
38948
39028
  form: "array",
38949
39029
  optional: false
38950
39030
  }],
39031
+ "pipelineAnalytics.listEventMedia": [{
39032
+ name: "deviceId",
39033
+ form: "single",
39034
+ optional: false
39035
+ }],
38951
39036
  "pipelineAnalytics.listGroups": [{
38952
39037
  name: "deviceIds",
38953
39038
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-unifi",
3
- "version": "0.2.46",
3
+ "version": "0.2.47",
4
4
  "description": "UniFi Network controller device-provider addon for CamStack — local-controller infra switches/APs (as containers) + network-client presence. NO cameras/Protect.",
5
5
  "keywords": [
6
6
  "camstack",