@camstack/addon-remote-storage 1.2.45 → 1.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.
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-BvqywmnV.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-Cysa81KY.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-BvqywmnV.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-Cysa81KY.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
  /**
@@ -18718,6 +18761,50 @@ var EventStoreFootprintSchema = object({
18718
18761
  totalBytes: number().int(),
18719
18762
  devices: array(EventStoreDeviceFootprintSchema).readonly()
18720
18763
  });
18764
+ /** Event-media footprint for one {@link MediaFileKind}. */
18765
+ var EventMediaKindFootprintSchema = object({
18766
+ kind: MediaFileKindEnum,
18767
+ /** Media rows of this kind. */
18768
+ rows: number().int(),
18769
+ /** Bytes on disk held by those rows. */
18770
+ bytes: number().int()
18771
+ });
18772
+ /**
18773
+ * The media footprint broken down by KIND — the axis a deletion decision
18774
+ * actually turns on.
18775
+ *
18776
+ * A byte total says how much there is; it cannot say what is safe to remove.
18777
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
18778
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
18779
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
18780
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
18781
+ * nothing else, so sizing a deletion means summing per kind.
18782
+ *
18783
+ * ## Why `unaccounted*` exists
18784
+ *
18785
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
18786
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
18787
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
18788
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
18789
+ * retired code path, or by a version that knew a kind this one does not) would
18790
+ * otherwise vanish from the total silently, and an operator would delete
18791
+ * against a denominator smaller than the disk.
18792
+ *
18793
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
18794
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
18795
+ */
18796
+ var EventMediaKindBreakdownSchema = object({
18797
+ /** Every media row in scope, from one unfiltered aggregate. */
18798
+ totalRows: number().int(),
18799
+ /** Every media byte in scope, from that same aggregate. */
18800
+ totalBytes: number().int(),
18801
+ /** Per-kind footprint, ordered by bytes descending. */
18802
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
18803
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
18804
+ unaccountedRows: number().int(),
18805
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
18806
+ unaccountedBytes: number().int()
18807
+ });
18721
18808
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
18722
18809
  var EventPruneCountsSchema = object({
18723
18810
  motion: number().int(),
@@ -18921,6 +19008,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18921
19008
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
18922
19009
  kind: "query",
18923
19010
  auth: "admin"
19011
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19012
+ kind: "query",
19013
+ auth: "admin"
18924
19014
  }), method(object({
18925
19015
  olderThanMs: number(),
18926
19016
  reason: OpsLogReasonSchema.optional()
@@ -19060,6 +19150,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19060
19150
  }), array(MediaFileSchema).readonly()), method(object({
19061
19151
  trackId: string(),
19062
19152
  deviceId: number()
19153
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19154
+ eventId: string(),
19155
+ deviceId: number()
19063
19156
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19064
19157
  kind: "mutation",
19065
19158
  auth: "admin"
@@ -20869,6 +20962,20 @@ method(object({
20869
20962
  error: string().optional()
20870
20963
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
20871
20964
  providerId: string(),
20965
+ /**
20966
+ * The location this config is an UNSAVED edit of, when there is one.
20967
+ *
20968
+ * `listLocations` replaces every declared secret with the redaction
20969
+ * sentinel, so the edit modal's form state holds the sentinel for any
20970
+ * credential the operator did not retype — and posting that here
20971
+ * without a way to resolve it makes the provider try to authenticate
20972
+ * as `__camstack_redacted__` and report the operator's own working
20973
+ * password as wrong. Given this id, the orchestrator restores each
20974
+ * sentinel from the stored config (same rule as `upsertLocation`)
20975
+ * before dispatching. Omitted by the "Add location" wizard, where
20976
+ * every value was typed just now and nothing is stored yet.
20977
+ */
20978
+ locationId: string().optional(),
20872
20979
  config: record(string(), unknown())
20873
20980
  }), object({
20874
20981
  ok: boolean(),
@@ -23380,10 +23487,24 @@ var FaceClusterSchema = object({
23380
23487
  size: number().int(),
23381
23488
  cohesion: number()
23382
23489
  });
23490
+ /**
23491
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23492
+ * are — never the bytes.
23493
+ *
23494
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23495
+ * track/event contract) is still populated because a deployed viewer requires
23496
+ * the field to parse a row at all; this method has no such reader. Its ONE
23497
+ * caller is the admin UI's detail modal, which was building
23498
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23499
+ * dialog already rendering its key FRAME from the `event-media` plane.
23500
+ *
23501
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23502
+ * media key directly, so this needed no new plane and no new access decision.
23503
+ */
23383
23504
  var MediaFileLiteSchema$1 = object({
23384
23505
  key: string(),
23385
23506
  kind: string(),
23386
- base64: string(),
23507
+ url: string(),
23387
23508
  sizeBytes: number(),
23388
23509
  timestamp: number()
23389
23510
  });
@@ -25635,10 +25756,24 @@ var PlateInfoSchema = object({
25635
25756
  */
25636
25757
  cropUrl: string().optional()
25637
25758
  });
25759
+ /**
25760
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25761
+ * are — never the bytes.
25762
+ *
25763
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25764
+ * track/event contract) is still populated because a deployed viewer requires
25765
+ * the field to parse a row at all; this method has no such reader. Its ONE
25766
+ * caller is the admin UI's detail modal, which was building
25767
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25768
+ * dialog already rendering its key FRAME from the `event-media` plane.
25769
+ *
25770
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25771
+ * media key directly, so this needed no new plane and no new access decision.
25772
+ */
25638
25773
  var MediaFileLiteSchema = object({
25639
25774
  key: string(),
25640
25775
  kind: string(),
25641
- base64: string(),
25776
+ url: string(),
25642
25777
  sizeBytes: number(),
25643
25778
  timestamp: number()
25644
25779
  });
@@ -31555,6 +31690,12 @@ Object.freeze({
31555
31690
  addonId: null,
31556
31691
  access: "view"
31557
31692
  },
31693
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
31694
+ capName: "pipeline-analytics",
31695
+ capScope: "device",
31696
+ addonId: null,
31697
+ access: "view"
31698
+ },
31558
31699
  "pipelineAnalytics.getEventStoreFootprint": {
31559
31700
  capName: "pipeline-analytics",
31560
31701
  capScope: "device",
@@ -31651,6 +31792,12 @@ Object.freeze({
31651
31792
  addonId: null,
31652
31793
  access: "view"
31653
31794
  },
31795
+ "pipelineAnalytics.listEventMedia": {
31796
+ capName: "pipeline-analytics",
31797
+ capScope: "device",
31798
+ addonId: null,
31799
+ access: "view"
31800
+ },
31654
31801
  "pipelineAnalytics.listGroups": {
31655
31802
  capName: "pipeline-analytics",
31656
31803
  capScope: "device",
@@ -35214,6 +35361,11 @@ Object.freeze({
35214
35361
  form: "single",
35215
35362
  optional: false
35216
35363
  }],
35364
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
35365
+ name: "deviceId",
35366
+ form: "single",
35367
+ optional: true
35368
+ }],
35217
35369
  "pipelineAnalytics.getGroup": [{
35218
35370
  name: "deviceId",
35219
35371
  form: "single",
@@ -35274,6 +35426,11 @@ Object.freeze({
35274
35426
  form: "array",
35275
35427
  optional: false
35276
35428
  }],
35429
+ "pipelineAnalytics.listEventMedia": [{
35430
+ name: "deviceId",
35431
+ form: "single",
35432
+ optional: false
35433
+ }],
35277
35434
  "pipelineAnalytics.listGroups": [{
35278
35435
  name: "deviceIds",
35279
35436
  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
  /**
@@ -18695,6 +18738,50 @@ var EventStoreFootprintSchema = object({
18695
18738
  totalBytes: number().int(),
18696
18739
  devices: array(EventStoreDeviceFootprintSchema).readonly()
18697
18740
  });
18741
+ /** Event-media footprint for one {@link MediaFileKind}. */
18742
+ var EventMediaKindFootprintSchema = object({
18743
+ kind: MediaFileKindEnum,
18744
+ /** Media rows of this kind. */
18745
+ rows: number().int(),
18746
+ /** Bytes on disk held by those rows. */
18747
+ bytes: number().int()
18748
+ });
18749
+ /**
18750
+ * The media footprint broken down by KIND — the axis a deletion decision
18751
+ * actually turns on.
18752
+ *
18753
+ * A byte total says how much there is; it cannot say what is safe to remove.
18754
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
18755
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
18756
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
18757
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
18758
+ * nothing else, so sizing a deletion means summing per kind.
18759
+ *
18760
+ * ## Why `unaccounted*` exists
18761
+ *
18762
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
18763
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
18764
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
18765
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
18766
+ * retired code path, or by a version that knew a kind this one does not) would
18767
+ * otherwise vanish from the total silently, and an operator would delete
18768
+ * against a denominator smaller than the disk.
18769
+ *
18770
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
18771
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
18772
+ */
18773
+ var EventMediaKindBreakdownSchema = object({
18774
+ /** Every media row in scope, from one unfiltered aggregate. */
18775
+ totalRows: number().int(),
18776
+ /** Every media byte in scope, from that same aggregate. */
18777
+ totalBytes: number().int(),
18778
+ /** Per-kind footprint, ordered by bytes descending. */
18779
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
18780
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
18781
+ unaccountedRows: number().int(),
18782
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
18783
+ unaccountedBytes: number().int()
18784
+ });
18698
18785
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
18699
18786
  var EventPruneCountsSchema = object({
18700
18787
  motion: number().int(),
@@ -18898,6 +18985,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18898
18985
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
18899
18986
  kind: "query",
18900
18987
  auth: "admin"
18988
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
18989
+ kind: "query",
18990
+ auth: "admin"
18901
18991
  }), method(object({
18902
18992
  olderThanMs: number(),
18903
18993
  reason: OpsLogReasonSchema.optional()
@@ -19037,6 +19127,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19037
19127
  }), array(MediaFileSchema).readonly()), method(object({
19038
19128
  trackId: string(),
19039
19129
  deviceId: number()
19130
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19131
+ eventId: string(),
19132
+ deviceId: number()
19040
19133
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19041
19134
  kind: "mutation",
19042
19135
  auth: "admin"
@@ -20846,6 +20939,20 @@ method(object({
20846
20939
  error: string().optional()
20847
20940
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
20848
20941
  providerId: string(),
20942
+ /**
20943
+ * The location this config is an UNSAVED edit of, when there is one.
20944
+ *
20945
+ * `listLocations` replaces every declared secret with the redaction
20946
+ * sentinel, so the edit modal's form state holds the sentinel for any
20947
+ * credential the operator did not retype — and posting that here
20948
+ * without a way to resolve it makes the provider try to authenticate
20949
+ * as `__camstack_redacted__` and report the operator's own working
20950
+ * password as wrong. Given this id, the orchestrator restores each
20951
+ * sentinel from the stored config (same rule as `upsertLocation`)
20952
+ * before dispatching. Omitted by the "Add location" wizard, where
20953
+ * every value was typed just now and nothing is stored yet.
20954
+ */
20955
+ locationId: string().optional(),
20849
20956
  config: record(string(), unknown())
20850
20957
  }), object({
20851
20958
  ok: boolean(),
@@ -23357,10 +23464,24 @@ var FaceClusterSchema = object({
23357
23464
  size: number().int(),
23358
23465
  cohesion: number()
23359
23466
  });
23467
+ /**
23468
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23469
+ * are — never the bytes.
23470
+ *
23471
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23472
+ * track/event contract) is still populated because a deployed viewer requires
23473
+ * the field to parse a row at all; this method has no such reader. Its ONE
23474
+ * caller is the admin UI's detail modal, which was building
23475
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23476
+ * dialog already rendering its key FRAME from the `event-media` plane.
23477
+ *
23478
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23479
+ * media key directly, so this needed no new plane and no new access decision.
23480
+ */
23360
23481
  var MediaFileLiteSchema$1 = object({
23361
23482
  key: string(),
23362
23483
  kind: string(),
23363
- base64: string(),
23484
+ url: string(),
23364
23485
  sizeBytes: number(),
23365
23486
  timestamp: number()
23366
23487
  });
@@ -25612,10 +25733,24 @@ var PlateInfoSchema = object({
25612
25733
  */
25613
25734
  cropUrl: string().optional()
25614
25735
  });
25736
+ /**
25737
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25738
+ * are — never the bytes.
25739
+ *
25740
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25741
+ * track/event contract) is still populated because a deployed viewer requires
25742
+ * the field to parse a row at all; this method has no such reader. Its ONE
25743
+ * caller is the admin UI's detail modal, which was building
25744
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25745
+ * dialog already rendering its key FRAME from the `event-media` plane.
25746
+ *
25747
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25748
+ * media key directly, so this needed no new plane and no new access decision.
25749
+ */
25615
25750
  var MediaFileLiteSchema = object({
25616
25751
  key: string(),
25617
25752
  kind: string(),
25618
- base64: string(),
25753
+ url: string(),
25619
25754
  sizeBytes: number(),
25620
25755
  timestamp: number()
25621
25756
  });
@@ -31532,6 +31667,12 @@ Object.freeze({
31532
31667
  addonId: null,
31533
31668
  access: "view"
31534
31669
  },
31670
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
31671
+ capName: "pipeline-analytics",
31672
+ capScope: "device",
31673
+ addonId: null,
31674
+ access: "view"
31675
+ },
31535
31676
  "pipelineAnalytics.getEventStoreFootprint": {
31536
31677
  capName: "pipeline-analytics",
31537
31678
  capScope: "device",
@@ -31628,6 +31769,12 @@ Object.freeze({
31628
31769
  addonId: null,
31629
31770
  access: "view"
31630
31771
  },
31772
+ "pipelineAnalytics.listEventMedia": {
31773
+ capName: "pipeline-analytics",
31774
+ capScope: "device",
31775
+ addonId: null,
31776
+ access: "view"
31777
+ },
31631
31778
  "pipelineAnalytics.listGroups": {
31632
31779
  capName: "pipeline-analytics",
31633
31780
  capScope: "device",
@@ -35191,6 +35338,11 @@ Object.freeze({
35191
35338
  form: "single",
35192
35339
  optional: false
35193
35340
  }],
35341
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
35342
+ name: "deviceId",
35343
+ form: "single",
35344
+ optional: true
35345
+ }],
35194
35346
  "pipelineAnalytics.getGroup": [{
35195
35347
  name: "deviceId",
35196
35348
  form: "single",
@@ -35251,6 +35403,11 @@ Object.freeze({
35251
35403
  form: "array",
35252
35404
  optional: false
35253
35405
  }],
35406
+ "pipelineAnalytics.listEventMedia": [{
35407
+ name: "deviceId",
35408
+ form: "single",
35409
+ optional: false
35410
+ }],
35254
35411
  "pipelineAnalytics.listGroups": [{
35255
35412
  name: "deviceIds",
35256
35413
  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-BvqywmnV.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-Cysa81KY.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-BvqywmnV.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-Cysa81KY.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.47",
4
4
  "description": "Remote storage providers (SFTP, S3, WebDAV, SMB) — unifies remote backends behind the storage-provider cap",
5
5
  "keywords": [
6
6
  "camstack",