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