@camstack/addon-remote-storage 1.2.45 → 1.2.46

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.
package/dist/s3.addon.js CHANGED
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_shared = require("./shared-BHqbklNT.js");
5
+ const require_shared = require("./shared-DAxSJcnX.js");
6
6
  let node_stream = require("node:stream");
7
7
  let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
8
8
  let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
package/dist/s3.addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { c as BaseAddon, i as rearmIdleAbort, n as getOptionalBasePath, o as scheduleIdleAbort, s as storageProviderCapability, t as createSessionId } from "./shared-CSwUP1hZ.mjs";
1
+ import { c as BaseAddon, i as rearmIdleAbort, n as getOptionalBasePath, o as scheduleIdleAbort, s as storageProviderCapability, t as createSessionId } from "./shared-hUiD7Ohg.mjs";
2
2
  import { PassThrough } from "node:stream";
3
3
  import { DeleteObjectCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
4
4
  import { Upload } from "@aws-sdk/lib-storage";
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_shared = require("./shared-BHqbklNT.js");
5
+ const require_shared = require("./shared-DAxSJcnX.js");
6
6
  let ssh2 = require("ssh2");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_shared.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { a as safeJoinRemotePath, c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as storageProviderCapability, t as createSessionId } from "./shared-CSwUP1hZ.mjs";
1
+ import { a as safeJoinRemotePath, c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as storageProviderCapability, t as createSessionId } from "./shared-hUiD7Ohg.mjs";
2
2
  import { Client } from "ssh2";
3
3
  import * as path from "node:path";
4
4
  //#region src/providers/sftp/sftp-config-schema.ts
@@ -12928,6 +12928,114 @@ method(object({
12928
12928
  height: number()
12929
12929
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
12930
12930
  /**
12931
+ * `failure-contribution` — the capability an addon reports its OWN losses
12932
+ * through, per camera, with the denominator attached. It stores nothing.
12933
+ *
12934
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
12935
+ *
12936
+ * `load-contribution` answers *what did this camera COST*. This answers *what
12937
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
12938
+ * copied: the contributor reports what it already knows, hub-main adds only
12939
+ * `addonId`, nothing needs global knowledge, and there is no central list for
12940
+ * somebody to forget to edit.
12941
+ *
12942
+ * They are not merged, because their invariants are opposites:
12943
+ *
12944
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
12945
+ * claim a camera cost nothing, which is a measurement nobody made;
12946
+ * - a `failure-contribution` zero is the **most valuable value on the
12947
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
12948
+ * and it is exactly what an absent entry cannot say.
12949
+ *
12950
+ * Putting a loss counter on a cost entry would also break the reconciliation
12951
+ * that gives `load-contribution` its point: contributions are subtracted from
12952
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
12953
+ * has no process.
12954
+ *
12955
+ * ## Why not a log line, since the counters already exist
12956
+ *
12957
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
12958
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
12959
+ * ends in a log line, and a log line is the thing the operator asked to stop
12960
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
12961
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
12962
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
12963
+ * media blackout were both diagnosed. The counters stay; this is where they can
12964
+ * be READ.
12965
+ *
12966
+ * ## The rate is served with its denominator or not at all
12967
+ *
12968
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
12969
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
12970
+ * than yesterday" and was **flat across twelve hours** once divided by the
12971
+ * successes on the same path. A surface that publishes only the numerator
12972
+ * reproduces that mistake on every read.
12973
+ *
12974
+ * ## Shape
12975
+ *
12976
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
12977
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
12978
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
12979
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
12980
+ * a forked runner's entries reach hub-main over transport that already exists.
12981
+ * No new UDS message, no second registry (D3). The operator reads the assembled
12982
+ * result through `system.getFailureContributions`.
12983
+ */
12984
+ var FailureReasonCountSchema = object({
12985
+ /**
12986
+ * Why the attempt did not land, in the contributor's own vocabulary —
12987
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
12988
+ * strings that already appear in this repo's logs and, where one exists, the
12989
+ * same string the per-track `previewMissReason` records (D276): a second
12990
+ * vocabulary for the same loss would make the row and the counter
12991
+ * un-joinable.
12992
+ */
12993
+ reason: string(),
12994
+ count: number().int().nonnegative()
12995
+ });
12996
+ var FailureContributionSchema = object({
12997
+ /**
12998
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
12999
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13000
+ * `unit` free: the families are owned by different addons and a shared enum
13001
+ * is a central list that rots invisibly.
13002
+ */
13003
+ family: string(),
13004
+ /**
13005
+ * The NUMERIC device id — the same value every log line carries as
13006
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13007
+ * cannot name the camera must not emit the entry, because a fleet total
13008
+ * cannot answer the only question anybody asks of this surface.
13009
+ */
13010
+ deviceId: number().int().positive(),
13011
+ /**
13012
+ * A second dimension inside the family: the model / step id for an inference
13013
+ * timeout, so "which camera AND which model" is one read. Absent when the
13014
+ * family has a single variant.
13015
+ */
13016
+ variant: string().optional(),
13017
+ /**
13018
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13019
+ * differencing two reads must drop the interval when it changes, because the
13020
+ * counter restarted from zero in a respawned runner. Same discipline as
13021
+ * `LoadContribution.startedAtMs`.
13022
+ */
13023
+ sinceMs: number(),
13024
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13025
+ atMs: number(),
13026
+ /**
13027
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13028
+ * window. A failure count published without it is the mistake this schema
13029
+ * exists to make impossible.
13030
+ */
13031
+ attempts: number().int().nonnegative(),
13032
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13033
+ succeeded: number().int().nonnegative(),
13034
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13035
+ reasons: array(FailureReasonCountSchema).readonly()
13036
+ });
13037
+ method(_void(), array(FailureContributionSchema).readonly());
13038
+ /**
12931
13039
  * filesystem-browse — per-node capability for browsing the node's local
12932
13040
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
12933
13041
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13449,6 +13557,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13449
13557
  kind: "mutation",
13450
13558
  auth: "admin"
13451
13559
  });
13560
+ var LoadContributionSchema = object({
13561
+ role: _enum([
13562
+ "decode",
13563
+ "transcode",
13564
+ "recording",
13565
+ "streaming",
13566
+ "detection"
13567
+ ]),
13568
+ /**
13569
+ * The NUMERIC device id — the same value every log line carries as
13570
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13571
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13572
+ * contributor that cannot name its camera must not emit the entry at all,
13573
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13574
+ * and would quietly turn one camera's cost into everybody's.
13575
+ */
13576
+ deviceId: number().int().positive().nullable(),
13577
+ attribution: _enum([
13578
+ "measured",
13579
+ "accounted",
13580
+ "unattributable"
13581
+ ]),
13582
+ /**
13583
+ * What ONE entry is, in the contributor's own words — `615/high`,
13584
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13585
+ * family and inventing a common one would lose the only information that
13586
+ * makes two entries for the same camera distinguishable.
13587
+ */
13588
+ unit: string(),
13589
+ /**
13590
+ * The OS process this cost lives in, when there is one. Present so a
13591
+ * consumer can (a) tell two generations of the same unit apart across a
13592
+ * restart, and (b) subtract claimed processes from the node's process
13593
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13594
+ * process of its own.
13595
+ */
13596
+ pid: number().int().positive().optional(),
13597
+ /**
13598
+ * When this generation started. The pid's incarnation marker: a consumer
13599
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13600
+ * window when this changes, because the counter restarted from zero in a new
13601
+ * process.
13602
+ */
13603
+ startedAtMs: number().optional(),
13604
+ /**
13605
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13606
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13607
+ * contribution is asked for.
13608
+ *
13609
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13610
+ * needs a sampler, and a new per-node sampler is the defect half of
13611
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13612
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13613
+ *
13614
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13615
+ * an entry with no process.
13616
+ */
13617
+ cpuSeconds: number().optional(),
13618
+ /** Resident bytes of this unit's process, same source and same rules. */
13619
+ rssBytes: number().optional()
13620
+ });
13621
+ method(_void(), array(LoadContributionSchema).readonly());
13452
13622
  /**
13453
13623
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13454
13624
  * through. It stores nothing.
@@ -13525,176 +13695,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13525
13695
  tags: record(string(), string()).optional()
13526
13696
  }), array(LogEntrySchema).readonly());
13527
13697
  /**
13528
- * `failure-contribution` — the capability an addon reports its OWN losses
13529
- * through, per camera, with the denominator attached. It stores nothing.
13530
- *
13531
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13532
- *
13533
- * `load-contribution` answers *what did this camera COST*. This answers *what
13534
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13535
- * copied: the contributor reports what it already knows, hub-main adds only
13536
- * `addonId`, nothing needs global knowledge, and there is no central list for
13537
- * somebody to forget to edit.
13538
- *
13539
- * They are not merged, because their invariants are opposites:
13540
- *
13541
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13542
- * claim a camera cost nothing, which is a measurement nobody made;
13543
- * - a `failure-contribution` zero is the **most valuable value on the
13544
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13545
- * and it is exactly what an absent entry cannot say.
13546
- *
13547
- * Putting a loss counter on a cost entry would also break the reconciliation
13548
- * that gives `load-contribution` its point: contributions are subtracted from
13549
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13550
- * has no process.
13551
- *
13552
- * ## Why not a log line, since the counters already exist
13553
- *
13554
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13555
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13556
- * ends in a log line, and a log line is the thing the operator asked to stop
13557
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13558
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13559
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13560
- * media blackout were both diagnosed. The counters stay; this is where they can
13561
- * be READ.
13562
- *
13563
- * ## The rate is served with its denominator or not at all
13564
- *
13565
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13566
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13567
- * than yesterday" and was **flat across twelve hours** once divided by the
13568
- * successes on the same path. A surface that publishes only the numerator
13569
- * reproduces that mistake on every read.
13570
- *
13571
- * ## Shape
13572
- *
13573
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13574
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13575
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13576
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13577
- * a forked runner's entries reach hub-main over transport that already exists.
13578
- * No new UDS message, no second registry (D3). The operator reads the assembled
13579
- * result through `system.getFailureContributions`.
13580
- */
13581
- var FailureReasonCountSchema = object({
13582
- /**
13583
- * Why the attempt did not land, in the contributor's own vocabulary —
13584
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13585
- * strings that already appear in this repo's logs and, where one exists, the
13586
- * same string the per-track `previewMissReason` records (D276): a second
13587
- * vocabulary for the same loss would make the row and the counter
13588
- * un-joinable.
13589
- */
13590
- reason: string(),
13591
- count: number().int().nonnegative()
13592
- });
13593
- var FailureContributionSchema = object({
13594
- /**
13595
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13596
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13597
- * `unit` free: the families are owned by different addons and a shared enum
13598
- * is a central list that rots invisibly.
13599
- */
13600
- family: string(),
13601
- /**
13602
- * The NUMERIC device id — the same value every log line carries as
13603
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13604
- * cannot name the camera must not emit the entry, because a fleet total
13605
- * cannot answer the only question anybody asks of this surface.
13606
- */
13607
- deviceId: number().int().positive(),
13608
- /**
13609
- * A second dimension inside the family: the model / step id for an inference
13610
- * timeout, so "which camera AND which model" is one read. Absent when the
13611
- * family has a single variant.
13612
- */
13613
- variant: string().optional(),
13614
- /**
13615
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13616
- * differencing two reads must drop the interval when it changes, because the
13617
- * counter restarted from zero in a respawned runner. Same discipline as
13618
- * `LoadContribution.startedAtMs`.
13619
- */
13620
- sinceMs: number(),
13621
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13622
- atMs: number(),
13623
- /**
13624
- * THE DENOMINATOR — every attempt on this path for this camera in the
13625
- * window. A failure count published without it is the mistake this schema
13626
- * exists to make impossible.
13627
- */
13628
- attempts: number().int().nonnegative(),
13629
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13630
- succeeded: number().int().nonnegative(),
13631
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13632
- reasons: array(FailureReasonCountSchema).readonly()
13633
- });
13634
- method(_void(), array(FailureContributionSchema).readonly());
13635
- var LoadContributionSchema = object({
13636
- role: _enum([
13637
- "decode",
13638
- "transcode",
13639
- "recording",
13640
- "streaming",
13641
- "detection"
13642
- ]),
13643
- /**
13644
- * The NUMERIC device id — the same value every log line carries as
13645
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13646
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13647
- * contributor that cannot name its camera must not emit the entry at all,
13648
- * because an unnamed per-camera entry is indistinguishable from a shared one
13649
- * and would quietly turn one camera's cost into everybody's.
13650
- */
13651
- deviceId: number().int().positive().nullable(),
13652
- attribution: _enum([
13653
- "measured",
13654
- "accounted",
13655
- "unattributable"
13656
- ]),
13657
- /**
13658
- * What ONE entry is, in the contributor's own words — `615/high`,
13659
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13660
- * family and inventing a common one would lose the only information that
13661
- * makes two entries for the same camera distinguishable.
13662
- */
13663
- unit: string(),
13664
- /**
13665
- * The OS process this cost lives in, when there is one. Present so a
13666
- * consumer can (a) tell two generations of the same unit apart across a
13667
- * restart, and (b) subtract claimed processes from the node's process
13668
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13669
- * process of its own.
13670
- */
13671
- pid: number().int().positive().optional(),
13672
- /**
13673
- * When this generation started. The pid's incarnation marker: a consumer
13674
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13675
- * window when this changes, because the counter restarted from zero in a new
13676
- * process.
13677
- */
13678
- startedAtMs: number().optional(),
13679
- /**
13680
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13681
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13682
- * contribution is asked for.
13683
- *
13684
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13685
- * needs a sampler, and a new per-node sampler is the defect half of
13686
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13687
- * by whoever already keeps a history; a rate cannot be un-averaged.
13688
- *
13689
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13690
- * an entry with no process.
13691
- */
13692
- cpuSeconds: number().optional(),
13693
- /** Resident bytes of this unit's process, same source and same rules. */
13694
- rssBytes: number().optional()
13695
- });
13696
- method(_void(), array(LoadContributionSchema).readonly());
13697
- /**
13698
13698
  * `login-method` — collection cap through which auth addons contribute
13699
13699
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13700
13700
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18354,12 +18354,53 @@ var MediaFileKindEnum = _enum([
18354
18354
  "keyFrameSmall",
18355
18355
  "thumbnailSmall"
18356
18356
  ]);
18357
+ /**
18358
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18359
+ * ARE — never the bytes themselves.
18360
+ *
18361
+ * ## Why `url` and not `base64`
18362
+ *
18363
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18364
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18365
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18366
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18367
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18368
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18369
+ *
18370
+ * `url` points at the `event-media` data plane
18371
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18372
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18373
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18374
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18375
+ * no less protected than they were inside a `view`-level cap response — see
18376
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18377
+ * (per-device scoping).
18378
+ *
18379
+ * The URL is built from the row's **stored** key, which is not always its
18380
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18381
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18382
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18383
+ *
18384
+ * ## `base64` is TRANSITIONAL and is going away
18385
+ *
18386
+ * It is still populated for one reason: the deployed viewer's track-detail
18387
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18388
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18389
+ * triangle — not as absence. Removing the field before that viewer ships is an
18390
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18391
+ * delete this line and the `withBytes` pass-through in
18392
+ * `analytics-query-facade.ts`; nothing else reads it.
18393
+ */
18357
18394
  var MediaFileSchema = object({
18358
18395
  key: string(),
18359
18396
  kind: MediaFileKindEnum,
18360
- base64: string(),
18361
18397
  sizeBytes: number(),
18362
18398
  timestamp: number()
18399
+ }).extend({
18400
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18401
+ url: string(),
18402
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18403
+ base64: string()
18363
18404
  });
18364
18405
  /**
18365
18406
  * One media row WITHOUT its bytes.
@@ -18371,7 +18412,9 @@ var MediaFileSchema = object({
18371
18412
  * blocks the whole view.
18372
18413
  *
18373
18414
  * `sizeBytes` is carried because it is what lets a client decide between the
18374
- * stored blob and a `?variant=thumb` rendering without fetching either.
18415
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18416
+ * `url` because a client that had to build the plane path itself is a second
18417
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18375
18418
  */
18376
18419
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18377
18420
  /**
@@ -19060,6 +19103,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19060
19103
  }), array(MediaFileSchema).readonly()), method(object({
19061
19104
  trackId: string(),
19062
19105
  deviceId: number()
19106
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19107
+ eventId: string(),
19108
+ deviceId: number()
19063
19109
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19064
19110
  kind: "mutation",
19065
19111
  auth: "admin"
@@ -23380,10 +23426,24 @@ var FaceClusterSchema = object({
23380
23426
  size: number().int(),
23381
23427
  cohesion: number()
23382
23428
  });
23429
+ /**
23430
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23431
+ * are — never the bytes.
23432
+ *
23433
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23434
+ * track/event contract) is still populated because a deployed viewer requires
23435
+ * the field to parse a row at all; this method has no such reader. Its ONE
23436
+ * caller is the admin UI's detail modal, which was building
23437
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23438
+ * dialog already rendering its key FRAME from the `event-media` plane.
23439
+ *
23440
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23441
+ * media key directly, so this needed no new plane and no new access decision.
23442
+ */
23383
23443
  var MediaFileLiteSchema$1 = object({
23384
23444
  key: string(),
23385
23445
  kind: string(),
23386
- base64: string(),
23446
+ url: string(),
23387
23447
  sizeBytes: number(),
23388
23448
  timestamp: number()
23389
23449
  });
@@ -25635,10 +25695,24 @@ var PlateInfoSchema = object({
25635
25695
  */
25636
25696
  cropUrl: string().optional()
25637
25697
  });
25698
+ /**
25699
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25700
+ * are — never the bytes.
25701
+ *
25702
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25703
+ * track/event contract) is still populated because a deployed viewer requires
25704
+ * the field to parse a row at all; this method has no such reader. Its ONE
25705
+ * caller is the admin UI's detail modal, which was building
25706
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25707
+ * dialog already rendering its key FRAME from the `event-media` plane.
25708
+ *
25709
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25710
+ * media key directly, so this needed no new plane and no new access decision.
25711
+ */
25638
25712
  var MediaFileLiteSchema = object({
25639
25713
  key: string(),
25640
25714
  kind: string(),
25641
- base64: string(),
25715
+ url: string(),
25642
25716
  sizeBytes: number(),
25643
25717
  timestamp: number()
25644
25718
  });
@@ -31651,6 +31725,12 @@ Object.freeze({
31651
31725
  addonId: null,
31652
31726
  access: "view"
31653
31727
  },
31728
+ "pipelineAnalytics.listEventMedia": {
31729
+ capName: "pipeline-analytics",
31730
+ capScope: "device",
31731
+ addonId: null,
31732
+ access: "view"
31733
+ },
31654
31734
  "pipelineAnalytics.listGroups": {
31655
31735
  capName: "pipeline-analytics",
31656
31736
  capScope: "device",
@@ -35274,6 +35354,11 @@ Object.freeze({
35274
35354
  form: "array",
35275
35355
  optional: false
35276
35356
  }],
35357
+ "pipelineAnalytics.listEventMedia": [{
35358
+ name: "deviceId",
35359
+ form: "single",
35360
+ optional: false
35361
+ }],
35277
35362
  "pipelineAnalytics.listGroups": [{
35278
35363
  name: "deviceIds",
35279
35364
  form: "array",
@@ -12905,6 +12905,114 @@ method(object({
12905
12905
  height: number()
12906
12906
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
12907
12907
  /**
12908
+ * `failure-contribution` — the capability an addon reports its OWN losses
12909
+ * through, per camera, with the denominator attached. It stores nothing.
12910
+ *
12911
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
12912
+ *
12913
+ * `load-contribution` answers *what did this camera COST*. This answers *what
12914
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
12915
+ * copied: the contributor reports what it already knows, hub-main adds only
12916
+ * `addonId`, nothing needs global knowledge, and there is no central list for
12917
+ * somebody to forget to edit.
12918
+ *
12919
+ * They are not merged, because their invariants are opposites:
12920
+ *
12921
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
12922
+ * claim a camera cost nothing, which is a measurement nobody made;
12923
+ * - a `failure-contribution` zero is the **most valuable value on the
12924
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
12925
+ * and it is exactly what an absent entry cannot say.
12926
+ *
12927
+ * Putting a loss counter on a cost entry would also break the reconciliation
12928
+ * that gives `load-contribution` its point: contributions are subtracted from
12929
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
12930
+ * has no process.
12931
+ *
12932
+ * ## Why not a log line, since the counters already exist
12933
+ *
12934
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
12935
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
12936
+ * ends in a log line, and a log line is the thing the operator asked to stop
12937
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
12938
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
12939
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
12940
+ * media blackout were both diagnosed. The counters stay; this is where they can
12941
+ * be READ.
12942
+ *
12943
+ * ## The rate is served with its denominator or not at all
12944
+ *
12945
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
12946
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
12947
+ * than yesterday" and was **flat across twelve hours** once divided by the
12948
+ * successes on the same path. A surface that publishes only the numerator
12949
+ * reproduces that mistake on every read.
12950
+ *
12951
+ * ## Shape
12952
+ *
12953
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
12954
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
12955
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
12956
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
12957
+ * a forked runner's entries reach hub-main over transport that already exists.
12958
+ * No new UDS message, no second registry (D3). The operator reads the assembled
12959
+ * result through `system.getFailureContributions`.
12960
+ */
12961
+ var FailureReasonCountSchema = object({
12962
+ /**
12963
+ * Why the attempt did not land, in the contributor's own vocabulary —
12964
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
12965
+ * strings that already appear in this repo's logs and, where one exists, the
12966
+ * same string the per-track `previewMissReason` records (D276): a second
12967
+ * vocabulary for the same loss would make the row and the counter
12968
+ * un-joinable.
12969
+ */
12970
+ reason: string(),
12971
+ count: number().int().nonnegative()
12972
+ });
12973
+ var FailureContributionSchema = object({
12974
+ /**
12975
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
12976
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
12977
+ * `unit` free: the families are owned by different addons and a shared enum
12978
+ * is a central list that rots invisibly.
12979
+ */
12980
+ family: string(),
12981
+ /**
12982
+ * The NUMERIC device id — the same value every log line carries as
12983
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
12984
+ * cannot name the camera must not emit the entry, because a fleet total
12985
+ * cannot answer the only question anybody asks of this surface.
12986
+ */
12987
+ deviceId: number().int().positive(),
12988
+ /**
12989
+ * A second dimension inside the family: the model / step id for an inference
12990
+ * timeout, so "which camera AND which model" is one read. Absent when the
12991
+ * family has a single variant.
12992
+ */
12993
+ variant: string().optional(),
12994
+ /**
12995
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
12996
+ * differencing two reads must drop the interval when it changes, because the
12997
+ * counter restarted from zero in a respawned runner. Same discipline as
12998
+ * `LoadContribution.startedAtMs`.
12999
+ */
13000
+ sinceMs: number(),
13001
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13002
+ atMs: number(),
13003
+ /**
13004
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13005
+ * window. A failure count published without it is the mistake this schema
13006
+ * exists to make impossible.
13007
+ */
13008
+ attempts: number().int().nonnegative(),
13009
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13010
+ succeeded: number().int().nonnegative(),
13011
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13012
+ reasons: array(FailureReasonCountSchema).readonly()
13013
+ });
13014
+ method(_void(), array(FailureContributionSchema).readonly());
13015
+ /**
12908
13016
  * filesystem-browse — per-node capability for browsing the node's local
12909
13017
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
12910
13018
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13426,6 +13534,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13426
13534
  kind: "mutation",
13427
13535
  auth: "admin"
13428
13536
  });
13537
+ var LoadContributionSchema = object({
13538
+ role: _enum([
13539
+ "decode",
13540
+ "transcode",
13541
+ "recording",
13542
+ "streaming",
13543
+ "detection"
13544
+ ]),
13545
+ /**
13546
+ * The NUMERIC device id — the same value every log line carries as
13547
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13548
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13549
+ * contributor that cannot name its camera must not emit the entry at all,
13550
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13551
+ * and would quietly turn one camera's cost into everybody's.
13552
+ */
13553
+ deviceId: number().int().positive().nullable(),
13554
+ attribution: _enum([
13555
+ "measured",
13556
+ "accounted",
13557
+ "unattributable"
13558
+ ]),
13559
+ /**
13560
+ * What ONE entry is, in the contributor's own words — `615/high`,
13561
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13562
+ * family and inventing a common one would lose the only information that
13563
+ * makes two entries for the same camera distinguishable.
13564
+ */
13565
+ unit: string(),
13566
+ /**
13567
+ * The OS process this cost lives in, when there is one. Present so a
13568
+ * consumer can (a) tell two generations of the same unit apart across a
13569
+ * restart, and (b) subtract claimed processes from the node's process
13570
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13571
+ * process of its own.
13572
+ */
13573
+ pid: number().int().positive().optional(),
13574
+ /**
13575
+ * When this generation started. The pid's incarnation marker: a consumer
13576
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13577
+ * window when this changes, because the counter restarted from zero in a new
13578
+ * process.
13579
+ */
13580
+ startedAtMs: number().optional(),
13581
+ /**
13582
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13583
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13584
+ * contribution is asked for.
13585
+ *
13586
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13587
+ * needs a sampler, and a new per-node sampler is the defect half of
13588
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13589
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13590
+ *
13591
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13592
+ * an entry with no process.
13593
+ */
13594
+ cpuSeconds: number().optional(),
13595
+ /** Resident bytes of this unit's process, same source and same rules. */
13596
+ rssBytes: number().optional()
13597
+ });
13598
+ method(_void(), array(LoadContributionSchema).readonly());
13429
13599
  /**
13430
13600
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13431
13601
  * through. It stores nothing.
@@ -13502,176 +13672,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13502
13672
  tags: record(string(), string()).optional()
13503
13673
  }), array(LogEntrySchema).readonly());
13504
13674
  /**
13505
- * `failure-contribution` — the capability an addon reports its OWN losses
13506
- * through, per camera, with the denominator attached. It stores nothing.
13507
- *
13508
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13509
- *
13510
- * `load-contribution` answers *what did this camera COST*. This answers *what
13511
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13512
- * copied: the contributor reports what it already knows, hub-main adds only
13513
- * `addonId`, nothing needs global knowledge, and there is no central list for
13514
- * somebody to forget to edit.
13515
- *
13516
- * They are not merged, because their invariants are opposites:
13517
- *
13518
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13519
- * claim a camera cost nothing, which is a measurement nobody made;
13520
- * - a `failure-contribution` zero is the **most valuable value on the
13521
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13522
- * and it is exactly what an absent entry cannot say.
13523
- *
13524
- * Putting a loss counter on a cost entry would also break the reconciliation
13525
- * that gives `load-contribution` its point: contributions are subtracted from
13526
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13527
- * has no process.
13528
- *
13529
- * ## Why not a log line, since the counters already exist
13530
- *
13531
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13532
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13533
- * ends in a log line, and a log line is the thing the operator asked to stop
13534
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13535
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13536
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13537
- * media blackout were both diagnosed. The counters stay; this is where they can
13538
- * be READ.
13539
- *
13540
- * ## The rate is served with its denominator or not at all
13541
- *
13542
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13543
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13544
- * than yesterday" and was **flat across twelve hours** once divided by the
13545
- * successes on the same path. A surface that publishes only the numerator
13546
- * reproduces that mistake on every read.
13547
- *
13548
- * ## Shape
13549
- *
13550
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13551
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13552
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13553
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13554
- * a forked runner's entries reach hub-main over transport that already exists.
13555
- * No new UDS message, no second registry (D3). The operator reads the assembled
13556
- * result through `system.getFailureContributions`.
13557
- */
13558
- var FailureReasonCountSchema = object({
13559
- /**
13560
- * Why the attempt did not land, in the contributor's own vocabulary —
13561
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13562
- * strings that already appear in this repo's logs and, where one exists, the
13563
- * same string the per-track `previewMissReason` records (D276): a second
13564
- * vocabulary for the same loss would make the row and the counter
13565
- * un-joinable.
13566
- */
13567
- reason: string(),
13568
- count: number().int().nonnegative()
13569
- });
13570
- var FailureContributionSchema = object({
13571
- /**
13572
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13573
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13574
- * `unit` free: the families are owned by different addons and a shared enum
13575
- * is a central list that rots invisibly.
13576
- */
13577
- family: string(),
13578
- /**
13579
- * The NUMERIC device id — the same value every log line carries as
13580
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13581
- * cannot name the camera must not emit the entry, because a fleet total
13582
- * cannot answer the only question anybody asks of this surface.
13583
- */
13584
- deviceId: number().int().positive(),
13585
- /**
13586
- * A second dimension inside the family: the model / step id for an inference
13587
- * timeout, so "which camera AND which model" is one read. Absent when the
13588
- * family has a single variant.
13589
- */
13590
- variant: string().optional(),
13591
- /**
13592
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13593
- * differencing two reads must drop the interval when it changes, because the
13594
- * counter restarted from zero in a respawned runner. Same discipline as
13595
- * `LoadContribution.startedAtMs`.
13596
- */
13597
- sinceMs: number(),
13598
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13599
- atMs: number(),
13600
- /**
13601
- * THE DENOMINATOR — every attempt on this path for this camera in the
13602
- * window. A failure count published without it is the mistake this schema
13603
- * exists to make impossible.
13604
- */
13605
- attempts: number().int().nonnegative(),
13606
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13607
- succeeded: number().int().nonnegative(),
13608
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13609
- reasons: array(FailureReasonCountSchema).readonly()
13610
- });
13611
- method(_void(), array(FailureContributionSchema).readonly());
13612
- var LoadContributionSchema = object({
13613
- role: _enum([
13614
- "decode",
13615
- "transcode",
13616
- "recording",
13617
- "streaming",
13618
- "detection"
13619
- ]),
13620
- /**
13621
- * The NUMERIC device id — the same value every log line carries as
13622
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13623
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13624
- * contributor that cannot name its camera must not emit the entry at all,
13625
- * because an unnamed per-camera entry is indistinguishable from a shared one
13626
- * and would quietly turn one camera's cost into everybody's.
13627
- */
13628
- deviceId: number().int().positive().nullable(),
13629
- attribution: _enum([
13630
- "measured",
13631
- "accounted",
13632
- "unattributable"
13633
- ]),
13634
- /**
13635
- * What ONE entry is, in the contributor's own words — `615/high`,
13636
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13637
- * family and inventing a common one would lose the only information that
13638
- * makes two entries for the same camera distinguishable.
13639
- */
13640
- unit: string(),
13641
- /**
13642
- * The OS process this cost lives in, when there is one. Present so a
13643
- * consumer can (a) tell two generations of the same unit apart across a
13644
- * restart, and (b) subtract claimed processes from the node's process
13645
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13646
- * process of its own.
13647
- */
13648
- pid: number().int().positive().optional(),
13649
- /**
13650
- * When this generation started. The pid's incarnation marker: a consumer
13651
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13652
- * window when this changes, because the counter restarted from zero in a new
13653
- * process.
13654
- */
13655
- startedAtMs: number().optional(),
13656
- /**
13657
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13658
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13659
- * contribution is asked for.
13660
- *
13661
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13662
- * needs a sampler, and a new per-node sampler is the defect half of
13663
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13664
- * by whoever already keeps a history; a rate cannot be un-averaged.
13665
- *
13666
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13667
- * an entry with no process.
13668
- */
13669
- cpuSeconds: number().optional(),
13670
- /** Resident bytes of this unit's process, same source and same rules. */
13671
- rssBytes: number().optional()
13672
- });
13673
- method(_void(), array(LoadContributionSchema).readonly());
13674
- /**
13675
13675
  * `login-method` — collection cap through which auth addons contribute
13676
13676
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13677
13677
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18331,12 +18331,53 @@ var MediaFileKindEnum = _enum([
18331
18331
  "keyFrameSmall",
18332
18332
  "thumbnailSmall"
18333
18333
  ]);
18334
+ /**
18335
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18336
+ * ARE — never the bytes themselves.
18337
+ *
18338
+ * ## Why `url` and not `base64`
18339
+ *
18340
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18341
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18342
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18343
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18344
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18345
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18346
+ *
18347
+ * `url` points at the `event-media` data plane
18348
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18349
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18350
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18351
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18352
+ * no less protected than they were inside a `view`-level cap response — see
18353
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18354
+ * (per-device scoping).
18355
+ *
18356
+ * The URL is built from the row's **stored** key, which is not always its
18357
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18358
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18359
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18360
+ *
18361
+ * ## `base64` is TRANSITIONAL and is going away
18362
+ *
18363
+ * It is still populated for one reason: the deployed viewer's track-detail
18364
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18365
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18366
+ * triangle — not as absence. Removing the field before that viewer ships is an
18367
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18368
+ * delete this line and the `withBytes` pass-through in
18369
+ * `analytics-query-facade.ts`; nothing else reads it.
18370
+ */
18334
18371
  var MediaFileSchema = object({
18335
18372
  key: string(),
18336
18373
  kind: MediaFileKindEnum,
18337
- base64: string(),
18338
18374
  sizeBytes: number(),
18339
18375
  timestamp: number()
18376
+ }).extend({
18377
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18378
+ url: string(),
18379
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18380
+ base64: string()
18340
18381
  });
18341
18382
  /**
18342
18383
  * One media row WITHOUT its bytes.
@@ -18348,7 +18389,9 @@ var MediaFileSchema = object({
18348
18389
  * blocks the whole view.
18349
18390
  *
18350
18391
  * `sizeBytes` is carried because it is what lets a client decide between the
18351
- * stored blob and a `?variant=thumb` rendering without fetching either.
18392
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18393
+ * `url` because a client that had to build the plane path itself is a second
18394
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18352
18395
  */
18353
18396
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18354
18397
  /**
@@ -19037,6 +19080,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19037
19080
  }), array(MediaFileSchema).readonly()), method(object({
19038
19081
  trackId: string(),
19039
19082
  deviceId: number()
19083
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19084
+ eventId: string(),
19085
+ deviceId: number()
19040
19086
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19041
19087
  kind: "mutation",
19042
19088
  auth: "admin"
@@ -23357,10 +23403,24 @@ var FaceClusterSchema = object({
23357
23403
  size: number().int(),
23358
23404
  cohesion: number()
23359
23405
  });
23406
+ /**
23407
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23408
+ * are — never the bytes.
23409
+ *
23410
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23411
+ * track/event contract) is still populated because a deployed viewer requires
23412
+ * the field to parse a row at all; this method has no such reader. Its ONE
23413
+ * caller is the admin UI's detail modal, which was building
23414
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23415
+ * dialog already rendering its key FRAME from the `event-media` plane.
23416
+ *
23417
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23418
+ * media key directly, so this needed no new plane and no new access decision.
23419
+ */
23360
23420
  var MediaFileLiteSchema$1 = object({
23361
23421
  key: string(),
23362
23422
  kind: string(),
23363
- base64: string(),
23423
+ url: string(),
23364
23424
  sizeBytes: number(),
23365
23425
  timestamp: number()
23366
23426
  });
@@ -25612,10 +25672,24 @@ var PlateInfoSchema = object({
25612
25672
  */
25613
25673
  cropUrl: string().optional()
25614
25674
  });
25675
+ /**
25676
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25677
+ * are — never the bytes.
25678
+ *
25679
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25680
+ * track/event contract) is still populated because a deployed viewer requires
25681
+ * the field to parse a row at all; this method has no such reader. Its ONE
25682
+ * caller is the admin UI's detail modal, which was building
25683
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25684
+ * dialog already rendering its key FRAME from the `event-media` plane.
25685
+ *
25686
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25687
+ * media key directly, so this needed no new plane and no new access decision.
25688
+ */
25615
25689
  var MediaFileLiteSchema = object({
25616
25690
  key: string(),
25617
25691
  kind: string(),
25618
- base64: string(),
25692
+ url: string(),
25619
25693
  sizeBytes: number(),
25620
25694
  timestamp: number()
25621
25695
  });
@@ -31628,6 +31702,12 @@ Object.freeze({
31628
31702
  addonId: null,
31629
31703
  access: "view"
31630
31704
  },
31705
+ "pipelineAnalytics.listEventMedia": {
31706
+ capName: "pipeline-analytics",
31707
+ capScope: "device",
31708
+ addonId: null,
31709
+ access: "view"
31710
+ },
31631
31711
  "pipelineAnalytics.listGroups": {
31632
31712
  capName: "pipeline-analytics",
31633
31713
  capScope: "device",
@@ -35251,6 +35331,11 @@ Object.freeze({
35251
35331
  form: "array",
35252
35332
  optional: false
35253
35333
  }],
35334
+ "pipelineAnalytics.listEventMedia": [{
35335
+ name: "deviceId",
35336
+ form: "single",
35337
+ optional: false
35338
+ }],
35254
35339
  "pipelineAnalytics.listGroups": [{
35255
35340
  name: "deviceIds",
35256
35341
  form: "array",
package/dist/smb.addon.js CHANGED
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_shared = require("./shared-BHqbklNT.js");
5
+ const require_shared = require("./shared-DAxSJcnX.js");
6
6
  let node_crypto = require("node:crypto");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_shared.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, s as storageProviderCapability } from "./shared-CSwUP1hZ.mjs";
1
+ import { c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, s as storageProviderCapability } from "./shared-hUiD7Ohg.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as path from "node:path";
4
4
  import * as fsp from "node:fs/promises";
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_shared = require("./shared-BHqbklNT.js");
5
+ const require_shared = require("./shared-DAxSJcnX.js");
6
6
  let node_stream = require("node:stream");
7
7
  let webdav = require("webdav");
8
8
  //#region src/providers/webdav/webdav-config-schema.ts
@@ -1,4 +1,4 @@
1
- import { a as safeJoinRemotePath, c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as storageProviderCapability, t as createSessionId } from "./shared-CSwUP1hZ.mjs";
1
+ import { a as safeJoinRemotePath, c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as storageProviderCapability, t as createSessionId } from "./shared-hUiD7Ohg.mjs";
2
2
  import { PassThrough } from "node:stream";
3
3
  import { AuthType, createClient } from "webdav";
4
4
  //#region src/providers/webdav/webdav-config-schema.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-remote-storage",
3
- "version": "1.2.45",
3
+ "version": "1.2.46",
4
4
  "description": "Remote storage providers (SFTP, S3, WebDAV, SMB) — unifies remote backends behind the storage-provider cap",
5
5
  "keywords": [
6
6
  "camstack",