@camstack/addon-provider-velux 0.2.45 → 0.2.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +313 -181
  2. package/dist/addon.mjs +313 -181
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -8003,6 +8003,21 @@ var RelocateJobSchema = object({
8003
8003
  bytesMoved: number().int(),
8004
8004
  /** Total files discovered up front; null while (or when) unknown. */
8005
8005
  filesTotal: number().int().nullable(),
8006
+ /**
8007
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8008
+ * made that nobody asked for, so it is reported where the operator reads the
8009
+ * job rather than only in a log line.
8010
+ *
8011
+ * A footage segment records its byte count in its own NAME, and the durable
8012
+ * hour row derives its aggregates from those names. A file that does not
8013
+ * match its name therefore makes the ledger's sums — and with them quota and
8014
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8015
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8016
+ *
8017
+ * Absent on lanes where the question has no meaning: a media blob's size is
8018
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8019
+ */
8020
+ rowsReconciled: number().int().nonnegative().optional(),
8006
8021
  startedAt: number(),
8007
8022
  finishedAt: number().nullable(),
8008
8023
  error: string().nullable()
@@ -8071,14 +8086,42 @@ var RelocateMediaInputSchema = object({
8071
8086
  /** Omitted = `move`, the pre-existing behaviour. */
8072
8087
  mode: MediaRelocateModeSchema.optional()
8073
8088
  });
8074
- /** How many rows still carry NO `locationId` — the population a repoint would
8075
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8076
- * value that permits a non-blocking `eventMedia` cutover. */
8077
- var UnstampedEventMediaCountSchema = object({
8078
- media: number().int().nonnegative(),
8079
- retrainFrames: number().int().nonnegative(),
8080
- total: number().int().nonnegative()
8089
+ /**
8090
+ * The unstamped population of ONE collection — split, because the gate and the
8091
+ * operator ask two different questions and only one of them has to be cheap.
8092
+ *
8093
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8094
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8095
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8096
+ * that matters — after a seal, when the population is empty.
8097
+ *
8098
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8099
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8100
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8101
+ * and useful answer: "there are some, and this read could not say how many"
8102
+ * still refuses the cutover, which is the whole job.
8103
+ */
8104
+ var UnstampedRowsSchema = object({
8105
+ present: boolean(),
8106
+ rows: number().int().nonnegative().nullable()
8081
8107
  });
8108
+ /**
8109
+ * How many rows still carry NO `locationId` — the population a repoint would
8110
+ * silently re-aim at a disk that does not hold their bytes.
8111
+ *
8112
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8113
+ * over. The gate opens on a measured absence and on nothing else; an unread
8114
+ * collection and an empty one are different facts, and this repo has already
8115
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8116
+ */
8117
+ var UnstampedEventMediaCountSchema = object({
8118
+ media: UnstampedRowsSchema,
8119
+ retrainFrames: UnstampedRowsSchema,
8120
+ /** True when EITHER collection holds one. The refusal reads this. */
8121
+ anyPresent: boolean(),
8122
+ /** Sum across both, or `null` when either lane could not be counted. */
8123
+ total: number().int().nonnegative().nullable()
8124
+ }).nullable();
8082
8125
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8083
8126
  /** The independently selectable logical storage classes — every class
8084
8127
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8189,6 +8232,10 @@ var StorageMigrationMoveProgressSchema = object({
8189
8232
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8190
8233
  filesTotal: number().int().nonnegative().nullable(),
8191
8234
  bytesMoved: number().int().nonnegative(),
8235
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8236
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8237
+ * rows would be the same failure as one that silently skipped them. */
8238
+ rowsReconciled: number().int().nonnegative().optional(),
8192
8239
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8193
8240
  * crash gets a new mover, and a rate computed from the migration's start
8194
8241
  * would silently average in the time nothing was running. */
@@ -13137,6 +13184,114 @@ method(object({
13137
13184
  height: number()
13138
13185
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13139
13186
  /**
13187
+ * `failure-contribution` — the capability an addon reports its OWN losses
13188
+ * through, per camera, with the denominator attached. It stores nothing.
13189
+ *
13190
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13191
+ *
13192
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13193
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13194
+ * copied: the contributor reports what it already knows, hub-main adds only
13195
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13196
+ * somebody to forget to edit.
13197
+ *
13198
+ * They are not merged, because their invariants are opposites:
13199
+ *
13200
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13201
+ * claim a camera cost nothing, which is a measurement nobody made;
13202
+ * - a `failure-contribution` zero is the **most valuable value on the
13203
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13204
+ * and it is exactly what an absent entry cannot say.
13205
+ *
13206
+ * Putting a loss counter on a cost entry would also break the reconciliation
13207
+ * that gives `load-contribution` its point: contributions are subtracted from
13208
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13209
+ * has no process.
13210
+ *
13211
+ * ## Why not a log line, since the counters already exist
13212
+ *
13213
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13214
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13215
+ * ends in a log line, and a log line is the thing the operator asked to stop
13216
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13217
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13218
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13219
+ * media blackout were both diagnosed. The counters stay; this is where they can
13220
+ * be READ.
13221
+ *
13222
+ * ## The rate is served with its denominator or not at all
13223
+ *
13224
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13225
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13226
+ * than yesterday" and was **flat across twelve hours** once divided by the
13227
+ * successes on the same path. A surface that publishes only the numerator
13228
+ * reproduces that mistake on every read.
13229
+ *
13230
+ * ## Shape
13231
+ *
13232
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13233
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13234
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13235
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13236
+ * a forked runner's entries reach hub-main over transport that already exists.
13237
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13238
+ * result through `system.getFailureContributions`.
13239
+ */
13240
+ var FailureReasonCountSchema = object({
13241
+ /**
13242
+ * Why the attempt did not land, in the contributor's own vocabulary —
13243
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13244
+ * strings that already appear in this repo's logs and, where one exists, the
13245
+ * same string the per-track `previewMissReason` records (D276): a second
13246
+ * vocabulary for the same loss would make the row and the counter
13247
+ * un-joinable.
13248
+ */
13249
+ reason: string(),
13250
+ count: number().int().nonnegative()
13251
+ });
13252
+ var FailureContributionSchema = object({
13253
+ /**
13254
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13255
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13256
+ * `unit` free: the families are owned by different addons and a shared enum
13257
+ * is a central list that rots invisibly.
13258
+ */
13259
+ family: string(),
13260
+ /**
13261
+ * The NUMERIC device id — the same value every log line carries as
13262
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13263
+ * cannot name the camera must not emit the entry, because a fleet total
13264
+ * cannot answer the only question anybody asks of this surface.
13265
+ */
13266
+ deviceId: number().int().positive(),
13267
+ /**
13268
+ * A second dimension inside the family: the model / step id for an inference
13269
+ * timeout, so "which camera AND which model" is one read. Absent when the
13270
+ * family has a single variant.
13271
+ */
13272
+ variant: string().optional(),
13273
+ /**
13274
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13275
+ * differencing two reads must drop the interval when it changes, because the
13276
+ * counter restarted from zero in a respawned runner. Same discipline as
13277
+ * `LoadContribution.startedAtMs`.
13278
+ */
13279
+ sinceMs: number(),
13280
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13281
+ atMs: number(),
13282
+ /**
13283
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13284
+ * window. A failure count published without it is the mistake this schema
13285
+ * exists to make impossible.
13286
+ */
13287
+ attempts: number().int().nonnegative(),
13288
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13289
+ succeeded: number().int().nonnegative(),
13290
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13291
+ reasons: array(FailureReasonCountSchema).readonly()
13292
+ });
13293
+ method(_void(), array(FailureContributionSchema).readonly());
13294
+ /**
13140
13295
  * filesystem-browse — per-node capability for browsing the node's local
13141
13296
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13142
13297
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13658,6 +13813,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13658
13813
  kind: "mutation",
13659
13814
  auth: "admin"
13660
13815
  });
13816
+ var LoadContributionSchema = object({
13817
+ role: _enum([
13818
+ "decode",
13819
+ "transcode",
13820
+ "recording",
13821
+ "streaming",
13822
+ "detection"
13823
+ ]),
13824
+ /**
13825
+ * The NUMERIC device id — the same value every log line carries as
13826
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13827
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13828
+ * contributor that cannot name its camera must not emit the entry at all,
13829
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13830
+ * and would quietly turn one camera's cost into everybody's.
13831
+ */
13832
+ deviceId: number().int().positive().nullable(),
13833
+ attribution: _enum([
13834
+ "measured",
13835
+ "accounted",
13836
+ "unattributable"
13837
+ ]),
13838
+ /**
13839
+ * What ONE entry is, in the contributor's own words — `615/high`,
13840
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13841
+ * family and inventing a common one would lose the only information that
13842
+ * makes two entries for the same camera distinguishable.
13843
+ */
13844
+ unit: string(),
13845
+ /**
13846
+ * The OS process this cost lives in, when there is one. Present so a
13847
+ * consumer can (a) tell two generations of the same unit apart across a
13848
+ * restart, and (b) subtract claimed processes from the node's process
13849
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13850
+ * process of its own.
13851
+ */
13852
+ pid: number().int().positive().optional(),
13853
+ /**
13854
+ * When this generation started. The pid's incarnation marker: a consumer
13855
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13856
+ * window when this changes, because the counter restarted from zero in a new
13857
+ * process.
13858
+ */
13859
+ startedAtMs: number().optional(),
13860
+ /**
13861
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13862
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13863
+ * contribution is asked for.
13864
+ *
13865
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13866
+ * needs a sampler, and a new per-node sampler is the defect half of
13867
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13868
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13869
+ *
13870
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13871
+ * an entry with no process.
13872
+ */
13873
+ cpuSeconds: number().optional(),
13874
+ /** Resident bytes of this unit's process, same source and same rules. */
13875
+ rssBytes: number().optional()
13876
+ });
13877
+ method(_void(), array(LoadContributionSchema).readonly());
13661
13878
  /**
13662
13879
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13663
13880
  * through. It stores nothing.
@@ -13734,176 +13951,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13734
13951
  tags: record(string(), string()).optional()
13735
13952
  }), array(LogEntrySchema).readonly());
13736
13953
  /**
13737
- * `failure-contribution` — the capability an addon reports its OWN losses
13738
- * through, per camera, with the denominator attached. It stores nothing.
13739
- *
13740
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13741
- *
13742
- * `load-contribution` answers *what did this camera COST*. This answers *what
13743
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13744
- * copied: the contributor reports what it already knows, hub-main adds only
13745
- * `addonId`, nothing needs global knowledge, and there is no central list for
13746
- * somebody to forget to edit.
13747
- *
13748
- * They are not merged, because their invariants are opposites:
13749
- *
13750
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13751
- * claim a camera cost nothing, which is a measurement nobody made;
13752
- * - a `failure-contribution` zero is the **most valuable value on the
13753
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13754
- * and it is exactly what an absent entry cannot say.
13755
- *
13756
- * Putting a loss counter on a cost entry would also break the reconciliation
13757
- * that gives `load-contribution` its point: contributions are subtracted from
13758
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13759
- * has no process.
13760
- *
13761
- * ## Why not a log line, since the counters already exist
13762
- *
13763
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13764
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13765
- * ends in a log line, and a log line is the thing the operator asked to stop
13766
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13767
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13768
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13769
- * media blackout were both diagnosed. The counters stay; this is where they can
13770
- * be READ.
13771
- *
13772
- * ## The rate is served with its denominator or not at all
13773
- *
13774
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13775
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13776
- * than yesterday" and was **flat across twelve hours** once divided by the
13777
- * successes on the same path. A surface that publishes only the numerator
13778
- * reproduces that mistake on every read.
13779
- *
13780
- * ## Shape
13781
- *
13782
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13783
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13784
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13785
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13786
- * a forked runner's entries reach hub-main over transport that already exists.
13787
- * No new UDS message, no second registry (D3). The operator reads the assembled
13788
- * result through `system.getFailureContributions`.
13789
- */
13790
- var FailureReasonCountSchema = object({
13791
- /**
13792
- * Why the attempt did not land, in the contributor's own vocabulary —
13793
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13794
- * strings that already appear in this repo's logs and, where one exists, the
13795
- * same string the per-track `previewMissReason` records (D276): a second
13796
- * vocabulary for the same loss would make the row and the counter
13797
- * un-joinable.
13798
- */
13799
- reason: string(),
13800
- count: number().int().nonnegative()
13801
- });
13802
- var FailureContributionSchema = object({
13803
- /**
13804
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13805
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13806
- * `unit` free: the families are owned by different addons and a shared enum
13807
- * is a central list that rots invisibly.
13808
- */
13809
- family: string(),
13810
- /**
13811
- * The NUMERIC device id — the same value every log line carries as
13812
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13813
- * cannot name the camera must not emit the entry, because a fleet total
13814
- * cannot answer the only question anybody asks of this surface.
13815
- */
13816
- deviceId: number().int().positive(),
13817
- /**
13818
- * A second dimension inside the family: the model / step id for an inference
13819
- * timeout, so "which camera AND which model" is one read. Absent when the
13820
- * family has a single variant.
13821
- */
13822
- variant: string().optional(),
13823
- /**
13824
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13825
- * differencing two reads must drop the interval when it changes, because the
13826
- * counter restarted from zero in a respawned runner. Same discipline as
13827
- * `LoadContribution.startedAtMs`.
13828
- */
13829
- sinceMs: number(),
13830
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13831
- atMs: number(),
13832
- /**
13833
- * THE DENOMINATOR — every attempt on this path for this camera in the
13834
- * window. A failure count published without it is the mistake this schema
13835
- * exists to make impossible.
13836
- */
13837
- attempts: number().int().nonnegative(),
13838
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13839
- succeeded: number().int().nonnegative(),
13840
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13841
- reasons: array(FailureReasonCountSchema).readonly()
13842
- });
13843
- method(_void(), array(FailureContributionSchema).readonly());
13844
- var LoadContributionSchema = object({
13845
- role: _enum([
13846
- "decode",
13847
- "transcode",
13848
- "recording",
13849
- "streaming",
13850
- "detection"
13851
- ]),
13852
- /**
13853
- * The NUMERIC device id — the same value every log line carries as
13854
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13855
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13856
- * contributor that cannot name its camera must not emit the entry at all,
13857
- * because an unnamed per-camera entry is indistinguishable from a shared one
13858
- * and would quietly turn one camera's cost into everybody's.
13859
- */
13860
- deviceId: number().int().positive().nullable(),
13861
- attribution: _enum([
13862
- "measured",
13863
- "accounted",
13864
- "unattributable"
13865
- ]),
13866
- /**
13867
- * What ONE entry is, in the contributor's own words — `615/high`,
13868
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13869
- * family and inventing a common one would lose the only information that
13870
- * makes two entries for the same camera distinguishable.
13871
- */
13872
- unit: string(),
13873
- /**
13874
- * The OS process this cost lives in, when there is one. Present so a
13875
- * consumer can (a) tell two generations of the same unit apart across a
13876
- * restart, and (b) subtract claimed processes from the node's process
13877
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13878
- * process of its own.
13879
- */
13880
- pid: number().int().positive().optional(),
13881
- /**
13882
- * When this generation started. The pid's incarnation marker: a consumer
13883
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13884
- * window when this changes, because the counter restarted from zero in a new
13885
- * process.
13886
- */
13887
- startedAtMs: number().optional(),
13888
- /**
13889
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13890
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13891
- * contribution is asked for.
13892
- *
13893
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13894
- * needs a sampler, and a new per-node sampler is the defect half of
13895
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13896
- * by whoever already keeps a history; a rate cannot be un-averaged.
13897
- *
13898
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13899
- * an entry with no process.
13900
- */
13901
- cpuSeconds: number().optional(),
13902
- /** Resident bytes of this unit's process, same source and same rules. */
13903
- rssBytes: number().optional()
13904
- });
13905
- method(_void(), array(LoadContributionSchema).readonly());
13906
- /**
13907
13954
  * `login-method` — collection cap through which auth addons contribute
13908
13955
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13909
13956
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18641,12 +18688,53 @@ var MediaFileKindEnum = _enum([
18641
18688
  "keyFrameSmall",
18642
18689
  "thumbnailSmall"
18643
18690
  ]);
18691
+ /**
18692
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18693
+ * ARE — never the bytes themselves.
18694
+ *
18695
+ * ## Why `url` and not `base64`
18696
+ *
18697
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18698
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18699
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18700
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18701
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18702
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18703
+ *
18704
+ * `url` points at the `event-media` data plane
18705
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18706
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18707
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18708
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18709
+ * no less protected than they were inside a `view`-level cap response — see
18710
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18711
+ * (per-device scoping).
18712
+ *
18713
+ * The URL is built from the row's **stored** key, which is not always its
18714
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18715
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18716
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18717
+ *
18718
+ * ## `base64` is TRANSITIONAL and is going away
18719
+ *
18720
+ * It is still populated for one reason: the deployed viewer's track-detail
18721
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18722
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18723
+ * triangle — not as absence. Removing the field before that viewer ships is an
18724
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18725
+ * delete this line and the `withBytes` pass-through in
18726
+ * `analytics-query-facade.ts`; nothing else reads it.
18727
+ */
18644
18728
  var MediaFileSchema = object({
18645
18729
  key: string(),
18646
18730
  kind: MediaFileKindEnum,
18647
- base64: string(),
18648
18731
  sizeBytes: number(),
18649
18732
  timestamp: number()
18733
+ }).extend({
18734
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18735
+ url: string(),
18736
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18737
+ base64: string()
18650
18738
  });
18651
18739
  /**
18652
18740
  * One media row WITHOUT its bytes.
@@ -18658,7 +18746,9 @@ var MediaFileSchema = object({
18658
18746
  * blocks the whole view.
18659
18747
  *
18660
18748
  * `sizeBytes` is carried because it is what lets a client decide between the
18661
- * stored blob and a `?variant=thumb` rendering without fetching either.
18749
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18750
+ * `url` because a client that had to build the plane path itself is a second
18751
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18662
18752
  */
18663
18753
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18664
18754
  /**
@@ -19347,6 +19437,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19347
19437
  }), array(MediaFileSchema).readonly()), method(object({
19348
19438
  trackId: string(),
19349
19439
  deviceId: number()
19440
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19441
+ eventId: string(),
19442
+ deviceId: number()
19350
19443
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19351
19444
  kind: "mutation",
19352
19445
  auth: "admin"
@@ -24337,10 +24430,24 @@ var FaceClusterSchema = object({
24337
24430
  size: number().int(),
24338
24431
  cohesion: number()
24339
24432
  });
24433
+ /**
24434
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24435
+ * are — never the bytes.
24436
+ *
24437
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24438
+ * track/event contract) is still populated because a deployed viewer requires
24439
+ * the field to parse a row at all; this method has no such reader. Its ONE
24440
+ * caller is the admin UI's detail modal, which was building
24441
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24442
+ * dialog already rendering its key FRAME from the `event-media` plane.
24443
+ *
24444
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24445
+ * media key directly, so this needed no new plane and no new access decision.
24446
+ */
24340
24447
  var MediaFileLiteSchema$1 = object({
24341
24448
  key: string(),
24342
24449
  kind: string(),
24343
- base64: string(),
24450
+ url: string(),
24344
24451
  sizeBytes: number(),
24345
24452
  timestamp: number()
24346
24453
  });
@@ -27362,10 +27469,24 @@ var PlateInfoSchema = object({
27362
27469
  */
27363
27470
  cropUrl: string().optional()
27364
27471
  });
27472
+ /**
27473
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27474
+ * are — never the bytes.
27475
+ *
27476
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27477
+ * track/event contract) is still populated because a deployed viewer requires
27478
+ * the field to parse a row at all; this method has no such reader. Its ONE
27479
+ * caller is the admin UI's detail modal, which was building
27480
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27481
+ * dialog already rendering its key FRAME from the `event-media` plane.
27482
+ *
27483
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27484
+ * media key directly, so this needed no new plane and no new access decision.
27485
+ */
27365
27486
  var MediaFileLiteSchema = object({
27366
27487
  key: string(),
27367
27488
  kind: string(),
27368
- base64: string(),
27489
+ url: string(),
27369
27490
  sizeBytes: number(),
27370
27491
  timestamp: number()
27371
27492
  });
@@ -35262,6 +35383,12 @@ Object.freeze({
35262
35383
  addonId: null,
35263
35384
  access: "view"
35264
35385
  },
35386
+ "pipelineAnalytics.listEventMedia": {
35387
+ capName: "pipeline-analytics",
35388
+ capScope: "device",
35389
+ addonId: null,
35390
+ access: "view"
35391
+ },
35265
35392
  "pipelineAnalytics.listGroups": {
35266
35393
  capName: "pipeline-analytics",
35267
35394
  capScope: "device",
@@ -38885,6 +39012,11 @@ Object.freeze({
38885
39012
  form: "array",
38886
39013
  optional: false
38887
39014
  }],
39015
+ "pipelineAnalytics.listEventMedia": [{
39016
+ name: "deviceId",
39017
+ form: "single",
39018
+ optional: false
39019
+ }],
38888
39020
  "pipelineAnalytics.listGroups": [{
38889
39021
  name: "deviceIds",
38890
39022
  form: "array",
package/dist/addon.mjs CHANGED
@@ -8002,6 +8002,21 @@ var RelocateJobSchema = object({
8002
8002
  bytesMoved: number().int(),
8003
8003
  /** Total files discovered up front; null while (or when) unknown. */
8004
8004
  filesTotal: number().int().nullable(),
8005
+ /**
8006
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8007
+ * made that nobody asked for, so it is reported where the operator reads the
8008
+ * job rather than only in a log line.
8009
+ *
8010
+ * A footage segment records its byte count in its own NAME, and the durable
8011
+ * hour row derives its aggregates from those names. A file that does not
8012
+ * match its name therefore makes the ledger's sums — and with them quota and
8013
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8014
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8015
+ *
8016
+ * Absent on lanes where the question has no meaning: a media blob's size is
8017
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8018
+ */
8019
+ rowsReconciled: number().int().nonnegative().optional(),
8005
8020
  startedAt: number(),
8006
8021
  finishedAt: number().nullable(),
8007
8022
  error: string().nullable()
@@ -8070,14 +8085,42 @@ var RelocateMediaInputSchema = object({
8070
8085
  /** Omitted = `move`, the pre-existing behaviour. */
8071
8086
  mode: MediaRelocateModeSchema.optional()
8072
8087
  });
8073
- /** How many rows still carry NO `locationId` — the population a repoint would
8074
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8075
- * value that permits a non-blocking `eventMedia` cutover. */
8076
- var UnstampedEventMediaCountSchema = object({
8077
- media: number().int().nonnegative(),
8078
- retrainFrames: number().int().nonnegative(),
8079
- total: number().int().nonnegative()
8088
+ /**
8089
+ * The unstamped population of ONE collection — split, because the gate and the
8090
+ * operator ask two different questions and only one of them has to be cheap.
8091
+ *
8092
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8093
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8094
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8095
+ * that matters — after a seal, when the population is empty.
8096
+ *
8097
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8098
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8099
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8100
+ * and useful answer: "there are some, and this read could not say how many"
8101
+ * still refuses the cutover, which is the whole job.
8102
+ */
8103
+ var UnstampedRowsSchema = object({
8104
+ present: boolean(),
8105
+ rows: number().int().nonnegative().nullable()
8080
8106
  });
8107
+ /**
8108
+ * How many rows still carry NO `locationId` — the population a repoint would
8109
+ * silently re-aim at a disk that does not hold their bytes.
8110
+ *
8111
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8112
+ * over. The gate opens on a measured absence and on nothing else; an unread
8113
+ * collection and an empty one are different facts, and this repo has already
8114
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8115
+ */
8116
+ var UnstampedEventMediaCountSchema = object({
8117
+ media: UnstampedRowsSchema,
8118
+ retrainFrames: UnstampedRowsSchema,
8119
+ /** True when EITHER collection holds one. The refusal reads this. */
8120
+ anyPresent: boolean(),
8121
+ /** Sum across both, or `null` when either lane could not be counted. */
8122
+ total: number().int().nonnegative().nullable()
8123
+ }).nullable();
8081
8124
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8082
8125
  /** The independently selectable logical storage classes — every class
8083
8126
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8188,6 +8231,10 @@ var StorageMigrationMoveProgressSchema = object({
8188
8231
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8189
8232
  filesTotal: number().int().nonnegative().nullable(),
8190
8233
  bytesMoved: number().int().nonnegative(),
8234
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8235
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8236
+ * rows would be the same failure as one that silently skipped them. */
8237
+ rowsReconciled: number().int().nonnegative().optional(),
8191
8238
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8192
8239
  * crash gets a new mover, and a rate computed from the migration's start
8193
8240
  * would silently average in the time nothing was running. */
@@ -13136,6 +13183,114 @@ method(object({
13136
13183
  height: number()
13137
13184
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13138
13185
  /**
13186
+ * `failure-contribution` — the capability an addon reports its OWN losses
13187
+ * through, per camera, with the denominator attached. It stores nothing.
13188
+ *
13189
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13190
+ *
13191
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13192
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13193
+ * copied: the contributor reports what it already knows, hub-main adds only
13194
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13195
+ * somebody to forget to edit.
13196
+ *
13197
+ * They are not merged, because their invariants are opposites:
13198
+ *
13199
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13200
+ * claim a camera cost nothing, which is a measurement nobody made;
13201
+ * - a `failure-contribution` zero is the **most valuable value on the
13202
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13203
+ * and it is exactly what an absent entry cannot say.
13204
+ *
13205
+ * Putting a loss counter on a cost entry would also break the reconciliation
13206
+ * that gives `load-contribution` its point: contributions are subtracted from
13207
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13208
+ * has no process.
13209
+ *
13210
+ * ## Why not a log line, since the counters already exist
13211
+ *
13212
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13213
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13214
+ * ends in a log line, and a log line is the thing the operator asked to stop
13215
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13216
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13217
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13218
+ * media blackout were both diagnosed. The counters stay; this is where they can
13219
+ * be READ.
13220
+ *
13221
+ * ## The rate is served with its denominator or not at all
13222
+ *
13223
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13224
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13225
+ * than yesterday" and was **flat across twelve hours** once divided by the
13226
+ * successes on the same path. A surface that publishes only the numerator
13227
+ * reproduces that mistake on every read.
13228
+ *
13229
+ * ## Shape
13230
+ *
13231
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13232
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13233
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13234
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13235
+ * a forked runner's entries reach hub-main over transport that already exists.
13236
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13237
+ * result through `system.getFailureContributions`.
13238
+ */
13239
+ var FailureReasonCountSchema = object({
13240
+ /**
13241
+ * Why the attempt did not land, in the contributor's own vocabulary —
13242
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13243
+ * strings that already appear in this repo's logs and, where one exists, the
13244
+ * same string the per-track `previewMissReason` records (D276): a second
13245
+ * vocabulary for the same loss would make the row and the counter
13246
+ * un-joinable.
13247
+ */
13248
+ reason: string(),
13249
+ count: number().int().nonnegative()
13250
+ });
13251
+ var FailureContributionSchema = object({
13252
+ /**
13253
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13254
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13255
+ * `unit` free: the families are owned by different addons and a shared enum
13256
+ * is a central list that rots invisibly.
13257
+ */
13258
+ family: string(),
13259
+ /**
13260
+ * The NUMERIC device id — the same value every log line carries as
13261
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13262
+ * cannot name the camera must not emit the entry, because a fleet total
13263
+ * cannot answer the only question anybody asks of this surface.
13264
+ */
13265
+ deviceId: number().int().positive(),
13266
+ /**
13267
+ * A second dimension inside the family: the model / step id for an inference
13268
+ * timeout, so "which camera AND which model" is one read. Absent when the
13269
+ * family has a single variant.
13270
+ */
13271
+ variant: string().optional(),
13272
+ /**
13273
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13274
+ * differencing two reads must drop the interval when it changes, because the
13275
+ * counter restarted from zero in a respawned runner. Same discipline as
13276
+ * `LoadContribution.startedAtMs`.
13277
+ */
13278
+ sinceMs: number(),
13279
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13280
+ atMs: number(),
13281
+ /**
13282
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13283
+ * window. A failure count published without it is the mistake this schema
13284
+ * exists to make impossible.
13285
+ */
13286
+ attempts: number().int().nonnegative(),
13287
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13288
+ succeeded: number().int().nonnegative(),
13289
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13290
+ reasons: array(FailureReasonCountSchema).readonly()
13291
+ });
13292
+ method(_void(), array(FailureContributionSchema).readonly());
13293
+ /**
13139
13294
  * filesystem-browse — per-node capability for browsing the node's local
13140
13295
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13141
13296
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13657,6 +13812,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13657
13812
  kind: "mutation",
13658
13813
  auth: "admin"
13659
13814
  });
13815
+ var LoadContributionSchema = object({
13816
+ role: _enum([
13817
+ "decode",
13818
+ "transcode",
13819
+ "recording",
13820
+ "streaming",
13821
+ "detection"
13822
+ ]),
13823
+ /**
13824
+ * The NUMERIC device id — the same value every log line carries as
13825
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13826
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13827
+ * contributor that cannot name its camera must not emit the entry at all,
13828
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13829
+ * and would quietly turn one camera's cost into everybody's.
13830
+ */
13831
+ deviceId: number().int().positive().nullable(),
13832
+ attribution: _enum([
13833
+ "measured",
13834
+ "accounted",
13835
+ "unattributable"
13836
+ ]),
13837
+ /**
13838
+ * What ONE entry is, in the contributor's own words — `615/high`,
13839
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13840
+ * family and inventing a common one would lose the only information that
13841
+ * makes two entries for the same camera distinguishable.
13842
+ */
13843
+ unit: string(),
13844
+ /**
13845
+ * The OS process this cost lives in, when there is one. Present so a
13846
+ * consumer can (a) tell two generations of the same unit apart across a
13847
+ * restart, and (b) subtract claimed processes from the node's process
13848
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13849
+ * process of its own.
13850
+ */
13851
+ pid: number().int().positive().optional(),
13852
+ /**
13853
+ * When this generation started. The pid's incarnation marker: a consumer
13854
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13855
+ * window when this changes, because the counter restarted from zero in a new
13856
+ * process.
13857
+ */
13858
+ startedAtMs: number().optional(),
13859
+ /**
13860
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13861
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13862
+ * contribution is asked for.
13863
+ *
13864
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13865
+ * needs a sampler, and a new per-node sampler is the defect half of
13866
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13867
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13868
+ *
13869
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13870
+ * an entry with no process.
13871
+ */
13872
+ cpuSeconds: number().optional(),
13873
+ /** Resident bytes of this unit's process, same source and same rules. */
13874
+ rssBytes: number().optional()
13875
+ });
13876
+ method(_void(), array(LoadContributionSchema).readonly());
13660
13877
  /**
13661
13878
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13662
13879
  * through. It stores nothing.
@@ -13733,176 +13950,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13733
13950
  tags: record(string(), string()).optional()
13734
13951
  }), array(LogEntrySchema).readonly());
13735
13952
  /**
13736
- * `failure-contribution` — the capability an addon reports its OWN losses
13737
- * through, per camera, with the denominator attached. It stores nothing.
13738
- *
13739
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13740
- *
13741
- * `load-contribution` answers *what did this camera COST*. This answers *what
13742
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13743
- * copied: the contributor reports what it already knows, hub-main adds only
13744
- * `addonId`, nothing needs global knowledge, and there is no central list for
13745
- * somebody to forget to edit.
13746
- *
13747
- * They are not merged, because their invariants are opposites:
13748
- *
13749
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13750
- * claim a camera cost nothing, which is a measurement nobody made;
13751
- * - a `failure-contribution` zero is the **most valuable value on the
13752
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13753
- * and it is exactly what an absent entry cannot say.
13754
- *
13755
- * Putting a loss counter on a cost entry would also break the reconciliation
13756
- * that gives `load-contribution` its point: contributions are subtracted from
13757
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13758
- * has no process.
13759
- *
13760
- * ## Why not a log line, since the counters already exist
13761
- *
13762
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13763
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13764
- * ends in a log line, and a log line is the thing the operator asked to stop
13765
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13766
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13767
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13768
- * media blackout were both diagnosed. The counters stay; this is where they can
13769
- * be READ.
13770
- *
13771
- * ## The rate is served with its denominator or not at all
13772
- *
13773
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13774
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13775
- * than yesterday" and was **flat across twelve hours** once divided by the
13776
- * successes on the same path. A surface that publishes only the numerator
13777
- * reproduces that mistake on every read.
13778
- *
13779
- * ## Shape
13780
- *
13781
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13782
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13783
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13784
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13785
- * a forked runner's entries reach hub-main over transport that already exists.
13786
- * No new UDS message, no second registry (D3). The operator reads the assembled
13787
- * result through `system.getFailureContributions`.
13788
- */
13789
- var FailureReasonCountSchema = object({
13790
- /**
13791
- * Why the attempt did not land, in the contributor's own vocabulary —
13792
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13793
- * strings that already appear in this repo's logs and, where one exists, the
13794
- * same string the per-track `previewMissReason` records (D276): a second
13795
- * vocabulary for the same loss would make the row and the counter
13796
- * un-joinable.
13797
- */
13798
- reason: string(),
13799
- count: number().int().nonnegative()
13800
- });
13801
- var FailureContributionSchema = object({
13802
- /**
13803
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13804
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13805
- * `unit` free: the families are owned by different addons and a shared enum
13806
- * is a central list that rots invisibly.
13807
- */
13808
- family: string(),
13809
- /**
13810
- * The NUMERIC device id — the same value every log line carries as
13811
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13812
- * cannot name the camera must not emit the entry, because a fleet total
13813
- * cannot answer the only question anybody asks of this surface.
13814
- */
13815
- deviceId: number().int().positive(),
13816
- /**
13817
- * A second dimension inside the family: the model / step id for an inference
13818
- * timeout, so "which camera AND which model" is one read. Absent when the
13819
- * family has a single variant.
13820
- */
13821
- variant: string().optional(),
13822
- /**
13823
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13824
- * differencing two reads must drop the interval when it changes, because the
13825
- * counter restarted from zero in a respawned runner. Same discipline as
13826
- * `LoadContribution.startedAtMs`.
13827
- */
13828
- sinceMs: number(),
13829
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13830
- atMs: number(),
13831
- /**
13832
- * THE DENOMINATOR — every attempt on this path for this camera in the
13833
- * window. A failure count published without it is the mistake this schema
13834
- * exists to make impossible.
13835
- */
13836
- attempts: number().int().nonnegative(),
13837
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13838
- succeeded: number().int().nonnegative(),
13839
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13840
- reasons: array(FailureReasonCountSchema).readonly()
13841
- });
13842
- method(_void(), array(FailureContributionSchema).readonly());
13843
- var LoadContributionSchema = object({
13844
- role: _enum([
13845
- "decode",
13846
- "transcode",
13847
- "recording",
13848
- "streaming",
13849
- "detection"
13850
- ]),
13851
- /**
13852
- * The NUMERIC device id — the same value every log line carries as
13853
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13854
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13855
- * contributor that cannot name its camera must not emit the entry at all,
13856
- * because an unnamed per-camera entry is indistinguishable from a shared one
13857
- * and would quietly turn one camera's cost into everybody's.
13858
- */
13859
- deviceId: number().int().positive().nullable(),
13860
- attribution: _enum([
13861
- "measured",
13862
- "accounted",
13863
- "unattributable"
13864
- ]),
13865
- /**
13866
- * What ONE entry is, in the contributor's own words — `615/high`,
13867
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13868
- * family and inventing a common one would lose the only information that
13869
- * makes two entries for the same camera distinguishable.
13870
- */
13871
- unit: string(),
13872
- /**
13873
- * The OS process this cost lives in, when there is one. Present so a
13874
- * consumer can (a) tell two generations of the same unit apart across a
13875
- * restart, and (b) subtract claimed processes from the node's process
13876
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13877
- * process of its own.
13878
- */
13879
- pid: number().int().positive().optional(),
13880
- /**
13881
- * When this generation started. The pid's incarnation marker: a consumer
13882
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13883
- * window when this changes, because the counter restarted from zero in a new
13884
- * process.
13885
- */
13886
- startedAtMs: number().optional(),
13887
- /**
13888
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13889
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13890
- * contribution is asked for.
13891
- *
13892
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13893
- * needs a sampler, and a new per-node sampler is the defect half of
13894
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13895
- * by whoever already keeps a history; a rate cannot be un-averaged.
13896
- *
13897
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13898
- * an entry with no process.
13899
- */
13900
- cpuSeconds: number().optional(),
13901
- /** Resident bytes of this unit's process, same source and same rules. */
13902
- rssBytes: number().optional()
13903
- });
13904
- method(_void(), array(LoadContributionSchema).readonly());
13905
- /**
13906
13953
  * `login-method` — collection cap through which auth addons contribute
13907
13954
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13908
13955
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18640,12 +18687,53 @@ var MediaFileKindEnum = _enum([
18640
18687
  "keyFrameSmall",
18641
18688
  "thumbnailSmall"
18642
18689
  ]);
18690
+ /**
18691
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18692
+ * ARE — never the bytes themselves.
18693
+ *
18694
+ * ## Why `url` and not `base64`
18695
+ *
18696
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18697
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18698
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18699
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18700
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18701
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18702
+ *
18703
+ * `url` points at the `event-media` data plane
18704
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18705
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18706
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18707
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18708
+ * no less protected than they were inside a `view`-level cap response — see
18709
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18710
+ * (per-device scoping).
18711
+ *
18712
+ * The URL is built from the row's **stored** key, which is not always its
18713
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18714
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18715
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18716
+ *
18717
+ * ## `base64` is TRANSITIONAL and is going away
18718
+ *
18719
+ * It is still populated for one reason: the deployed viewer's track-detail
18720
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18721
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18722
+ * triangle — not as absence. Removing the field before that viewer ships is an
18723
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18724
+ * delete this line and the `withBytes` pass-through in
18725
+ * `analytics-query-facade.ts`; nothing else reads it.
18726
+ */
18643
18727
  var MediaFileSchema = object({
18644
18728
  key: string(),
18645
18729
  kind: MediaFileKindEnum,
18646
- base64: string(),
18647
18730
  sizeBytes: number(),
18648
18731
  timestamp: number()
18732
+ }).extend({
18733
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18734
+ url: string(),
18735
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18736
+ base64: string()
18649
18737
  });
18650
18738
  /**
18651
18739
  * One media row WITHOUT its bytes.
@@ -18657,7 +18745,9 @@ var MediaFileSchema = object({
18657
18745
  * blocks the whole view.
18658
18746
  *
18659
18747
  * `sizeBytes` is carried because it is what lets a client decide between the
18660
- * stored blob and a `?variant=thumb` rendering without fetching either.
18748
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18749
+ * `url` because a client that had to build the plane path itself is a second
18750
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18661
18751
  */
18662
18752
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18663
18753
  /**
@@ -19346,6 +19436,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19346
19436
  }), array(MediaFileSchema).readonly()), method(object({
19347
19437
  trackId: string(),
19348
19438
  deviceId: number()
19439
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19440
+ eventId: string(),
19441
+ deviceId: number()
19349
19442
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19350
19443
  kind: "mutation",
19351
19444
  auth: "admin"
@@ -24336,10 +24429,24 @@ var FaceClusterSchema = object({
24336
24429
  size: number().int(),
24337
24430
  cohesion: number()
24338
24431
  });
24432
+ /**
24433
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24434
+ * are — never the bytes.
24435
+ *
24436
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24437
+ * track/event contract) is still populated because a deployed viewer requires
24438
+ * the field to parse a row at all; this method has no such reader. Its ONE
24439
+ * caller is the admin UI's detail modal, which was building
24440
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24441
+ * dialog already rendering its key FRAME from the `event-media` plane.
24442
+ *
24443
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24444
+ * media key directly, so this needed no new plane and no new access decision.
24445
+ */
24339
24446
  var MediaFileLiteSchema$1 = object({
24340
24447
  key: string(),
24341
24448
  kind: string(),
24342
- base64: string(),
24449
+ url: string(),
24343
24450
  sizeBytes: number(),
24344
24451
  timestamp: number()
24345
24452
  });
@@ -27361,10 +27468,24 @@ var PlateInfoSchema = object({
27361
27468
  */
27362
27469
  cropUrl: string().optional()
27363
27470
  });
27471
+ /**
27472
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27473
+ * are — never the bytes.
27474
+ *
27475
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27476
+ * track/event contract) is still populated because a deployed viewer requires
27477
+ * the field to parse a row at all; this method has no such reader. Its ONE
27478
+ * caller is the admin UI's detail modal, which was building
27479
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27480
+ * dialog already rendering its key FRAME from the `event-media` plane.
27481
+ *
27482
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27483
+ * media key directly, so this needed no new plane and no new access decision.
27484
+ */
27364
27485
  var MediaFileLiteSchema = object({
27365
27486
  key: string(),
27366
27487
  kind: string(),
27367
- base64: string(),
27488
+ url: string(),
27368
27489
  sizeBytes: number(),
27369
27490
  timestamp: number()
27370
27491
  });
@@ -35261,6 +35382,12 @@ Object.freeze({
35261
35382
  addonId: null,
35262
35383
  access: "view"
35263
35384
  },
35385
+ "pipelineAnalytics.listEventMedia": {
35386
+ capName: "pipeline-analytics",
35387
+ capScope: "device",
35388
+ addonId: null,
35389
+ access: "view"
35390
+ },
35264
35391
  "pipelineAnalytics.listGroups": {
35265
35392
  capName: "pipeline-analytics",
35266
35393
  capScope: "device",
@@ -38884,6 +39011,11 @@ Object.freeze({
38884
39011
  form: "array",
38885
39012
  optional: false
38886
39013
  }],
39014
+ "pipelineAnalytics.listEventMedia": [{
39015
+ name: "deviceId",
39016
+ form: "single",
39017
+ optional: false
39018
+ }],
38887
39019
  "pipelineAnalytics.listGroups": [{
38888
39020
  name: "deviceIds",
38889
39021
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-velux",
3
- "version": "0.2.45",
3
+ "version": "0.2.47",
4
4
  "description": "Velux KLF-200 device-provider addon for CamStack — local-gateway window + shutter covers (positional, no tilt)",
5
5
  "keywords": [
6
6
  "camstack",