@camstack/addon-provider-petkit 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
@@ -14303,6 +14303,114 @@ method(object({
14303
14303
  height: number()
14304
14304
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
14305
14305
  /**
14306
+ * `failure-contribution` — the capability an addon reports its OWN losses
14307
+ * through, per camera, with the denominator attached. It stores nothing.
14308
+ *
14309
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
14310
+ *
14311
+ * `load-contribution` answers *what did this camera COST*. This answers *what
14312
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
14313
+ * copied: the contributor reports what it already knows, hub-main adds only
14314
+ * `addonId`, nothing needs global knowledge, and there is no central list for
14315
+ * somebody to forget to edit.
14316
+ *
14317
+ * They are not merged, because their invariants are opposites:
14318
+ *
14319
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
14320
+ * claim a camera cost nothing, which is a measurement nobody made;
14321
+ * - a `failure-contribution` zero is the **most valuable value on the
14322
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14323
+ * and it is exactly what an absent entry cannot say.
14324
+ *
14325
+ * Putting a loss counter on a cost entry would also break the reconciliation
14326
+ * that gives `load-contribution` its point: contributions are subtracted from
14327
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14328
+ * has no process.
14329
+ *
14330
+ * ## Why not a log line, since the counters already exist
14331
+ *
14332
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
14333
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14334
+ * ends in a log line, and a log line is the thing the operator asked to stop
14335
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14336
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14337
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14338
+ * media blackout were both diagnosed. The counters stay; this is where they can
14339
+ * be READ.
14340
+ *
14341
+ * ## The rate is served with its denominator or not at all
14342
+ *
14343
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
14344
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14345
+ * than yesterday" and was **flat across twelve hours** once divided by the
14346
+ * successes on the same path. A surface that publishes only the numerator
14347
+ * reproduces that mistake on every read.
14348
+ *
14349
+ * ## Shape
14350
+ *
14351
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14352
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14353
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14354
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14355
+ * a forked runner's entries reach hub-main over transport that already exists.
14356
+ * No new UDS message, no second registry (D3). The operator reads the assembled
14357
+ * result through `system.getFailureContributions`.
14358
+ */
14359
+ var FailureReasonCountSchema = object({
14360
+ /**
14361
+ * Why the attempt did not land, in the contributor's own vocabulary —
14362
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14363
+ * strings that already appear in this repo's logs and, where one exists, the
14364
+ * same string the per-track `previewMissReason` records (D276): a second
14365
+ * vocabulary for the same loss would make the row and the counter
14366
+ * un-joinable.
14367
+ */
14368
+ reason: string(),
14369
+ count: number().int().nonnegative()
14370
+ });
14371
+ var FailureContributionSchema = object({
14372
+ /**
14373
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14374
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14375
+ * `unit` free: the families are owned by different addons and a shared enum
14376
+ * is a central list that rots invisibly.
14377
+ */
14378
+ family: string(),
14379
+ /**
14380
+ * The NUMERIC device id — the same value every log line carries as
14381
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
14382
+ * cannot name the camera must not emit the entry, because a fleet total
14383
+ * cannot answer the only question anybody asks of this surface.
14384
+ */
14385
+ deviceId: number().int().positive(),
14386
+ /**
14387
+ * A second dimension inside the family: the model / step id for an inference
14388
+ * timeout, so "which camera AND which model" is one read. Absent when the
14389
+ * family has a single variant.
14390
+ */
14391
+ variant: string().optional(),
14392
+ /**
14393
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14394
+ * differencing two reads must drop the interval when it changes, because the
14395
+ * counter restarted from zero in a respawned runner. Same discipline as
14396
+ * `LoadContribution.startedAtMs`.
14397
+ */
14398
+ sinceMs: number(),
14399
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14400
+ atMs: number(),
14401
+ /**
14402
+ * THE DENOMINATOR — every attempt on this path for this camera in the
14403
+ * window. A failure count published without it is the mistake this schema
14404
+ * exists to make impossible.
14405
+ */
14406
+ attempts: number().int().nonnegative(),
14407
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
14408
+ succeeded: number().int().nonnegative(),
14409
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
14410
+ reasons: array(FailureReasonCountSchema).readonly()
14411
+ });
14412
+ method(_void(), array(FailureContributionSchema).readonly());
14413
+ /**
14306
14414
  * filesystem-browse — per-node capability for browsing the node's local
14307
14415
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
14308
14416
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -14824,6 +14932,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
14824
14932
  kind: "mutation",
14825
14933
  auth: "admin"
14826
14934
  });
14935
+ var LoadContributionSchema = object({
14936
+ role: _enum([
14937
+ "decode",
14938
+ "transcode",
14939
+ "recording",
14940
+ "streaming",
14941
+ "detection"
14942
+ ]),
14943
+ /**
14944
+ * The NUMERIC device id — the same value every log line carries as
14945
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14946
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
14947
+ * contributor that cannot name its camera must not emit the entry at all,
14948
+ * because an unnamed per-camera entry is indistinguishable from a shared one
14949
+ * and would quietly turn one camera's cost into everybody's.
14950
+ */
14951
+ deviceId: number().int().positive().nullable(),
14952
+ attribution: _enum([
14953
+ "measured",
14954
+ "accounted",
14955
+ "unattributable"
14956
+ ]),
14957
+ /**
14958
+ * What ONE entry is, in the contributor's own words — `615/high`,
14959
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14960
+ * family and inventing a common one would lose the only information that
14961
+ * makes two entries for the same camera distinguishable.
14962
+ */
14963
+ unit: string(),
14964
+ /**
14965
+ * The OS process this cost lives in, when there is one. Present so a
14966
+ * consumer can (a) tell two generations of the same unit apart across a
14967
+ * restart, and (b) subtract claimed processes from the node's process
14968
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14969
+ * process of its own.
14970
+ */
14971
+ pid: number().int().positive().optional(),
14972
+ /**
14973
+ * When this generation started. The pid's incarnation marker: a consumer
14974
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14975
+ * window when this changes, because the counter restarted from zero in a new
14976
+ * process.
14977
+ */
14978
+ startedAtMs: number().optional(),
14979
+ /**
14980
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14981
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
14982
+ * contribution is asked for.
14983
+ *
14984
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
14985
+ * needs a sampler, and a new per-node sampler is the defect half of
14986
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14987
+ * by whoever already keeps a history; a rate cannot be un-averaged.
14988
+ *
14989
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14990
+ * an entry with no process.
14991
+ */
14992
+ cpuSeconds: number().optional(),
14993
+ /** Resident bytes of this unit's process, same source and same rules. */
14994
+ rssBytes: number().optional()
14995
+ });
14996
+ method(_void(), array(LoadContributionSchema).readonly());
14827
14997
  /**
14828
14998
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
14829
14999
  * through. It stores nothing.
@@ -14900,176 +15070,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
14900
15070
  tags: record(string(), string()).optional()
14901
15071
  }), array(LogEntrySchema).readonly());
14902
15072
  /**
14903
- * `failure-contribution` — the capability an addon reports its OWN losses
14904
- * through, per camera, with the denominator attached. It stores nothing.
14905
- *
14906
- * ## The twin of `load-contribution`, and why it is a twin and not a field
14907
- *
14908
- * `load-contribution` answers *what did this camera COST*. This answers *what
14909
- * did this camera LOSE*. The reporting discipline is identical and deliberately
14910
- * copied: the contributor reports what it already knows, hub-main adds only
14911
- * `addonId`, nothing needs global knowledge, and there is no central list for
14912
- * somebody to forget to edit.
14913
- *
14914
- * They are not merged, because their invariants are opposites:
14915
- *
14916
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
14917
- * claim a camera cost nothing, which is a measurement nobody made;
14918
- * - a `failure-contribution` zero is the **most valuable value on the
14919
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14920
- * and it is exactly what an absent entry cannot say.
14921
- *
14922
- * Putting a loss counter on a cost entry would also break the reconciliation
14923
- * that gives `load-contribution` its point: contributions are subtracted from
14924
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14925
- * has no process.
14926
- *
14927
- * ## Why not a log line, since the counters already exist
14928
- *
14929
- * Several of these paths already counted themselves — `CaptureScheduler`'s
14930
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14931
- * ends in a log line, and a log line is the thing the operator asked to stop
14932
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14933
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14934
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14935
- * media blackout were both diagnosed. The counters stay; this is where they can
14936
- * be READ.
14937
- *
14938
- * ## The rate is served with its denominator or not at all
14939
- *
14940
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
14941
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14942
- * than yesterday" and was **flat across twelve hours** once divided by the
14943
- * successes on the same path. A surface that publishes only the numerator
14944
- * reproduces that mistake on every read.
14945
- *
14946
- * ## Shape
14947
- *
14948
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14949
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14950
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14951
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14952
- * a forked runner's entries reach hub-main over transport that already exists.
14953
- * No new UDS message, no second registry (D3). The operator reads the assembled
14954
- * result through `system.getFailureContributions`.
14955
- */
14956
- var FailureReasonCountSchema = object({
14957
- /**
14958
- * Why the attempt did not land, in the contributor's own vocabulary —
14959
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14960
- * strings that already appear in this repo's logs and, where one exists, the
14961
- * same string the per-track `previewMissReason` records (D276): a second
14962
- * vocabulary for the same loss would make the row and the counter
14963
- * un-joinable.
14964
- */
14965
- reason: string(),
14966
- count: number().int().nonnegative()
14967
- });
14968
- var FailureContributionSchema = object({
14969
- /**
14970
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14971
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14972
- * `unit` free: the families are owned by different addons and a shared enum
14973
- * is a central list that rots invisibly.
14974
- */
14975
- family: string(),
14976
- /**
14977
- * The NUMERIC device id — the same value every log line carries as
14978
- * `tags.deviceId`. Never nullable and never absent: a contributor that
14979
- * cannot name the camera must not emit the entry, because a fleet total
14980
- * cannot answer the only question anybody asks of this surface.
14981
- */
14982
- deviceId: number().int().positive(),
14983
- /**
14984
- * A second dimension inside the family: the model / step id for an inference
14985
- * timeout, so "which camera AND which model" is one read. Absent when the
14986
- * family has a single variant.
14987
- */
14988
- variant: string().optional(),
14989
- /**
14990
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14991
- * differencing two reads must drop the interval when it changes, because the
14992
- * counter restarted from zero in a respawned runner. Same discipline as
14993
- * `LoadContribution.startedAtMs`.
14994
- */
14995
- sinceMs: number(),
14996
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14997
- atMs: number(),
14998
- /**
14999
- * THE DENOMINATOR — every attempt on this path for this camera in the
15000
- * window. A failure count published without it is the mistake this schema
15001
- * exists to make impossible.
15002
- */
15003
- attempts: number().int().nonnegative(),
15004
- /** Attempts that landed. `attempts - succeeded` is the loss. */
15005
- succeeded: number().int().nonnegative(),
15006
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
15007
- reasons: array(FailureReasonCountSchema).readonly()
15008
- });
15009
- method(_void(), array(FailureContributionSchema).readonly());
15010
- var LoadContributionSchema = object({
15011
- role: _enum([
15012
- "decode",
15013
- "transcode",
15014
- "recording",
15015
- "streaming",
15016
- "detection"
15017
- ]),
15018
- /**
15019
- * The NUMERIC device id — the same value every log line carries as
15020
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
15021
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
15022
- * contributor that cannot name its camera must not emit the entry at all,
15023
- * because an unnamed per-camera entry is indistinguishable from a shared one
15024
- * and would quietly turn one camera's cost into everybody's.
15025
- */
15026
- deviceId: number().int().positive().nullable(),
15027
- attribution: _enum([
15028
- "measured",
15029
- "accounted",
15030
- "unattributable"
15031
- ]),
15032
- /**
15033
- * What ONE entry is, in the contributor's own words — `615/high`,
15034
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
15035
- * family and inventing a common one would lose the only information that
15036
- * makes two entries for the same camera distinguishable.
15037
- */
15038
- unit: string(),
15039
- /**
15040
- * The OS process this cost lives in, when there is one. Present so a
15041
- * consumer can (a) tell two generations of the same unit apart across a
15042
- * restart, and (b) subtract claimed processes from the node's process
15043
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
15044
- * process of its own.
15045
- */
15046
- pid: number().int().positive().optional(),
15047
- /**
15048
- * When this generation started. The pid's incarnation marker: a consumer
15049
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
15050
- * window when this changes, because the counter restarted from zero in a new
15051
- * process.
15052
- */
15053
- startedAtMs: number().optional(),
15054
- /**
15055
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
15056
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
15057
- * contribution is asked for.
15058
- *
15059
- * Cumulative and not a rate on purpose: a rate needs a window, a window
15060
- * needs a sampler, and a new per-node sampler is the defect half of
15061
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
15062
- * by whoever already keeps a history; a rate cannot be un-averaged.
15063
- *
15064
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
15065
- * an entry with no process.
15066
- */
15067
- cpuSeconds: number().optional(),
15068
- /** Resident bytes of this unit's process, same source and same rules. */
15069
- rssBytes: number().optional()
15070
- });
15071
- method(_void(), array(LoadContributionSchema).readonly());
15072
- /**
15073
15073
  * `login-method` — collection cap through which auth addons contribute
15074
15074
  * their pre-auth login surfaces to the login page. This is the SINGLE,
15075
15075
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -19807,12 +19807,53 @@ var MediaFileKindEnum = _enum([
19807
19807
  "keyFrameSmall",
19808
19808
  "thumbnailSmall"
19809
19809
  ]);
19810
+ /**
19811
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
19812
+ * ARE — never the bytes themselves.
19813
+ *
19814
+ * ## Why `url` and not `base64`
19815
+ *
19816
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
19817
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
19818
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
19819
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
19820
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
19821
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
19822
+ *
19823
+ * `url` points at the `event-media` data plane
19824
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
19825
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
19826
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
19827
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
19828
+ * no less protected than they were inside a `view`-level cap response — see
19829
+ * `data-plane-access.ts` for the rule and the one gap it does not close
19830
+ * (per-device scoping).
19831
+ *
19832
+ * The URL is built from the row's **stored** key, which is not always its
19833
+ * published `kind`: a track's face/plate crop is stored as `crop` under
19834
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
19835
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
19836
+ *
19837
+ * ## `base64` is TRANSITIONAL and is going away
19838
+ *
19839
+ * It is still populated for one reason: the deployed viewer's track-detail
19840
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
19841
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
19842
+ * triangle — not as absence. Removing the field before that viewer ships is an
19843
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
19844
+ * delete this line and the `withBytes` pass-through in
19845
+ * `analytics-query-facade.ts`; nothing else reads it.
19846
+ */
19810
19847
  var MediaFileSchema = object({
19811
19848
  key: string(),
19812
19849
  kind: MediaFileKindEnum,
19813
- base64: string(),
19814
19850
  sizeBytes: number(),
19815
19851
  timestamp: number()
19852
+ }).extend({
19853
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
19854
+ url: string(),
19855
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
19856
+ base64: string()
19816
19857
  });
19817
19858
  /**
19818
19859
  * One media row WITHOUT its bytes.
@@ -19824,7 +19865,9 @@ var MediaFileSchema = object({
19824
19865
  * blocks the whole view.
19825
19866
  *
19826
19867
  * `sizeBytes` is carried because it is what lets a client decide between the
19827
- * stored blob and a `?variant=thumb` rendering without fetching either.
19868
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
19869
+ * `url` because a client that had to build the plane path itself is a second
19870
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
19828
19871
  */
19829
19872
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
19830
19873
  /**
@@ -20513,6 +20556,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20513
20556
  }), array(MediaFileSchema).readonly()), method(object({
20514
20557
  trackId: string(),
20515
20558
  deviceId: number()
20559
+ }), array(MediaFileInfoSchema).readonly()), method(object({
20560
+ eventId: string(),
20561
+ deviceId: number()
20516
20562
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
20517
20563
  kind: "mutation",
20518
20564
  auth: "admin"
@@ -25511,10 +25557,24 @@ var FaceClusterSchema = object({
25511
25557
  size: number().int(),
25512
25558
  cohesion: number()
25513
25559
  });
25560
+ /**
25561
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25562
+ * are — never the bytes.
25563
+ *
25564
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25565
+ * track/event contract) is still populated because a deployed viewer requires
25566
+ * the field to parse a row at all; this method has no such reader. Its ONE
25567
+ * caller is the admin UI's detail modal, which was building
25568
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25569
+ * dialog already rendering its key FRAME from the `event-media` plane.
25570
+ *
25571
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25572
+ * media key directly, so this needed no new plane and no new access decision.
25573
+ */
25514
25574
  var MediaFileLiteSchema$1 = object({
25515
25575
  key: string(),
25516
25576
  kind: string(),
25517
- base64: string(),
25577
+ url: string(),
25518
25578
  sizeBytes: number(),
25519
25579
  timestamp: number()
25520
25580
  });
@@ -28536,10 +28596,24 @@ var PlateInfoSchema = object({
28536
28596
  */
28537
28597
  cropUrl: string().optional()
28538
28598
  });
28599
+ /**
28600
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
28601
+ * are — never the bytes.
28602
+ *
28603
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
28604
+ * track/event contract) is still populated because a deployed viewer requires
28605
+ * the field to parse a row at all; this method has no such reader. Its ONE
28606
+ * caller is the admin UI's detail modal, which was building
28607
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
28608
+ * dialog already rendering its key FRAME from the `event-media` plane.
28609
+ *
28610
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
28611
+ * media key directly, so this needed no new plane and no new access decision.
28612
+ */
28539
28613
  var MediaFileLiteSchema = object({
28540
28614
  key: string(),
28541
28615
  kind: string(),
28542
- base64: string(),
28616
+ url: string(),
28543
28617
  sizeBytes: number(),
28544
28618
  timestamp: number()
28545
28619
  });
@@ -36436,6 +36510,12 @@ Object.freeze({
36436
36510
  addonId: null,
36437
36511
  access: "view"
36438
36512
  },
36513
+ "pipelineAnalytics.listEventMedia": {
36514
+ capName: "pipeline-analytics",
36515
+ capScope: "device",
36516
+ addonId: null,
36517
+ access: "view"
36518
+ },
36439
36519
  "pipelineAnalytics.listGroups": {
36440
36520
  capName: "pipeline-analytics",
36441
36521
  capScope: "device",
@@ -40059,6 +40139,11 @@ Object.freeze({
40059
40139
  form: "array",
40060
40140
  optional: false
40061
40141
  }],
40142
+ "pipelineAnalytics.listEventMedia": [{
40143
+ name: "deviceId",
40144
+ form: "single",
40145
+ optional: false
40146
+ }],
40062
40147
  "pipelineAnalytics.listGroups": [{
40063
40148
  name: "deviceIds",
40064
40149
  form: "array",
package/dist/addon.mjs CHANGED
@@ -14302,6 +14302,114 @@ method(object({
14302
14302
  height: number()
14303
14303
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
14304
14304
  /**
14305
+ * `failure-contribution` — the capability an addon reports its OWN losses
14306
+ * through, per camera, with the denominator attached. It stores nothing.
14307
+ *
14308
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
14309
+ *
14310
+ * `load-contribution` answers *what did this camera COST*. This answers *what
14311
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
14312
+ * copied: the contributor reports what it already knows, hub-main adds only
14313
+ * `addonId`, nothing needs global knowledge, and there is no central list for
14314
+ * somebody to forget to edit.
14315
+ *
14316
+ * They are not merged, because their invariants are opposites:
14317
+ *
14318
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
14319
+ * claim a camera cost nothing, which is a measurement nobody made;
14320
+ * - a `failure-contribution` zero is the **most valuable value on the
14321
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14322
+ * and it is exactly what an absent entry cannot say.
14323
+ *
14324
+ * Putting a loss counter on a cost entry would also break the reconciliation
14325
+ * that gives `load-contribution` its point: contributions are subtracted from
14326
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14327
+ * has no process.
14328
+ *
14329
+ * ## Why not a log line, since the counters already exist
14330
+ *
14331
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
14332
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14333
+ * ends in a log line, and a log line is the thing the operator asked to stop
14334
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14335
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14336
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14337
+ * media blackout were both diagnosed. The counters stay; this is where they can
14338
+ * be READ.
14339
+ *
14340
+ * ## The rate is served with its denominator or not at all
14341
+ *
14342
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
14343
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14344
+ * than yesterday" and was **flat across twelve hours** once divided by the
14345
+ * successes on the same path. A surface that publishes only the numerator
14346
+ * reproduces that mistake on every read.
14347
+ *
14348
+ * ## Shape
14349
+ *
14350
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14351
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14352
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14353
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14354
+ * a forked runner's entries reach hub-main over transport that already exists.
14355
+ * No new UDS message, no second registry (D3). The operator reads the assembled
14356
+ * result through `system.getFailureContributions`.
14357
+ */
14358
+ var FailureReasonCountSchema = object({
14359
+ /**
14360
+ * Why the attempt did not land, in the contributor's own vocabulary —
14361
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14362
+ * strings that already appear in this repo's logs and, where one exists, the
14363
+ * same string the per-track `previewMissReason` records (D276): a second
14364
+ * vocabulary for the same loss would make the row and the counter
14365
+ * un-joinable.
14366
+ */
14367
+ reason: string(),
14368
+ count: number().int().nonnegative()
14369
+ });
14370
+ var FailureContributionSchema = object({
14371
+ /**
14372
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14373
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14374
+ * `unit` free: the families are owned by different addons and a shared enum
14375
+ * is a central list that rots invisibly.
14376
+ */
14377
+ family: string(),
14378
+ /**
14379
+ * The NUMERIC device id — the same value every log line carries as
14380
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
14381
+ * cannot name the camera must not emit the entry, because a fleet total
14382
+ * cannot answer the only question anybody asks of this surface.
14383
+ */
14384
+ deviceId: number().int().positive(),
14385
+ /**
14386
+ * A second dimension inside the family: the model / step id for an inference
14387
+ * timeout, so "which camera AND which model" is one read. Absent when the
14388
+ * family has a single variant.
14389
+ */
14390
+ variant: string().optional(),
14391
+ /**
14392
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14393
+ * differencing two reads must drop the interval when it changes, because the
14394
+ * counter restarted from zero in a respawned runner. Same discipline as
14395
+ * `LoadContribution.startedAtMs`.
14396
+ */
14397
+ sinceMs: number(),
14398
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14399
+ atMs: number(),
14400
+ /**
14401
+ * THE DENOMINATOR — every attempt on this path for this camera in the
14402
+ * window. A failure count published without it is the mistake this schema
14403
+ * exists to make impossible.
14404
+ */
14405
+ attempts: number().int().nonnegative(),
14406
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
14407
+ succeeded: number().int().nonnegative(),
14408
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
14409
+ reasons: array(FailureReasonCountSchema).readonly()
14410
+ });
14411
+ method(_void(), array(FailureContributionSchema).readonly());
14412
+ /**
14305
14413
  * filesystem-browse — per-node capability for browsing the node's local
14306
14414
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
14307
14415
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -14823,6 +14931,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
14823
14931
  kind: "mutation",
14824
14932
  auth: "admin"
14825
14933
  });
14934
+ var LoadContributionSchema = object({
14935
+ role: _enum([
14936
+ "decode",
14937
+ "transcode",
14938
+ "recording",
14939
+ "streaming",
14940
+ "detection"
14941
+ ]),
14942
+ /**
14943
+ * The NUMERIC device id — the same value every log line carries as
14944
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14945
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
14946
+ * contributor that cannot name its camera must not emit the entry at all,
14947
+ * because an unnamed per-camera entry is indistinguishable from a shared one
14948
+ * and would quietly turn one camera's cost into everybody's.
14949
+ */
14950
+ deviceId: number().int().positive().nullable(),
14951
+ attribution: _enum([
14952
+ "measured",
14953
+ "accounted",
14954
+ "unattributable"
14955
+ ]),
14956
+ /**
14957
+ * What ONE entry is, in the contributor's own words — `615/high`,
14958
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14959
+ * family and inventing a common one would lose the only information that
14960
+ * makes two entries for the same camera distinguishable.
14961
+ */
14962
+ unit: string(),
14963
+ /**
14964
+ * The OS process this cost lives in, when there is one. Present so a
14965
+ * consumer can (a) tell two generations of the same unit apart across a
14966
+ * restart, and (b) subtract claimed processes from the node's process
14967
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14968
+ * process of its own.
14969
+ */
14970
+ pid: number().int().positive().optional(),
14971
+ /**
14972
+ * When this generation started. The pid's incarnation marker: a consumer
14973
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14974
+ * window when this changes, because the counter restarted from zero in a new
14975
+ * process.
14976
+ */
14977
+ startedAtMs: number().optional(),
14978
+ /**
14979
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14980
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
14981
+ * contribution is asked for.
14982
+ *
14983
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
14984
+ * needs a sampler, and a new per-node sampler is the defect half of
14985
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14986
+ * by whoever already keeps a history; a rate cannot be un-averaged.
14987
+ *
14988
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14989
+ * an entry with no process.
14990
+ */
14991
+ cpuSeconds: number().optional(),
14992
+ /** Resident bytes of this unit's process, same source and same rules. */
14993
+ rssBytes: number().optional()
14994
+ });
14995
+ method(_void(), array(LoadContributionSchema).readonly());
14826
14996
  /**
14827
14997
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
14828
14998
  * through. It stores nothing.
@@ -14899,176 +15069,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
14899
15069
  tags: record(string(), string()).optional()
14900
15070
  }), array(LogEntrySchema).readonly());
14901
15071
  /**
14902
- * `failure-contribution` — the capability an addon reports its OWN losses
14903
- * through, per camera, with the denominator attached. It stores nothing.
14904
- *
14905
- * ## The twin of `load-contribution`, and why it is a twin and not a field
14906
- *
14907
- * `load-contribution` answers *what did this camera COST*. This answers *what
14908
- * did this camera LOSE*. The reporting discipline is identical and deliberately
14909
- * copied: the contributor reports what it already knows, hub-main adds only
14910
- * `addonId`, nothing needs global knowledge, and there is no central list for
14911
- * somebody to forget to edit.
14912
- *
14913
- * They are not merged, because their invariants are opposites:
14914
- *
14915
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
14916
- * claim a camera cost nothing, which is a measurement nobody made;
14917
- * - a `failure-contribution` zero is the **most valuable value on the
14918
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14919
- * and it is exactly what an absent entry cannot say.
14920
- *
14921
- * Putting a loss counter on a cost entry would also break the reconciliation
14922
- * that gives `load-contribution` its point: contributions are subtracted from
14923
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14924
- * has no process.
14925
- *
14926
- * ## Why not a log line, since the counters already exist
14927
- *
14928
- * Several of these paths already counted themselves — `CaptureScheduler`'s
14929
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14930
- * ends in a log line, and a log line is the thing the operator asked to stop
14931
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14932
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14933
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14934
- * media blackout were both diagnosed. The counters stay; this is where they can
14935
- * be READ.
14936
- *
14937
- * ## The rate is served with its denominator or not at all
14938
- *
14939
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
14940
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14941
- * than yesterday" and was **flat across twelve hours** once divided by the
14942
- * successes on the same path. A surface that publishes only the numerator
14943
- * reproduces that mistake on every read.
14944
- *
14945
- * ## Shape
14946
- *
14947
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14948
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14949
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14950
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14951
- * a forked runner's entries reach hub-main over transport that already exists.
14952
- * No new UDS message, no second registry (D3). The operator reads the assembled
14953
- * result through `system.getFailureContributions`.
14954
- */
14955
- var FailureReasonCountSchema = object({
14956
- /**
14957
- * Why the attempt did not land, in the contributor's own vocabulary —
14958
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14959
- * strings that already appear in this repo's logs and, where one exists, the
14960
- * same string the per-track `previewMissReason` records (D276): a second
14961
- * vocabulary for the same loss would make the row and the counter
14962
- * un-joinable.
14963
- */
14964
- reason: string(),
14965
- count: number().int().nonnegative()
14966
- });
14967
- var FailureContributionSchema = object({
14968
- /**
14969
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14970
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14971
- * `unit` free: the families are owned by different addons and a shared enum
14972
- * is a central list that rots invisibly.
14973
- */
14974
- family: string(),
14975
- /**
14976
- * The NUMERIC device id — the same value every log line carries as
14977
- * `tags.deviceId`. Never nullable and never absent: a contributor that
14978
- * cannot name the camera must not emit the entry, because a fleet total
14979
- * cannot answer the only question anybody asks of this surface.
14980
- */
14981
- deviceId: number().int().positive(),
14982
- /**
14983
- * A second dimension inside the family: the model / step id for an inference
14984
- * timeout, so "which camera AND which model" is one read. Absent when the
14985
- * family has a single variant.
14986
- */
14987
- variant: string().optional(),
14988
- /**
14989
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14990
- * differencing two reads must drop the interval when it changes, because the
14991
- * counter restarted from zero in a respawned runner. Same discipline as
14992
- * `LoadContribution.startedAtMs`.
14993
- */
14994
- sinceMs: number(),
14995
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14996
- atMs: number(),
14997
- /**
14998
- * THE DENOMINATOR — every attempt on this path for this camera in the
14999
- * window. A failure count published without it is the mistake this schema
15000
- * exists to make impossible.
15001
- */
15002
- attempts: number().int().nonnegative(),
15003
- /** Attempts that landed. `attempts - succeeded` is the loss. */
15004
- succeeded: number().int().nonnegative(),
15005
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
15006
- reasons: array(FailureReasonCountSchema).readonly()
15007
- });
15008
- method(_void(), array(FailureContributionSchema).readonly());
15009
- var LoadContributionSchema = object({
15010
- role: _enum([
15011
- "decode",
15012
- "transcode",
15013
- "recording",
15014
- "streaming",
15015
- "detection"
15016
- ]),
15017
- /**
15018
- * The NUMERIC device id — the same value every log line carries as
15019
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
15020
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
15021
- * contributor that cannot name its camera must not emit the entry at all,
15022
- * because an unnamed per-camera entry is indistinguishable from a shared one
15023
- * and would quietly turn one camera's cost into everybody's.
15024
- */
15025
- deviceId: number().int().positive().nullable(),
15026
- attribution: _enum([
15027
- "measured",
15028
- "accounted",
15029
- "unattributable"
15030
- ]),
15031
- /**
15032
- * What ONE entry is, in the contributor's own words — `615/high`,
15033
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
15034
- * family and inventing a common one would lose the only information that
15035
- * makes two entries for the same camera distinguishable.
15036
- */
15037
- unit: string(),
15038
- /**
15039
- * The OS process this cost lives in, when there is one. Present so a
15040
- * consumer can (a) tell two generations of the same unit apart across a
15041
- * restart, and (b) subtract claimed processes from the node's process
15042
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
15043
- * process of its own.
15044
- */
15045
- pid: number().int().positive().optional(),
15046
- /**
15047
- * When this generation started. The pid's incarnation marker: a consumer
15048
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
15049
- * window when this changes, because the counter restarted from zero in a new
15050
- * process.
15051
- */
15052
- startedAtMs: number().optional(),
15053
- /**
15054
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
15055
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
15056
- * contribution is asked for.
15057
- *
15058
- * Cumulative and not a rate on purpose: a rate needs a window, a window
15059
- * needs a sampler, and a new per-node sampler is the defect half of
15060
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
15061
- * by whoever already keeps a history; a rate cannot be un-averaged.
15062
- *
15063
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
15064
- * an entry with no process.
15065
- */
15066
- cpuSeconds: number().optional(),
15067
- /** Resident bytes of this unit's process, same source and same rules. */
15068
- rssBytes: number().optional()
15069
- });
15070
- method(_void(), array(LoadContributionSchema).readonly());
15071
- /**
15072
15072
  * `login-method` — collection cap through which auth addons contribute
15073
15073
  * their pre-auth login surfaces to the login page. This is the SINGLE,
15074
15074
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -19806,12 +19806,53 @@ var MediaFileKindEnum = _enum([
19806
19806
  "keyFrameSmall",
19807
19807
  "thumbnailSmall"
19808
19808
  ]);
19809
+ /**
19810
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
19811
+ * ARE — never the bytes themselves.
19812
+ *
19813
+ * ## Why `url` and not `base64`
19814
+ *
19815
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
19816
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
19817
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
19818
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
19819
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
19820
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
19821
+ *
19822
+ * `url` points at the `event-media` data plane
19823
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
19824
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
19825
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
19826
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
19827
+ * no less protected than they were inside a `view`-level cap response — see
19828
+ * `data-plane-access.ts` for the rule and the one gap it does not close
19829
+ * (per-device scoping).
19830
+ *
19831
+ * The URL is built from the row's **stored** key, which is not always its
19832
+ * published `kind`: a track's face/plate crop is stored as `crop` under
19833
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
19834
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
19835
+ *
19836
+ * ## `base64` is TRANSITIONAL and is going away
19837
+ *
19838
+ * It is still populated for one reason: the deployed viewer's track-detail
19839
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
19840
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
19841
+ * triangle — not as absence. Removing the field before that viewer ships is an
19842
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
19843
+ * delete this line and the `withBytes` pass-through in
19844
+ * `analytics-query-facade.ts`; nothing else reads it.
19845
+ */
19809
19846
  var MediaFileSchema = object({
19810
19847
  key: string(),
19811
19848
  kind: MediaFileKindEnum,
19812
- base64: string(),
19813
19849
  sizeBytes: number(),
19814
19850
  timestamp: number()
19851
+ }).extend({
19852
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
19853
+ url: string(),
19854
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
19855
+ base64: string()
19815
19856
  });
19816
19857
  /**
19817
19858
  * One media row WITHOUT its bytes.
@@ -19823,7 +19864,9 @@ var MediaFileSchema = object({
19823
19864
  * blocks the whole view.
19824
19865
  *
19825
19866
  * `sizeBytes` is carried because it is what lets a client decide between the
19826
- * stored blob and a `?variant=thumb` rendering without fetching either.
19867
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
19868
+ * `url` because a client that had to build the plane path itself is a second
19869
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
19827
19870
  */
19828
19871
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
19829
19872
  /**
@@ -20512,6 +20555,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20512
20555
  }), array(MediaFileSchema).readonly()), method(object({
20513
20556
  trackId: string(),
20514
20557
  deviceId: number()
20558
+ }), array(MediaFileInfoSchema).readonly()), method(object({
20559
+ eventId: string(),
20560
+ deviceId: number()
20515
20561
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
20516
20562
  kind: "mutation",
20517
20563
  auth: "admin"
@@ -25510,10 +25556,24 @@ var FaceClusterSchema = object({
25510
25556
  size: number().int(),
25511
25557
  cohesion: number()
25512
25558
  });
25559
+ /**
25560
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25561
+ * are — never the bytes.
25562
+ *
25563
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25564
+ * track/event contract) is still populated because a deployed viewer requires
25565
+ * the field to parse a row at all; this method has no such reader. Its ONE
25566
+ * caller is the admin UI's detail modal, which was building
25567
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25568
+ * dialog already rendering its key FRAME from the `event-media` plane.
25569
+ *
25570
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25571
+ * media key directly, so this needed no new plane and no new access decision.
25572
+ */
25513
25573
  var MediaFileLiteSchema$1 = object({
25514
25574
  key: string(),
25515
25575
  kind: string(),
25516
- base64: string(),
25576
+ url: string(),
25517
25577
  sizeBytes: number(),
25518
25578
  timestamp: number()
25519
25579
  });
@@ -28535,10 +28595,24 @@ var PlateInfoSchema = object({
28535
28595
  */
28536
28596
  cropUrl: string().optional()
28537
28597
  });
28598
+ /**
28599
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
28600
+ * are — never the bytes.
28601
+ *
28602
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
28603
+ * track/event contract) is still populated because a deployed viewer requires
28604
+ * the field to parse a row at all; this method has no such reader. Its ONE
28605
+ * caller is the admin UI's detail modal, which was building
28606
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
28607
+ * dialog already rendering its key FRAME from the `event-media` plane.
28608
+ *
28609
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
28610
+ * media key directly, so this needed no new plane and no new access decision.
28611
+ */
28538
28612
  var MediaFileLiteSchema = object({
28539
28613
  key: string(),
28540
28614
  kind: string(),
28541
- base64: string(),
28615
+ url: string(),
28542
28616
  sizeBytes: number(),
28543
28617
  timestamp: number()
28544
28618
  });
@@ -36435,6 +36509,12 @@ Object.freeze({
36435
36509
  addonId: null,
36436
36510
  access: "view"
36437
36511
  },
36512
+ "pipelineAnalytics.listEventMedia": {
36513
+ capName: "pipeline-analytics",
36514
+ capScope: "device",
36515
+ addonId: null,
36516
+ access: "view"
36517
+ },
36438
36518
  "pipelineAnalytics.listGroups": {
36439
36519
  capName: "pipeline-analytics",
36440
36520
  capScope: "device",
@@ -40058,6 +40138,11 @@ Object.freeze({
40058
40138
  form: "array",
40059
40139
  optional: false
40060
40140
  }],
40141
+ "pipelineAnalytics.listEventMedia": [{
40142
+ name: "deviceId",
40143
+ form: "single",
40144
+ optional: false
40145
+ }],
40061
40146
  "pipelineAnalytics.listGroups": [{
40062
40147
  name: "deviceIds",
40063
40148
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-petkit",
3
- "version": "0.2.46",
3
+ "version": "0.2.47",
4
4
  "description": "PetKit smart-feeder device-provider addon for CamStack — wraps the @apocaliss92/nodepetkit PetKit cloud client",
5
5
  "keywords": [
6
6
  "camstack",