@camstack/addon-decoder-nodeav 1.2.46 → 1.2.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +331 -174
  2. package/dist/index.mjs +331 -174
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13050,6 +13050,114 @@ method(object({
13050
13050
  height: number()
13051
13051
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13052
13052
  /**
13053
+ * `failure-contribution` — the capability an addon reports its OWN losses
13054
+ * through, per camera, with the denominator attached. It stores nothing.
13055
+ *
13056
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13057
+ *
13058
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13059
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13060
+ * copied: the contributor reports what it already knows, hub-main adds only
13061
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13062
+ * somebody to forget to edit.
13063
+ *
13064
+ * They are not merged, because their invariants are opposites:
13065
+ *
13066
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13067
+ * claim a camera cost nothing, which is a measurement nobody made;
13068
+ * - a `failure-contribution` zero is the **most valuable value on the
13069
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13070
+ * and it is exactly what an absent entry cannot say.
13071
+ *
13072
+ * Putting a loss counter on a cost entry would also break the reconciliation
13073
+ * that gives `load-contribution` its point: contributions are subtracted from
13074
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13075
+ * has no process.
13076
+ *
13077
+ * ## Why not a log line, since the counters already exist
13078
+ *
13079
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13080
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13081
+ * ends in a log line, and a log line is the thing the operator asked to stop
13082
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13083
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13084
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13085
+ * media blackout were both diagnosed. The counters stay; this is where they can
13086
+ * be READ.
13087
+ *
13088
+ * ## The rate is served with its denominator or not at all
13089
+ *
13090
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13091
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13092
+ * than yesterday" and was **flat across twelve hours** once divided by the
13093
+ * successes on the same path. A surface that publishes only the numerator
13094
+ * reproduces that mistake on every read.
13095
+ *
13096
+ * ## Shape
13097
+ *
13098
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13099
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13100
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13101
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13102
+ * a forked runner's entries reach hub-main over transport that already exists.
13103
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13104
+ * result through `system.getFailureContributions`.
13105
+ */
13106
+ var FailureReasonCountSchema = object({
13107
+ /**
13108
+ * Why the attempt did not land, in the contributor's own vocabulary —
13109
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13110
+ * strings that already appear in this repo's logs and, where one exists, the
13111
+ * same string the per-track `previewMissReason` records (D276): a second
13112
+ * vocabulary for the same loss would make the row and the counter
13113
+ * un-joinable.
13114
+ */
13115
+ reason: string(),
13116
+ count: number().int().nonnegative()
13117
+ });
13118
+ var FailureContributionSchema = object({
13119
+ /**
13120
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13121
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13122
+ * `unit` free: the families are owned by different addons and a shared enum
13123
+ * is a central list that rots invisibly.
13124
+ */
13125
+ family: string(),
13126
+ /**
13127
+ * The NUMERIC device id — the same value every log line carries as
13128
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13129
+ * cannot name the camera must not emit the entry, because a fleet total
13130
+ * cannot answer the only question anybody asks of this surface.
13131
+ */
13132
+ deviceId: number().int().positive(),
13133
+ /**
13134
+ * A second dimension inside the family: the model / step id for an inference
13135
+ * timeout, so "which camera AND which model" is one read. Absent when the
13136
+ * family has a single variant.
13137
+ */
13138
+ variant: string().optional(),
13139
+ /**
13140
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13141
+ * differencing two reads must drop the interval when it changes, because the
13142
+ * counter restarted from zero in a respawned runner. Same discipline as
13143
+ * `LoadContribution.startedAtMs`.
13144
+ */
13145
+ sinceMs: number(),
13146
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13147
+ atMs: number(),
13148
+ /**
13149
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13150
+ * window. A failure count published without it is the mistake this schema
13151
+ * exists to make impossible.
13152
+ */
13153
+ attempts: number().int().nonnegative(),
13154
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13155
+ succeeded: number().int().nonnegative(),
13156
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13157
+ reasons: array(FailureReasonCountSchema).readonly()
13158
+ });
13159
+ method(_void(), array(FailureContributionSchema).readonly());
13160
+ /**
13053
13161
  * filesystem-browse — per-node capability for browsing the node's local
13054
13162
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13055
13163
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13571,6 +13679,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13571
13679
  kind: "mutation",
13572
13680
  auth: "admin"
13573
13681
  });
13682
+ var LoadContributionSchema = object({
13683
+ role: _enum([
13684
+ "decode",
13685
+ "transcode",
13686
+ "recording",
13687
+ "streaming",
13688
+ "detection"
13689
+ ]),
13690
+ /**
13691
+ * The NUMERIC device id — the same value every log line carries as
13692
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13693
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13694
+ * contributor that cannot name its camera must not emit the entry at all,
13695
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13696
+ * and would quietly turn one camera's cost into everybody's.
13697
+ */
13698
+ deviceId: number().int().positive().nullable(),
13699
+ attribution: _enum([
13700
+ "measured",
13701
+ "accounted",
13702
+ "unattributable"
13703
+ ]),
13704
+ /**
13705
+ * What ONE entry is, in the contributor's own words — `615/high`,
13706
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13707
+ * family and inventing a common one would lose the only information that
13708
+ * makes two entries for the same camera distinguishable.
13709
+ */
13710
+ unit: string(),
13711
+ /**
13712
+ * The OS process this cost lives in, when there is one. Present so a
13713
+ * consumer can (a) tell two generations of the same unit apart across a
13714
+ * restart, and (b) subtract claimed processes from the node's process
13715
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13716
+ * process of its own.
13717
+ */
13718
+ pid: number().int().positive().optional(),
13719
+ /**
13720
+ * When this generation started. The pid's incarnation marker: a consumer
13721
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13722
+ * window when this changes, because the counter restarted from zero in a new
13723
+ * process.
13724
+ */
13725
+ startedAtMs: number().optional(),
13726
+ /**
13727
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13728
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13729
+ * contribution is asked for.
13730
+ *
13731
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13732
+ * needs a sampler, and a new per-node sampler is the defect half of
13733
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13734
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13735
+ *
13736
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13737
+ * an entry with no process.
13738
+ */
13739
+ cpuSeconds: number().optional(),
13740
+ /** Resident bytes of this unit's process, same source and same rules. */
13741
+ rssBytes: number().optional()
13742
+ });
13743
+ method(_void(), array(LoadContributionSchema).readonly());
13574
13744
  /**
13575
13745
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13576
13746
  * through. It stores nothing.
@@ -13647,176 +13817,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13647
13817
  tags: record(string(), string()).optional()
13648
13818
  }), array(LogEntrySchema).readonly());
13649
13819
  /**
13650
- * `failure-contribution` — the capability an addon reports its OWN losses
13651
- * through, per camera, with the denominator attached. It stores nothing.
13652
- *
13653
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13654
- *
13655
- * `load-contribution` answers *what did this camera COST*. This answers *what
13656
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13657
- * copied: the contributor reports what it already knows, hub-main adds only
13658
- * `addonId`, nothing needs global knowledge, and there is no central list for
13659
- * somebody to forget to edit.
13660
- *
13661
- * They are not merged, because their invariants are opposites:
13662
- *
13663
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13664
- * claim a camera cost nothing, which is a measurement nobody made;
13665
- * - a `failure-contribution` zero is the **most valuable value on the
13666
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13667
- * and it is exactly what an absent entry cannot say.
13668
- *
13669
- * Putting a loss counter on a cost entry would also break the reconciliation
13670
- * that gives `load-contribution` its point: contributions are subtracted from
13671
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13672
- * has no process.
13673
- *
13674
- * ## Why not a log line, since the counters already exist
13675
- *
13676
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13677
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13678
- * ends in a log line, and a log line is the thing the operator asked to stop
13679
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13680
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13681
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13682
- * media blackout were both diagnosed. The counters stay; this is where they can
13683
- * be READ.
13684
- *
13685
- * ## The rate is served with its denominator or not at all
13686
- *
13687
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13688
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13689
- * than yesterday" and was **flat across twelve hours** once divided by the
13690
- * successes on the same path. A surface that publishes only the numerator
13691
- * reproduces that mistake on every read.
13692
- *
13693
- * ## Shape
13694
- *
13695
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13696
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13697
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13698
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13699
- * a forked runner's entries reach hub-main over transport that already exists.
13700
- * No new UDS message, no second registry (D3). The operator reads the assembled
13701
- * result through `system.getFailureContributions`.
13702
- */
13703
- var FailureReasonCountSchema = object({
13704
- /**
13705
- * Why the attempt did not land, in the contributor's own vocabulary —
13706
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13707
- * strings that already appear in this repo's logs and, where one exists, the
13708
- * same string the per-track `previewMissReason` records (D276): a second
13709
- * vocabulary for the same loss would make the row and the counter
13710
- * un-joinable.
13711
- */
13712
- reason: string(),
13713
- count: number().int().nonnegative()
13714
- });
13715
- var FailureContributionSchema = object({
13716
- /**
13717
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13718
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13719
- * `unit` free: the families are owned by different addons and a shared enum
13720
- * is a central list that rots invisibly.
13721
- */
13722
- family: string(),
13723
- /**
13724
- * The NUMERIC device id — the same value every log line carries as
13725
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13726
- * cannot name the camera must not emit the entry, because a fleet total
13727
- * cannot answer the only question anybody asks of this surface.
13728
- */
13729
- deviceId: number().int().positive(),
13730
- /**
13731
- * A second dimension inside the family: the model / step id for an inference
13732
- * timeout, so "which camera AND which model" is one read. Absent when the
13733
- * family has a single variant.
13734
- */
13735
- variant: string().optional(),
13736
- /**
13737
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13738
- * differencing two reads must drop the interval when it changes, because the
13739
- * counter restarted from zero in a respawned runner. Same discipline as
13740
- * `LoadContribution.startedAtMs`.
13741
- */
13742
- sinceMs: number(),
13743
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13744
- atMs: number(),
13745
- /**
13746
- * THE DENOMINATOR — every attempt on this path for this camera in the
13747
- * window. A failure count published without it is the mistake this schema
13748
- * exists to make impossible.
13749
- */
13750
- attempts: number().int().nonnegative(),
13751
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13752
- succeeded: number().int().nonnegative(),
13753
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13754
- reasons: array(FailureReasonCountSchema).readonly()
13755
- });
13756
- method(_void(), array(FailureContributionSchema).readonly());
13757
- var LoadContributionSchema = object({
13758
- role: _enum([
13759
- "decode",
13760
- "transcode",
13761
- "recording",
13762
- "streaming",
13763
- "detection"
13764
- ]),
13765
- /**
13766
- * The NUMERIC device id — the same value every log line carries as
13767
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13768
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13769
- * contributor that cannot name its camera must not emit the entry at all,
13770
- * because an unnamed per-camera entry is indistinguishable from a shared one
13771
- * and would quietly turn one camera's cost into everybody's.
13772
- */
13773
- deviceId: number().int().positive().nullable(),
13774
- attribution: _enum([
13775
- "measured",
13776
- "accounted",
13777
- "unattributable"
13778
- ]),
13779
- /**
13780
- * What ONE entry is, in the contributor's own words — `615/high`,
13781
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13782
- * family and inventing a common one would lose the only information that
13783
- * makes two entries for the same camera distinguishable.
13784
- */
13785
- unit: string(),
13786
- /**
13787
- * The OS process this cost lives in, when there is one. Present so a
13788
- * consumer can (a) tell two generations of the same unit apart across a
13789
- * restart, and (b) subtract claimed processes from the node's process
13790
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13791
- * process of its own.
13792
- */
13793
- pid: number().int().positive().optional(),
13794
- /**
13795
- * When this generation started. The pid's incarnation marker: a consumer
13796
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13797
- * window when this changes, because the counter restarted from zero in a new
13798
- * process.
13799
- */
13800
- startedAtMs: number().optional(),
13801
- /**
13802
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13803
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13804
- * contribution is asked for.
13805
- *
13806
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13807
- * needs a sampler, and a new per-node sampler is the defect half of
13808
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13809
- * by whoever already keeps a history; a rate cannot be un-averaged.
13810
- *
13811
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13812
- * an entry with no process.
13813
- */
13814
- cpuSeconds: number().optional(),
13815
- /** Resident bytes of this unit's process, same source and same rules. */
13816
- rssBytes: number().optional()
13817
- });
13818
- method(_void(), array(LoadContributionSchema).readonly());
13819
- /**
13820
13820
  * `login-method` — collection cap through which auth addons contribute
13821
13821
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13822
13822
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18476,12 +18476,53 @@ var MediaFileKindEnum = _enum([
18476
18476
  "keyFrameSmall",
18477
18477
  "thumbnailSmall"
18478
18478
  ]);
18479
+ /**
18480
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18481
+ * ARE — never the bytes themselves.
18482
+ *
18483
+ * ## Why `url` and not `base64`
18484
+ *
18485
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18486
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18487
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18488
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18489
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18490
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18491
+ *
18492
+ * `url` points at the `event-media` data plane
18493
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18494
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18495
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18496
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18497
+ * no less protected than they were inside a `view`-level cap response — see
18498
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18499
+ * (per-device scoping).
18500
+ *
18501
+ * The URL is built from the row's **stored** key, which is not always its
18502
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18503
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18504
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18505
+ *
18506
+ * ## `base64` is TRANSITIONAL and is going away
18507
+ *
18508
+ * It is still populated for one reason: the deployed viewer's track-detail
18509
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18510
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18511
+ * triangle — not as absence. Removing the field before that viewer ships is an
18512
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18513
+ * delete this line and the `withBytes` pass-through in
18514
+ * `analytics-query-facade.ts`; nothing else reads it.
18515
+ */
18479
18516
  var MediaFileSchema = object({
18480
18517
  key: string(),
18481
18518
  kind: MediaFileKindEnum,
18482
- base64: string(),
18483
18519
  sizeBytes: number(),
18484
18520
  timestamp: number()
18521
+ }).extend({
18522
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18523
+ url: string(),
18524
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18525
+ base64: string()
18485
18526
  });
18486
18527
  /**
18487
18528
  * One media row WITHOUT its bytes.
@@ -18493,7 +18534,9 @@ var MediaFileSchema = object({
18493
18534
  * blocks the whole view.
18494
18535
  *
18495
18536
  * `sizeBytes` is carried because it is what lets a client decide between the
18496
- * stored blob and a `?variant=thumb` rendering without fetching either.
18537
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18538
+ * `url` because a client that had to build the plane path itself is a second
18539
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18497
18540
  */
18498
18541
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18499
18542
  /**
@@ -18840,6 +18883,50 @@ var EventStoreFootprintSchema = object({
18840
18883
  totalBytes: number().int(),
18841
18884
  devices: array(EventStoreDeviceFootprintSchema).readonly()
18842
18885
  });
18886
+ /** Event-media footprint for one {@link MediaFileKind}. */
18887
+ var EventMediaKindFootprintSchema = object({
18888
+ kind: MediaFileKindEnum,
18889
+ /** Media rows of this kind. */
18890
+ rows: number().int(),
18891
+ /** Bytes on disk held by those rows. */
18892
+ bytes: number().int()
18893
+ });
18894
+ /**
18895
+ * The media footprint broken down by KIND — the axis a deletion decision
18896
+ * actually turns on.
18897
+ *
18898
+ * A byte total says how much there is; it cannot say what is safe to remove.
18899
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
18900
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
18901
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
18902
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
18903
+ * nothing else, so sizing a deletion means summing per kind.
18904
+ *
18905
+ * ## Why `unaccounted*` exists
18906
+ *
18907
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
18908
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
18909
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
18910
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
18911
+ * retired code path, or by a version that knew a kind this one does not) would
18912
+ * otherwise vanish from the total silently, and an operator would delete
18913
+ * against a denominator smaller than the disk.
18914
+ *
18915
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
18916
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
18917
+ */
18918
+ var EventMediaKindBreakdownSchema = object({
18919
+ /** Every media row in scope, from one unfiltered aggregate. */
18920
+ totalRows: number().int(),
18921
+ /** Every media byte in scope, from that same aggregate. */
18922
+ totalBytes: number().int(),
18923
+ /** Per-kind footprint, ordered by bytes descending. */
18924
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
18925
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
18926
+ unaccountedRows: number().int(),
18927
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
18928
+ unaccountedBytes: number().int()
18929
+ });
18843
18930
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
18844
18931
  var EventPruneCountsSchema = object({
18845
18932
  motion: number().int(),
@@ -19043,6 +19130,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19043
19130
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19044
19131
  kind: "query",
19045
19132
  auth: "admin"
19133
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19134
+ kind: "query",
19135
+ auth: "admin"
19046
19136
  }), method(object({
19047
19137
  olderThanMs: number(),
19048
19138
  reason: OpsLogReasonSchema.optional()
@@ -19182,6 +19272,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19182
19272
  }), array(MediaFileSchema).readonly()), method(object({
19183
19273
  trackId: string(),
19184
19274
  deviceId: number()
19275
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19276
+ eventId: string(),
19277
+ deviceId: number()
19185
19278
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19186
19279
  kind: "mutation",
19187
19280
  auth: "admin"
@@ -20991,6 +21084,20 @@ method(object({
20991
21084
  error: string().optional()
20992
21085
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
20993
21086
  providerId: string(),
21087
+ /**
21088
+ * The location this config is an UNSAVED edit of, when there is one.
21089
+ *
21090
+ * `listLocations` replaces every declared secret with the redaction
21091
+ * sentinel, so the edit modal's form state holds the sentinel for any
21092
+ * credential the operator did not retype — and posting that here
21093
+ * without a way to resolve it makes the provider try to authenticate
21094
+ * as `__camstack_redacted__` and report the operator's own working
21095
+ * password as wrong. Given this id, the orchestrator restores each
21096
+ * sentinel from the stored config (same rule as `upsertLocation`)
21097
+ * before dispatching. Omitted by the "Add location" wizard, where
21098
+ * every value was typed just now and nothing is stored yet.
21099
+ */
21100
+ locationId: string().optional(),
20994
21101
  config: record(string(), unknown())
20995
21102
  }), object({
20996
21103
  ok: boolean(),
@@ -23442,10 +23549,24 @@ var FaceClusterSchema = object({
23442
23549
  size: number().int(),
23443
23550
  cohesion: number()
23444
23551
  });
23552
+ /**
23553
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23554
+ * are — never the bytes.
23555
+ *
23556
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23557
+ * track/event contract) is still populated because a deployed viewer requires
23558
+ * the field to parse a row at all; this method has no such reader. Its ONE
23559
+ * caller is the admin UI's detail modal, which was building
23560
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23561
+ * dialog already rendering its key FRAME from the `event-media` plane.
23562
+ *
23563
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23564
+ * media key directly, so this needed no new plane and no new access decision.
23565
+ */
23445
23566
  var MediaFileLiteSchema$1 = object({
23446
23567
  key: string(),
23447
23568
  kind: string(),
23448
- base64: string(),
23569
+ url: string(),
23449
23570
  sizeBytes: number(),
23450
23571
  timestamp: number()
23451
23572
  });
@@ -25697,10 +25818,24 @@ var PlateInfoSchema = object({
25697
25818
  */
25698
25819
  cropUrl: string().optional()
25699
25820
  });
25821
+ /**
25822
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25823
+ * are — never the bytes.
25824
+ *
25825
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25826
+ * track/event contract) is still populated because a deployed viewer requires
25827
+ * the field to parse a row at all; this method has no such reader. Its ONE
25828
+ * caller is the admin UI's detail modal, which was building
25829
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25830
+ * dialog already rendering its key FRAME from the `event-media` plane.
25831
+ *
25832
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25833
+ * media key directly, so this needed no new plane and no new access decision.
25834
+ */
25700
25835
  var MediaFileLiteSchema = object({
25701
25836
  key: string(),
25702
25837
  kind: string(),
25703
- base64: string(),
25838
+ url: string(),
25704
25839
  sizeBytes: number(),
25705
25840
  timestamp: number()
25706
25841
  });
@@ -31617,6 +31752,12 @@ Object.freeze({
31617
31752
  addonId: null,
31618
31753
  access: "view"
31619
31754
  },
31755
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
31756
+ capName: "pipeline-analytics",
31757
+ capScope: "device",
31758
+ addonId: null,
31759
+ access: "view"
31760
+ },
31620
31761
  "pipelineAnalytics.getEventStoreFootprint": {
31621
31762
  capName: "pipeline-analytics",
31622
31763
  capScope: "device",
@@ -31713,6 +31854,12 @@ Object.freeze({
31713
31854
  addonId: null,
31714
31855
  access: "view"
31715
31856
  },
31857
+ "pipelineAnalytics.listEventMedia": {
31858
+ capName: "pipeline-analytics",
31859
+ capScope: "device",
31860
+ addonId: null,
31861
+ access: "view"
31862
+ },
31716
31863
  "pipelineAnalytics.listGroups": {
31717
31864
  capName: "pipeline-analytics",
31718
31865
  capScope: "device",
@@ -35276,6 +35423,11 @@ Object.freeze({
35276
35423
  form: "single",
35277
35424
  optional: false
35278
35425
  }],
35426
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
35427
+ name: "deviceId",
35428
+ form: "single",
35429
+ optional: true
35430
+ }],
35279
35431
  "pipelineAnalytics.getGroup": [{
35280
35432
  name: "deviceId",
35281
35433
  form: "single",
@@ -35336,6 +35488,11 @@ Object.freeze({
35336
35488
  form: "array",
35337
35489
  optional: false
35338
35490
  }],
35491
+ "pipelineAnalytics.listEventMedia": [{
35492
+ name: "deviceId",
35493
+ form: "single",
35494
+ optional: false
35495
+ }],
35339
35496
  "pipelineAnalytics.listGroups": [{
35340
35497
  name: "deviceIds",
35341
35498
  form: "array",
package/dist/index.mjs CHANGED
@@ -13046,6 +13046,114 @@ method(object({
13046
13046
  height: number()
13047
13047
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13048
13048
  /**
13049
+ * `failure-contribution` — the capability an addon reports its OWN losses
13050
+ * through, per camera, with the denominator attached. It stores nothing.
13051
+ *
13052
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13053
+ *
13054
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13055
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13056
+ * copied: the contributor reports what it already knows, hub-main adds only
13057
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13058
+ * somebody to forget to edit.
13059
+ *
13060
+ * They are not merged, because their invariants are opposites:
13061
+ *
13062
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13063
+ * claim a camera cost nothing, which is a measurement nobody made;
13064
+ * - a `failure-contribution` zero is the **most valuable value on the
13065
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13066
+ * and it is exactly what an absent entry cannot say.
13067
+ *
13068
+ * Putting a loss counter on a cost entry would also break the reconciliation
13069
+ * that gives `load-contribution` its point: contributions are subtracted from
13070
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13071
+ * has no process.
13072
+ *
13073
+ * ## Why not a log line, since the counters already exist
13074
+ *
13075
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13076
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13077
+ * ends in a log line, and a log line is the thing the operator asked to stop
13078
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13079
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13080
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13081
+ * media blackout were both diagnosed. The counters stay; this is where they can
13082
+ * be READ.
13083
+ *
13084
+ * ## The rate is served with its denominator or not at all
13085
+ *
13086
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13087
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13088
+ * than yesterday" and was **flat across twelve hours** once divided by the
13089
+ * successes on the same path. A surface that publishes only the numerator
13090
+ * reproduces that mistake on every read.
13091
+ *
13092
+ * ## Shape
13093
+ *
13094
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13095
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13096
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13097
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13098
+ * a forked runner's entries reach hub-main over transport that already exists.
13099
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13100
+ * result through `system.getFailureContributions`.
13101
+ */
13102
+ var FailureReasonCountSchema = object({
13103
+ /**
13104
+ * Why the attempt did not land, in the contributor's own vocabulary —
13105
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13106
+ * strings that already appear in this repo's logs and, where one exists, the
13107
+ * same string the per-track `previewMissReason` records (D276): a second
13108
+ * vocabulary for the same loss would make the row and the counter
13109
+ * un-joinable.
13110
+ */
13111
+ reason: string(),
13112
+ count: number().int().nonnegative()
13113
+ });
13114
+ var FailureContributionSchema = object({
13115
+ /**
13116
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13117
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13118
+ * `unit` free: the families are owned by different addons and a shared enum
13119
+ * is a central list that rots invisibly.
13120
+ */
13121
+ family: string(),
13122
+ /**
13123
+ * The NUMERIC device id — the same value every log line carries as
13124
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13125
+ * cannot name the camera must not emit the entry, because a fleet total
13126
+ * cannot answer the only question anybody asks of this surface.
13127
+ */
13128
+ deviceId: number().int().positive(),
13129
+ /**
13130
+ * A second dimension inside the family: the model / step id for an inference
13131
+ * timeout, so "which camera AND which model" is one read. Absent when the
13132
+ * family has a single variant.
13133
+ */
13134
+ variant: string().optional(),
13135
+ /**
13136
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13137
+ * differencing two reads must drop the interval when it changes, because the
13138
+ * counter restarted from zero in a respawned runner. Same discipline as
13139
+ * `LoadContribution.startedAtMs`.
13140
+ */
13141
+ sinceMs: number(),
13142
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13143
+ atMs: number(),
13144
+ /**
13145
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13146
+ * window. A failure count published without it is the mistake this schema
13147
+ * exists to make impossible.
13148
+ */
13149
+ attempts: number().int().nonnegative(),
13150
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13151
+ succeeded: number().int().nonnegative(),
13152
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13153
+ reasons: array(FailureReasonCountSchema).readonly()
13154
+ });
13155
+ method(_void(), array(FailureContributionSchema).readonly());
13156
+ /**
13049
13157
  * filesystem-browse — per-node capability for browsing the node's local
13050
13158
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13051
13159
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13567,6 +13675,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13567
13675
  kind: "mutation",
13568
13676
  auth: "admin"
13569
13677
  });
13678
+ var LoadContributionSchema = object({
13679
+ role: _enum([
13680
+ "decode",
13681
+ "transcode",
13682
+ "recording",
13683
+ "streaming",
13684
+ "detection"
13685
+ ]),
13686
+ /**
13687
+ * The NUMERIC device id — the same value every log line carries as
13688
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13689
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13690
+ * contributor that cannot name its camera must not emit the entry at all,
13691
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13692
+ * and would quietly turn one camera's cost into everybody's.
13693
+ */
13694
+ deviceId: number().int().positive().nullable(),
13695
+ attribution: _enum([
13696
+ "measured",
13697
+ "accounted",
13698
+ "unattributable"
13699
+ ]),
13700
+ /**
13701
+ * What ONE entry is, in the contributor's own words — `615/high`,
13702
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13703
+ * family and inventing a common one would lose the only information that
13704
+ * makes two entries for the same camera distinguishable.
13705
+ */
13706
+ unit: string(),
13707
+ /**
13708
+ * The OS process this cost lives in, when there is one. Present so a
13709
+ * consumer can (a) tell two generations of the same unit apart across a
13710
+ * restart, and (b) subtract claimed processes from the node's process
13711
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13712
+ * process of its own.
13713
+ */
13714
+ pid: number().int().positive().optional(),
13715
+ /**
13716
+ * When this generation started. The pid's incarnation marker: a consumer
13717
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13718
+ * window when this changes, because the counter restarted from zero in a new
13719
+ * process.
13720
+ */
13721
+ startedAtMs: number().optional(),
13722
+ /**
13723
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13724
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13725
+ * contribution is asked for.
13726
+ *
13727
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13728
+ * needs a sampler, and a new per-node sampler is the defect half of
13729
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13730
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13731
+ *
13732
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13733
+ * an entry with no process.
13734
+ */
13735
+ cpuSeconds: number().optional(),
13736
+ /** Resident bytes of this unit's process, same source and same rules. */
13737
+ rssBytes: number().optional()
13738
+ });
13739
+ method(_void(), array(LoadContributionSchema).readonly());
13570
13740
  /**
13571
13741
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13572
13742
  * through. It stores nothing.
@@ -13643,176 +13813,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13643
13813
  tags: record(string(), string()).optional()
13644
13814
  }), array(LogEntrySchema).readonly());
13645
13815
  /**
13646
- * `failure-contribution` — the capability an addon reports its OWN losses
13647
- * through, per camera, with the denominator attached. It stores nothing.
13648
- *
13649
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13650
- *
13651
- * `load-contribution` answers *what did this camera COST*. This answers *what
13652
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13653
- * copied: the contributor reports what it already knows, hub-main adds only
13654
- * `addonId`, nothing needs global knowledge, and there is no central list for
13655
- * somebody to forget to edit.
13656
- *
13657
- * They are not merged, because their invariants are opposites:
13658
- *
13659
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13660
- * claim a camera cost nothing, which is a measurement nobody made;
13661
- * - a `failure-contribution` zero is the **most valuable value on the
13662
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13663
- * and it is exactly what an absent entry cannot say.
13664
- *
13665
- * Putting a loss counter on a cost entry would also break the reconciliation
13666
- * that gives `load-contribution` its point: contributions are subtracted from
13667
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13668
- * has no process.
13669
- *
13670
- * ## Why not a log line, since the counters already exist
13671
- *
13672
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13673
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13674
- * ends in a log line, and a log line is the thing the operator asked to stop
13675
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13676
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13677
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13678
- * media blackout were both diagnosed. The counters stay; this is where they can
13679
- * be READ.
13680
- *
13681
- * ## The rate is served with its denominator or not at all
13682
- *
13683
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13684
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13685
- * than yesterday" and was **flat across twelve hours** once divided by the
13686
- * successes on the same path. A surface that publishes only the numerator
13687
- * reproduces that mistake on every read.
13688
- *
13689
- * ## Shape
13690
- *
13691
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13692
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13693
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13694
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13695
- * a forked runner's entries reach hub-main over transport that already exists.
13696
- * No new UDS message, no second registry (D3). The operator reads the assembled
13697
- * result through `system.getFailureContributions`.
13698
- */
13699
- var FailureReasonCountSchema = object({
13700
- /**
13701
- * Why the attempt did not land, in the contributor's own vocabulary —
13702
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13703
- * strings that already appear in this repo's logs and, where one exists, the
13704
- * same string the per-track `previewMissReason` records (D276): a second
13705
- * vocabulary for the same loss would make the row and the counter
13706
- * un-joinable.
13707
- */
13708
- reason: string(),
13709
- count: number().int().nonnegative()
13710
- });
13711
- var FailureContributionSchema = object({
13712
- /**
13713
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13714
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13715
- * `unit` free: the families are owned by different addons and a shared enum
13716
- * is a central list that rots invisibly.
13717
- */
13718
- family: string(),
13719
- /**
13720
- * The NUMERIC device id — the same value every log line carries as
13721
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13722
- * cannot name the camera must not emit the entry, because a fleet total
13723
- * cannot answer the only question anybody asks of this surface.
13724
- */
13725
- deviceId: number().int().positive(),
13726
- /**
13727
- * A second dimension inside the family: the model / step id for an inference
13728
- * timeout, so "which camera AND which model" is one read. Absent when the
13729
- * family has a single variant.
13730
- */
13731
- variant: string().optional(),
13732
- /**
13733
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13734
- * differencing two reads must drop the interval when it changes, because the
13735
- * counter restarted from zero in a respawned runner. Same discipline as
13736
- * `LoadContribution.startedAtMs`.
13737
- */
13738
- sinceMs: number(),
13739
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13740
- atMs: number(),
13741
- /**
13742
- * THE DENOMINATOR — every attempt on this path for this camera in the
13743
- * window. A failure count published without it is the mistake this schema
13744
- * exists to make impossible.
13745
- */
13746
- attempts: number().int().nonnegative(),
13747
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13748
- succeeded: number().int().nonnegative(),
13749
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13750
- reasons: array(FailureReasonCountSchema).readonly()
13751
- });
13752
- method(_void(), array(FailureContributionSchema).readonly());
13753
- var LoadContributionSchema = object({
13754
- role: _enum([
13755
- "decode",
13756
- "transcode",
13757
- "recording",
13758
- "streaming",
13759
- "detection"
13760
- ]),
13761
- /**
13762
- * The NUMERIC device id — the same value every log line carries as
13763
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13764
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13765
- * contributor that cannot name its camera must not emit the entry at all,
13766
- * because an unnamed per-camera entry is indistinguishable from a shared one
13767
- * and would quietly turn one camera's cost into everybody's.
13768
- */
13769
- deviceId: number().int().positive().nullable(),
13770
- attribution: _enum([
13771
- "measured",
13772
- "accounted",
13773
- "unattributable"
13774
- ]),
13775
- /**
13776
- * What ONE entry is, in the contributor's own words — `615/high`,
13777
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13778
- * family and inventing a common one would lose the only information that
13779
- * makes two entries for the same camera distinguishable.
13780
- */
13781
- unit: string(),
13782
- /**
13783
- * The OS process this cost lives in, when there is one. Present so a
13784
- * consumer can (a) tell two generations of the same unit apart across a
13785
- * restart, and (b) subtract claimed processes from the node's process
13786
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13787
- * process of its own.
13788
- */
13789
- pid: number().int().positive().optional(),
13790
- /**
13791
- * When this generation started. The pid's incarnation marker: a consumer
13792
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13793
- * window when this changes, because the counter restarted from zero in a new
13794
- * process.
13795
- */
13796
- startedAtMs: number().optional(),
13797
- /**
13798
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13799
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13800
- * contribution is asked for.
13801
- *
13802
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13803
- * needs a sampler, and a new per-node sampler is the defect half of
13804
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13805
- * by whoever already keeps a history; a rate cannot be un-averaged.
13806
- *
13807
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13808
- * an entry with no process.
13809
- */
13810
- cpuSeconds: number().optional(),
13811
- /** Resident bytes of this unit's process, same source and same rules. */
13812
- rssBytes: number().optional()
13813
- });
13814
- method(_void(), array(LoadContributionSchema).readonly());
13815
- /**
13816
13816
  * `login-method` — collection cap through which auth addons contribute
13817
13817
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13818
13818
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18472,12 +18472,53 @@ var MediaFileKindEnum = _enum([
18472
18472
  "keyFrameSmall",
18473
18473
  "thumbnailSmall"
18474
18474
  ]);
18475
+ /**
18476
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18477
+ * ARE — never the bytes themselves.
18478
+ *
18479
+ * ## Why `url` and not `base64`
18480
+ *
18481
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18482
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18483
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18484
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18485
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18486
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18487
+ *
18488
+ * `url` points at the `event-media` data plane
18489
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18490
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18491
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18492
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18493
+ * no less protected than they were inside a `view`-level cap response — see
18494
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18495
+ * (per-device scoping).
18496
+ *
18497
+ * The URL is built from the row's **stored** key, which is not always its
18498
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18499
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18500
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18501
+ *
18502
+ * ## `base64` is TRANSITIONAL and is going away
18503
+ *
18504
+ * It is still populated for one reason: the deployed viewer's track-detail
18505
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18506
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18507
+ * triangle — not as absence. Removing the field before that viewer ships is an
18508
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18509
+ * delete this line and the `withBytes` pass-through in
18510
+ * `analytics-query-facade.ts`; nothing else reads it.
18511
+ */
18475
18512
  var MediaFileSchema = object({
18476
18513
  key: string(),
18477
18514
  kind: MediaFileKindEnum,
18478
- base64: string(),
18479
18515
  sizeBytes: number(),
18480
18516
  timestamp: number()
18517
+ }).extend({
18518
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18519
+ url: string(),
18520
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18521
+ base64: string()
18481
18522
  });
18482
18523
  /**
18483
18524
  * One media row WITHOUT its bytes.
@@ -18489,7 +18530,9 @@ var MediaFileSchema = object({
18489
18530
  * blocks the whole view.
18490
18531
  *
18491
18532
  * `sizeBytes` is carried because it is what lets a client decide between the
18492
- * stored blob and a `?variant=thumb` rendering without fetching either.
18533
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18534
+ * `url` because a client that had to build the plane path itself is a second
18535
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18493
18536
  */
18494
18537
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18495
18538
  /**
@@ -18836,6 +18879,50 @@ var EventStoreFootprintSchema = object({
18836
18879
  totalBytes: number().int(),
18837
18880
  devices: array(EventStoreDeviceFootprintSchema).readonly()
18838
18881
  });
18882
+ /** Event-media footprint for one {@link MediaFileKind}. */
18883
+ var EventMediaKindFootprintSchema = object({
18884
+ kind: MediaFileKindEnum,
18885
+ /** Media rows of this kind. */
18886
+ rows: number().int(),
18887
+ /** Bytes on disk held by those rows. */
18888
+ bytes: number().int()
18889
+ });
18890
+ /**
18891
+ * The media footprint broken down by KIND — the axis a deletion decision
18892
+ * actually turns on.
18893
+ *
18894
+ * A byte total says how much there is; it cannot say what is safe to remove.
18895
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
18896
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
18897
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
18898
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
18899
+ * nothing else, so sizing a deletion means summing per kind.
18900
+ *
18901
+ * ## Why `unaccounted*` exists
18902
+ *
18903
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
18904
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
18905
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
18906
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
18907
+ * retired code path, or by a version that knew a kind this one does not) would
18908
+ * otherwise vanish from the total silently, and an operator would delete
18909
+ * against a denominator smaller than the disk.
18910
+ *
18911
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
18912
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
18913
+ */
18914
+ var EventMediaKindBreakdownSchema = object({
18915
+ /** Every media row in scope, from one unfiltered aggregate. */
18916
+ totalRows: number().int(),
18917
+ /** Every media byte in scope, from that same aggregate. */
18918
+ totalBytes: number().int(),
18919
+ /** Per-kind footprint, ordered by bytes descending. */
18920
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
18921
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
18922
+ unaccountedRows: number().int(),
18923
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
18924
+ unaccountedBytes: number().int()
18925
+ });
18839
18926
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
18840
18927
  var EventPruneCountsSchema = object({
18841
18928
  motion: number().int(),
@@ -19039,6 +19126,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19039
19126
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19040
19127
  kind: "query",
19041
19128
  auth: "admin"
19129
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19130
+ kind: "query",
19131
+ auth: "admin"
19042
19132
  }), method(object({
19043
19133
  olderThanMs: number(),
19044
19134
  reason: OpsLogReasonSchema.optional()
@@ -19178,6 +19268,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19178
19268
  }), array(MediaFileSchema).readonly()), method(object({
19179
19269
  trackId: string(),
19180
19270
  deviceId: number()
19271
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19272
+ eventId: string(),
19273
+ deviceId: number()
19181
19274
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19182
19275
  kind: "mutation",
19183
19276
  auth: "admin"
@@ -20987,6 +21080,20 @@ method(object({
20987
21080
  error: string().optional()
20988
21081
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
20989
21082
  providerId: string(),
21083
+ /**
21084
+ * The location this config is an UNSAVED edit of, when there is one.
21085
+ *
21086
+ * `listLocations` replaces every declared secret with the redaction
21087
+ * sentinel, so the edit modal's form state holds the sentinel for any
21088
+ * credential the operator did not retype — and posting that here
21089
+ * without a way to resolve it makes the provider try to authenticate
21090
+ * as `__camstack_redacted__` and report the operator's own working
21091
+ * password as wrong. Given this id, the orchestrator restores each
21092
+ * sentinel from the stored config (same rule as `upsertLocation`)
21093
+ * before dispatching. Omitted by the "Add location" wizard, where
21094
+ * every value was typed just now and nothing is stored yet.
21095
+ */
21096
+ locationId: string().optional(),
20990
21097
  config: record(string(), unknown())
20991
21098
  }), object({
20992
21099
  ok: boolean(),
@@ -23438,10 +23545,24 @@ var FaceClusterSchema = object({
23438
23545
  size: number().int(),
23439
23546
  cohesion: number()
23440
23547
  });
23548
+ /**
23549
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23550
+ * are — never the bytes.
23551
+ *
23552
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23553
+ * track/event contract) is still populated because a deployed viewer requires
23554
+ * the field to parse a row at all; this method has no such reader. Its ONE
23555
+ * caller is the admin UI's detail modal, which was building
23556
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23557
+ * dialog already rendering its key FRAME from the `event-media` plane.
23558
+ *
23559
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23560
+ * media key directly, so this needed no new plane and no new access decision.
23561
+ */
23441
23562
  var MediaFileLiteSchema$1 = object({
23442
23563
  key: string(),
23443
23564
  kind: string(),
23444
- base64: string(),
23565
+ url: string(),
23445
23566
  sizeBytes: number(),
23446
23567
  timestamp: number()
23447
23568
  });
@@ -25693,10 +25814,24 @@ var PlateInfoSchema = object({
25693
25814
  */
25694
25815
  cropUrl: string().optional()
25695
25816
  });
25817
+ /**
25818
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25819
+ * are — never the bytes.
25820
+ *
25821
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25822
+ * track/event contract) is still populated because a deployed viewer requires
25823
+ * the field to parse a row at all; this method has no such reader. Its ONE
25824
+ * caller is the admin UI's detail modal, which was building
25825
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25826
+ * dialog already rendering its key FRAME from the `event-media` plane.
25827
+ *
25828
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25829
+ * media key directly, so this needed no new plane and no new access decision.
25830
+ */
25696
25831
  var MediaFileLiteSchema = object({
25697
25832
  key: string(),
25698
25833
  kind: string(),
25699
- base64: string(),
25834
+ url: string(),
25700
25835
  sizeBytes: number(),
25701
25836
  timestamp: number()
25702
25837
  });
@@ -31613,6 +31748,12 @@ Object.freeze({
31613
31748
  addonId: null,
31614
31749
  access: "view"
31615
31750
  },
31751
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
31752
+ capName: "pipeline-analytics",
31753
+ capScope: "device",
31754
+ addonId: null,
31755
+ access: "view"
31756
+ },
31616
31757
  "pipelineAnalytics.getEventStoreFootprint": {
31617
31758
  capName: "pipeline-analytics",
31618
31759
  capScope: "device",
@@ -31709,6 +31850,12 @@ Object.freeze({
31709
31850
  addonId: null,
31710
31851
  access: "view"
31711
31852
  },
31853
+ "pipelineAnalytics.listEventMedia": {
31854
+ capName: "pipeline-analytics",
31855
+ capScope: "device",
31856
+ addonId: null,
31857
+ access: "view"
31858
+ },
31712
31859
  "pipelineAnalytics.listGroups": {
31713
31860
  capName: "pipeline-analytics",
31714
31861
  capScope: "device",
@@ -35272,6 +35419,11 @@ Object.freeze({
35272
35419
  form: "single",
35273
35420
  optional: false
35274
35421
  }],
35422
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
35423
+ name: "deviceId",
35424
+ form: "single",
35425
+ optional: true
35426
+ }],
35275
35427
  "pipelineAnalytics.getGroup": [{
35276
35428
  name: "deviceId",
35277
35429
  form: "single",
@@ -35332,6 +35484,11 @@ Object.freeze({
35332
35484
  form: "array",
35333
35485
  optional: false
35334
35486
  }],
35487
+ "pipelineAnalytics.listEventMedia": [{
35488
+ name: "deviceId",
35489
+ form: "single",
35490
+ optional: false
35491
+ }],
35335
35492
  "pipelineAnalytics.listGroups": [{
35336
35493
  name: "deviceIds",
35337
35494
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-decoder-nodeav",
3
- "version": "1.2.46",
3
+ "version": "1.2.48",
4
4
  "description": "Standalone in-process node-av decoder addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",