@camstack/addon-provider-homematic 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/addon.js +313 -181
  2. package/dist/addon.mjs +313 -181
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -8012,6 +8012,21 @@ var RelocateJobSchema = object({
8012
8012
  bytesMoved: number().int(),
8013
8013
  /** Total files discovered up front; null while (or when) unknown. */
8014
8014
  filesTotal: number().int().nullable(),
8015
+ /**
8016
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8017
+ * made that nobody asked for, so it is reported where the operator reads the
8018
+ * job rather than only in a log line.
8019
+ *
8020
+ * A footage segment records its byte count in its own NAME, and the durable
8021
+ * hour row derives its aggregates from those names. A file that does not
8022
+ * match its name therefore makes the ledger's sums — and with them quota and
8023
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8024
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8025
+ *
8026
+ * Absent on lanes where the question has no meaning: a media blob's size is
8027
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8028
+ */
8029
+ rowsReconciled: number().int().nonnegative().optional(),
8015
8030
  startedAt: number(),
8016
8031
  finishedAt: number().nullable(),
8017
8032
  error: string().nullable()
@@ -8080,14 +8095,42 @@ var RelocateMediaInputSchema = object({
8080
8095
  /** Omitted = `move`, the pre-existing behaviour. */
8081
8096
  mode: MediaRelocateModeSchema.optional()
8082
8097
  });
8083
- /** How many rows still carry NO `locationId` — the population a repoint would
8084
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8085
- * value that permits a non-blocking `eventMedia` cutover. */
8086
- var UnstampedEventMediaCountSchema = object({
8087
- media: number().int().nonnegative(),
8088
- retrainFrames: number().int().nonnegative(),
8089
- total: number().int().nonnegative()
8098
+ /**
8099
+ * The unstamped population of ONE collection — split, because the gate and the
8100
+ * operator ask two different questions and only one of them has to be cheap.
8101
+ *
8102
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8103
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8104
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8105
+ * that matters — after a seal, when the population is empty.
8106
+ *
8107
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8108
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8109
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8110
+ * and useful answer: "there are some, and this read could not say how many"
8111
+ * still refuses the cutover, which is the whole job.
8112
+ */
8113
+ var UnstampedRowsSchema = object({
8114
+ present: boolean(),
8115
+ rows: number().int().nonnegative().nullable()
8090
8116
  });
8117
+ /**
8118
+ * How many rows still carry NO `locationId` — the population a repoint would
8119
+ * silently re-aim at a disk that does not hold their bytes.
8120
+ *
8121
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8122
+ * over. The gate opens on a measured absence and on nothing else; an unread
8123
+ * collection and an empty one are different facts, and this repo has already
8124
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8125
+ */
8126
+ var UnstampedEventMediaCountSchema = object({
8127
+ media: UnstampedRowsSchema,
8128
+ retrainFrames: UnstampedRowsSchema,
8129
+ /** True when EITHER collection holds one. The refusal reads this. */
8130
+ anyPresent: boolean(),
8131
+ /** Sum across both, or `null` when either lane could not be counted. */
8132
+ total: number().int().nonnegative().nullable()
8133
+ }).nullable();
8091
8134
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8092
8135
  /** The independently selectable logical storage classes — every class
8093
8136
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8198,6 +8241,10 @@ var StorageMigrationMoveProgressSchema = object({
8198
8241
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8199
8242
  filesTotal: number().int().nonnegative().nullable(),
8200
8243
  bytesMoved: number().int().nonnegative(),
8244
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8245
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8246
+ * rows would be the same failure as one that silently skipped them. */
8247
+ rowsReconciled: number().int().nonnegative().optional(),
8201
8248
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8202
8249
  * crash gets a new mover, and a rate computed from the migration's start
8203
8250
  * would silently average in the time nothing was running. */
@@ -13212,6 +13259,114 @@ method(object({
13212
13259
  height: number()
13213
13260
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13214
13261
  /**
13262
+ * `failure-contribution` — the capability an addon reports its OWN losses
13263
+ * through, per camera, with the denominator attached. It stores nothing.
13264
+ *
13265
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13266
+ *
13267
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13268
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13269
+ * copied: the contributor reports what it already knows, hub-main adds only
13270
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13271
+ * somebody to forget to edit.
13272
+ *
13273
+ * They are not merged, because their invariants are opposites:
13274
+ *
13275
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13276
+ * claim a camera cost nothing, which is a measurement nobody made;
13277
+ * - a `failure-contribution` zero is the **most valuable value on the
13278
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13279
+ * and it is exactly what an absent entry cannot say.
13280
+ *
13281
+ * Putting a loss counter on a cost entry would also break the reconciliation
13282
+ * that gives `load-contribution` its point: contributions are subtracted from
13283
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13284
+ * has no process.
13285
+ *
13286
+ * ## Why not a log line, since the counters already exist
13287
+ *
13288
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13289
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13290
+ * ends in a log line, and a log line is the thing the operator asked to stop
13291
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13292
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13293
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13294
+ * media blackout were both diagnosed. The counters stay; this is where they can
13295
+ * be READ.
13296
+ *
13297
+ * ## The rate is served with its denominator or not at all
13298
+ *
13299
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13300
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13301
+ * than yesterday" and was **flat across twelve hours** once divided by the
13302
+ * successes on the same path. A surface that publishes only the numerator
13303
+ * reproduces that mistake on every read.
13304
+ *
13305
+ * ## Shape
13306
+ *
13307
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13308
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13309
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13310
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13311
+ * a forked runner's entries reach hub-main over transport that already exists.
13312
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13313
+ * result through `system.getFailureContributions`.
13314
+ */
13315
+ var FailureReasonCountSchema = object({
13316
+ /**
13317
+ * Why the attempt did not land, in the contributor's own vocabulary —
13318
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13319
+ * strings that already appear in this repo's logs and, where one exists, the
13320
+ * same string the per-track `previewMissReason` records (D276): a second
13321
+ * vocabulary for the same loss would make the row and the counter
13322
+ * un-joinable.
13323
+ */
13324
+ reason: string(),
13325
+ count: number().int().nonnegative()
13326
+ });
13327
+ var FailureContributionSchema = object({
13328
+ /**
13329
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13330
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13331
+ * `unit` free: the families are owned by different addons and a shared enum
13332
+ * is a central list that rots invisibly.
13333
+ */
13334
+ family: string(),
13335
+ /**
13336
+ * The NUMERIC device id — the same value every log line carries as
13337
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13338
+ * cannot name the camera must not emit the entry, because a fleet total
13339
+ * cannot answer the only question anybody asks of this surface.
13340
+ */
13341
+ deviceId: number().int().positive(),
13342
+ /**
13343
+ * A second dimension inside the family: the model / step id for an inference
13344
+ * timeout, so "which camera AND which model" is one read. Absent when the
13345
+ * family has a single variant.
13346
+ */
13347
+ variant: string().optional(),
13348
+ /**
13349
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13350
+ * differencing two reads must drop the interval when it changes, because the
13351
+ * counter restarted from zero in a respawned runner. Same discipline as
13352
+ * `LoadContribution.startedAtMs`.
13353
+ */
13354
+ sinceMs: number(),
13355
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13356
+ atMs: number(),
13357
+ /**
13358
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13359
+ * window. A failure count published without it is the mistake this schema
13360
+ * exists to make impossible.
13361
+ */
13362
+ attempts: number().int().nonnegative(),
13363
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13364
+ succeeded: number().int().nonnegative(),
13365
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13366
+ reasons: array(FailureReasonCountSchema).readonly()
13367
+ });
13368
+ method(_void(), array(FailureContributionSchema).readonly());
13369
+ /**
13215
13370
  * filesystem-browse — per-node capability for browsing the node's local
13216
13371
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13217
13372
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13733,6 +13888,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13733
13888
  kind: "mutation",
13734
13889
  auth: "admin"
13735
13890
  });
13891
+ var LoadContributionSchema = object({
13892
+ role: _enum([
13893
+ "decode",
13894
+ "transcode",
13895
+ "recording",
13896
+ "streaming",
13897
+ "detection"
13898
+ ]),
13899
+ /**
13900
+ * The NUMERIC device id — the same value every log line carries as
13901
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13902
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13903
+ * contributor that cannot name its camera must not emit the entry at all,
13904
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13905
+ * and would quietly turn one camera's cost into everybody's.
13906
+ */
13907
+ deviceId: number().int().positive().nullable(),
13908
+ attribution: _enum([
13909
+ "measured",
13910
+ "accounted",
13911
+ "unattributable"
13912
+ ]),
13913
+ /**
13914
+ * What ONE entry is, in the contributor's own words — `615/high`,
13915
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13916
+ * family and inventing a common one would lose the only information that
13917
+ * makes two entries for the same camera distinguishable.
13918
+ */
13919
+ unit: string(),
13920
+ /**
13921
+ * The OS process this cost lives in, when there is one. Present so a
13922
+ * consumer can (a) tell two generations of the same unit apart across a
13923
+ * restart, and (b) subtract claimed processes from the node's process
13924
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13925
+ * process of its own.
13926
+ */
13927
+ pid: number().int().positive().optional(),
13928
+ /**
13929
+ * When this generation started. The pid's incarnation marker: a consumer
13930
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13931
+ * window when this changes, because the counter restarted from zero in a new
13932
+ * process.
13933
+ */
13934
+ startedAtMs: number().optional(),
13935
+ /**
13936
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13937
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13938
+ * contribution is asked for.
13939
+ *
13940
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13941
+ * needs a sampler, and a new per-node sampler is the defect half of
13942
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13943
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13944
+ *
13945
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13946
+ * an entry with no process.
13947
+ */
13948
+ cpuSeconds: number().optional(),
13949
+ /** Resident bytes of this unit's process, same source and same rules. */
13950
+ rssBytes: number().optional()
13951
+ });
13952
+ method(_void(), array(LoadContributionSchema).readonly());
13736
13953
  /**
13737
13954
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13738
13955
  * through. It stores nothing.
@@ -13809,176 +14026,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13809
14026
  tags: record(string(), string()).optional()
13810
14027
  }), array(LogEntrySchema).readonly());
13811
14028
  /**
13812
- * `failure-contribution` — the capability an addon reports its OWN losses
13813
- * through, per camera, with the denominator attached. It stores nothing.
13814
- *
13815
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13816
- *
13817
- * `load-contribution` answers *what did this camera COST*. This answers *what
13818
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13819
- * copied: the contributor reports what it already knows, hub-main adds only
13820
- * `addonId`, nothing needs global knowledge, and there is no central list for
13821
- * somebody to forget to edit.
13822
- *
13823
- * They are not merged, because their invariants are opposites:
13824
- *
13825
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13826
- * claim a camera cost nothing, which is a measurement nobody made;
13827
- * - a `failure-contribution` zero is the **most valuable value on the
13828
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13829
- * and it is exactly what an absent entry cannot say.
13830
- *
13831
- * Putting a loss counter on a cost entry would also break the reconciliation
13832
- * that gives `load-contribution` its point: contributions are subtracted from
13833
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13834
- * has no process.
13835
- *
13836
- * ## Why not a log line, since the counters already exist
13837
- *
13838
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13839
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13840
- * ends in a log line, and a log line is the thing the operator asked to stop
13841
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13842
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13843
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13844
- * media blackout were both diagnosed. The counters stay; this is where they can
13845
- * be READ.
13846
- *
13847
- * ## The rate is served with its denominator or not at all
13848
- *
13849
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13850
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13851
- * than yesterday" and was **flat across twelve hours** once divided by the
13852
- * successes on the same path. A surface that publishes only the numerator
13853
- * reproduces that mistake on every read.
13854
- *
13855
- * ## Shape
13856
- *
13857
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13858
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13859
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13860
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13861
- * a forked runner's entries reach hub-main over transport that already exists.
13862
- * No new UDS message, no second registry (D3). The operator reads the assembled
13863
- * result through `system.getFailureContributions`.
13864
- */
13865
- var FailureReasonCountSchema = object({
13866
- /**
13867
- * Why the attempt did not land, in the contributor's own vocabulary —
13868
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13869
- * strings that already appear in this repo's logs and, where one exists, the
13870
- * same string the per-track `previewMissReason` records (D276): a second
13871
- * vocabulary for the same loss would make the row and the counter
13872
- * un-joinable.
13873
- */
13874
- reason: string(),
13875
- count: number().int().nonnegative()
13876
- });
13877
- var FailureContributionSchema = object({
13878
- /**
13879
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13880
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13881
- * `unit` free: the families are owned by different addons and a shared enum
13882
- * is a central list that rots invisibly.
13883
- */
13884
- family: string(),
13885
- /**
13886
- * The NUMERIC device id — the same value every log line carries as
13887
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13888
- * cannot name the camera must not emit the entry, because a fleet total
13889
- * cannot answer the only question anybody asks of this surface.
13890
- */
13891
- deviceId: number().int().positive(),
13892
- /**
13893
- * A second dimension inside the family: the model / step id for an inference
13894
- * timeout, so "which camera AND which model" is one read. Absent when the
13895
- * family has a single variant.
13896
- */
13897
- variant: string().optional(),
13898
- /**
13899
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13900
- * differencing two reads must drop the interval when it changes, because the
13901
- * counter restarted from zero in a respawned runner. Same discipline as
13902
- * `LoadContribution.startedAtMs`.
13903
- */
13904
- sinceMs: number(),
13905
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13906
- atMs: number(),
13907
- /**
13908
- * THE DENOMINATOR — every attempt on this path for this camera in the
13909
- * window. A failure count published without it is the mistake this schema
13910
- * exists to make impossible.
13911
- */
13912
- attempts: number().int().nonnegative(),
13913
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13914
- succeeded: number().int().nonnegative(),
13915
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13916
- reasons: array(FailureReasonCountSchema).readonly()
13917
- });
13918
- method(_void(), array(FailureContributionSchema).readonly());
13919
- var LoadContributionSchema = object({
13920
- role: _enum([
13921
- "decode",
13922
- "transcode",
13923
- "recording",
13924
- "streaming",
13925
- "detection"
13926
- ]),
13927
- /**
13928
- * The NUMERIC device id — the same value every log line carries as
13929
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13930
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13931
- * contributor that cannot name its camera must not emit the entry at all,
13932
- * because an unnamed per-camera entry is indistinguishable from a shared one
13933
- * and would quietly turn one camera's cost into everybody's.
13934
- */
13935
- deviceId: number().int().positive().nullable(),
13936
- attribution: _enum([
13937
- "measured",
13938
- "accounted",
13939
- "unattributable"
13940
- ]),
13941
- /**
13942
- * What ONE entry is, in the contributor's own words — `615/high`,
13943
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13944
- * family and inventing a common one would lose the only information that
13945
- * makes two entries for the same camera distinguishable.
13946
- */
13947
- unit: string(),
13948
- /**
13949
- * The OS process this cost lives in, when there is one. Present so a
13950
- * consumer can (a) tell two generations of the same unit apart across a
13951
- * restart, and (b) subtract claimed processes from the node's process
13952
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13953
- * process of its own.
13954
- */
13955
- pid: number().int().positive().optional(),
13956
- /**
13957
- * When this generation started. The pid's incarnation marker: a consumer
13958
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13959
- * window when this changes, because the counter restarted from zero in a new
13960
- * process.
13961
- */
13962
- startedAtMs: number().optional(),
13963
- /**
13964
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13965
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13966
- * contribution is asked for.
13967
- *
13968
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13969
- * needs a sampler, and a new per-node sampler is the defect half of
13970
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13971
- * by whoever already keeps a history; a rate cannot be un-averaged.
13972
- *
13973
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13974
- * an entry with no process.
13975
- */
13976
- cpuSeconds: number().optional(),
13977
- /** Resident bytes of this unit's process, same source and same rules. */
13978
- rssBytes: number().optional()
13979
- });
13980
- method(_void(), array(LoadContributionSchema).readonly());
13981
- /**
13982
14029
  * `login-method` — collection cap through which auth addons contribute
13983
14030
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13984
14031
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18716,12 +18763,53 @@ var MediaFileKindEnum = _enum([
18716
18763
  "keyFrameSmall",
18717
18764
  "thumbnailSmall"
18718
18765
  ]);
18766
+ /**
18767
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18768
+ * ARE — never the bytes themselves.
18769
+ *
18770
+ * ## Why `url` and not `base64`
18771
+ *
18772
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18773
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18774
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18775
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18776
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18777
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18778
+ *
18779
+ * `url` points at the `event-media` data plane
18780
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18781
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18782
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18783
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18784
+ * no less protected than they were inside a `view`-level cap response — see
18785
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18786
+ * (per-device scoping).
18787
+ *
18788
+ * The URL is built from the row's **stored** key, which is not always its
18789
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18790
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18791
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18792
+ *
18793
+ * ## `base64` is TRANSITIONAL and is going away
18794
+ *
18795
+ * It is still populated for one reason: the deployed viewer's track-detail
18796
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18797
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18798
+ * triangle — not as absence. Removing the field before that viewer ships is an
18799
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18800
+ * delete this line and the `withBytes` pass-through in
18801
+ * `analytics-query-facade.ts`; nothing else reads it.
18802
+ */
18719
18803
  var MediaFileSchema = object({
18720
18804
  key: string(),
18721
18805
  kind: MediaFileKindEnum,
18722
- base64: string(),
18723
18806
  sizeBytes: number(),
18724
18807
  timestamp: number()
18808
+ }).extend({
18809
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18810
+ url: string(),
18811
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18812
+ base64: string()
18725
18813
  });
18726
18814
  /**
18727
18815
  * One media row WITHOUT its bytes.
@@ -18733,7 +18821,9 @@ var MediaFileSchema = object({
18733
18821
  * blocks the whole view.
18734
18822
  *
18735
18823
  * `sizeBytes` is carried because it is what lets a client decide between the
18736
- * stored blob and a `?variant=thumb` rendering without fetching either.
18824
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18825
+ * `url` because a client that had to build the plane path itself is a second
18826
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18737
18827
  */
18738
18828
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18739
18829
  /**
@@ -19422,6 +19512,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19422
19512
  }), array(MediaFileSchema).readonly()), method(object({
19423
19513
  trackId: string(),
19424
19514
  deviceId: number()
19515
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19516
+ eventId: string(),
19517
+ deviceId: number()
19425
19518
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19426
19519
  kind: "mutation",
19427
19520
  auth: "admin"
@@ -24412,10 +24505,24 @@ var FaceClusterSchema = object({
24412
24505
  size: number().int(),
24413
24506
  cohesion: number()
24414
24507
  });
24508
+ /**
24509
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24510
+ * are — never the bytes.
24511
+ *
24512
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24513
+ * track/event contract) is still populated because a deployed viewer requires
24514
+ * the field to parse a row at all; this method has no such reader. Its ONE
24515
+ * caller is the admin UI's detail modal, which was building
24516
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24517
+ * dialog already rendering its key FRAME from the `event-media` plane.
24518
+ *
24519
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24520
+ * media key directly, so this needed no new plane and no new access decision.
24521
+ */
24415
24522
  var MediaFileLiteSchema$1 = object({
24416
24523
  key: string(),
24417
24524
  kind: string(),
24418
- base64: string(),
24525
+ url: string(),
24419
24526
  sizeBytes: number(),
24420
24527
  timestamp: number()
24421
24528
  });
@@ -27437,10 +27544,24 @@ var PlateInfoSchema = object({
27437
27544
  */
27438
27545
  cropUrl: string().optional()
27439
27546
  });
27547
+ /**
27548
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27549
+ * are — never the bytes.
27550
+ *
27551
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27552
+ * track/event contract) is still populated because a deployed viewer requires
27553
+ * the field to parse a row at all; this method has no such reader. Its ONE
27554
+ * caller is the admin UI's detail modal, which was building
27555
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27556
+ * dialog already rendering its key FRAME from the `event-media` plane.
27557
+ *
27558
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27559
+ * media key directly, so this needed no new plane and no new access decision.
27560
+ */
27440
27561
  var MediaFileLiteSchema = object({
27441
27562
  key: string(),
27442
27563
  kind: string(),
27443
- base64: string(),
27564
+ url: string(),
27444
27565
  sizeBytes: number(),
27445
27566
  timestamp: number()
27446
27567
  });
@@ -35337,6 +35458,12 @@ Object.freeze({
35337
35458
  addonId: null,
35338
35459
  access: "view"
35339
35460
  },
35461
+ "pipelineAnalytics.listEventMedia": {
35462
+ capName: "pipeline-analytics",
35463
+ capScope: "device",
35464
+ addonId: null,
35465
+ access: "view"
35466
+ },
35340
35467
  "pipelineAnalytics.listGroups": {
35341
35468
  capName: "pipeline-analytics",
35342
35469
  capScope: "device",
@@ -38960,6 +39087,11 @@ Object.freeze({
38960
39087
  form: "array",
38961
39088
  optional: false
38962
39089
  }],
39090
+ "pipelineAnalytics.listEventMedia": [{
39091
+ name: "deviceId",
39092
+ form: "single",
39093
+ optional: false
39094
+ }],
38963
39095
  "pipelineAnalytics.listGroups": [{
38964
39096
  name: "deviceIds",
38965
39097
  form: "array",
package/dist/addon.mjs CHANGED
@@ -8013,6 +8013,21 @@ var RelocateJobSchema = object({
8013
8013
  bytesMoved: number().int(),
8014
8014
  /** Total files discovered up front; null while (or when) unknown. */
8015
8015
  filesTotal: number().int().nullable(),
8016
+ /**
8017
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8018
+ * made that nobody asked for, so it is reported where the operator reads the
8019
+ * job rather than only in a log line.
8020
+ *
8021
+ * A footage segment records its byte count in its own NAME, and the durable
8022
+ * hour row derives its aggregates from those names. A file that does not
8023
+ * match its name therefore makes the ledger's sums — and with them quota and
8024
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8025
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8026
+ *
8027
+ * Absent on lanes where the question has no meaning: a media blob's size is
8028
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8029
+ */
8030
+ rowsReconciled: number().int().nonnegative().optional(),
8016
8031
  startedAt: number(),
8017
8032
  finishedAt: number().nullable(),
8018
8033
  error: string().nullable()
@@ -8081,14 +8096,42 @@ var RelocateMediaInputSchema = object({
8081
8096
  /** Omitted = `move`, the pre-existing behaviour. */
8082
8097
  mode: MediaRelocateModeSchema.optional()
8083
8098
  });
8084
- /** How many rows still carry NO `locationId` — the population a repoint would
8085
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8086
- * value that permits a non-blocking `eventMedia` cutover. */
8087
- var UnstampedEventMediaCountSchema = object({
8088
- media: number().int().nonnegative(),
8089
- retrainFrames: number().int().nonnegative(),
8090
- total: number().int().nonnegative()
8099
+ /**
8100
+ * The unstamped population of ONE collection — split, because the gate and the
8101
+ * operator ask two different questions and only one of them has to be cheap.
8102
+ *
8103
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8104
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8105
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8106
+ * that matters — after a seal, when the population is empty.
8107
+ *
8108
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8109
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8110
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8111
+ * and useful answer: "there are some, and this read could not say how many"
8112
+ * still refuses the cutover, which is the whole job.
8113
+ */
8114
+ var UnstampedRowsSchema = object({
8115
+ present: boolean(),
8116
+ rows: number().int().nonnegative().nullable()
8091
8117
  });
8118
+ /**
8119
+ * How many rows still carry NO `locationId` — the population a repoint would
8120
+ * silently re-aim at a disk that does not hold their bytes.
8121
+ *
8122
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8123
+ * over. The gate opens on a measured absence and on nothing else; an unread
8124
+ * collection and an empty one are different facts, and this repo has already
8125
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8126
+ */
8127
+ var UnstampedEventMediaCountSchema = object({
8128
+ media: UnstampedRowsSchema,
8129
+ retrainFrames: UnstampedRowsSchema,
8130
+ /** True when EITHER collection holds one. The refusal reads this. */
8131
+ anyPresent: boolean(),
8132
+ /** Sum across both, or `null` when either lane could not be counted. */
8133
+ total: number().int().nonnegative().nullable()
8134
+ }).nullable();
8092
8135
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8093
8136
  /** The independently selectable logical storage classes — every class
8094
8137
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8199,6 +8242,10 @@ var StorageMigrationMoveProgressSchema = object({
8199
8242
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8200
8243
  filesTotal: number().int().nonnegative().nullable(),
8201
8244
  bytesMoved: number().int().nonnegative(),
8245
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8246
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8247
+ * rows would be the same failure as one that silently skipped them. */
8248
+ rowsReconciled: number().int().nonnegative().optional(),
8202
8249
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8203
8250
  * crash gets a new mover, and a rate computed from the migration's start
8204
8251
  * would silently average in the time nothing was running. */
@@ -13213,6 +13260,114 @@ method(object({
13213
13260
  height: number()
13214
13261
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13215
13262
  /**
13263
+ * `failure-contribution` — the capability an addon reports its OWN losses
13264
+ * through, per camera, with the denominator attached. It stores nothing.
13265
+ *
13266
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13267
+ *
13268
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13269
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13270
+ * copied: the contributor reports what it already knows, hub-main adds only
13271
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13272
+ * somebody to forget to edit.
13273
+ *
13274
+ * They are not merged, because their invariants are opposites:
13275
+ *
13276
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13277
+ * claim a camera cost nothing, which is a measurement nobody made;
13278
+ * - a `failure-contribution` zero is the **most valuable value on the
13279
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13280
+ * and it is exactly what an absent entry cannot say.
13281
+ *
13282
+ * Putting a loss counter on a cost entry would also break the reconciliation
13283
+ * that gives `load-contribution` its point: contributions are subtracted from
13284
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13285
+ * has no process.
13286
+ *
13287
+ * ## Why not a log line, since the counters already exist
13288
+ *
13289
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13290
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13291
+ * ends in a log line, and a log line is the thing the operator asked to stop
13292
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13293
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13294
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13295
+ * media blackout were both diagnosed. The counters stay; this is where they can
13296
+ * be READ.
13297
+ *
13298
+ * ## The rate is served with its denominator or not at all
13299
+ *
13300
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13301
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13302
+ * than yesterday" and was **flat across twelve hours** once divided by the
13303
+ * successes on the same path. A surface that publishes only the numerator
13304
+ * reproduces that mistake on every read.
13305
+ *
13306
+ * ## Shape
13307
+ *
13308
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13309
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13310
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13311
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13312
+ * a forked runner's entries reach hub-main over transport that already exists.
13313
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13314
+ * result through `system.getFailureContributions`.
13315
+ */
13316
+ var FailureReasonCountSchema = object({
13317
+ /**
13318
+ * Why the attempt did not land, in the contributor's own vocabulary —
13319
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13320
+ * strings that already appear in this repo's logs and, where one exists, the
13321
+ * same string the per-track `previewMissReason` records (D276): a second
13322
+ * vocabulary for the same loss would make the row and the counter
13323
+ * un-joinable.
13324
+ */
13325
+ reason: string(),
13326
+ count: number().int().nonnegative()
13327
+ });
13328
+ var FailureContributionSchema = object({
13329
+ /**
13330
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13331
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13332
+ * `unit` free: the families are owned by different addons and a shared enum
13333
+ * is a central list that rots invisibly.
13334
+ */
13335
+ family: string(),
13336
+ /**
13337
+ * The NUMERIC device id — the same value every log line carries as
13338
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13339
+ * cannot name the camera must not emit the entry, because a fleet total
13340
+ * cannot answer the only question anybody asks of this surface.
13341
+ */
13342
+ deviceId: number().int().positive(),
13343
+ /**
13344
+ * A second dimension inside the family: the model / step id for an inference
13345
+ * timeout, so "which camera AND which model" is one read. Absent when the
13346
+ * family has a single variant.
13347
+ */
13348
+ variant: string().optional(),
13349
+ /**
13350
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13351
+ * differencing two reads must drop the interval when it changes, because the
13352
+ * counter restarted from zero in a respawned runner. Same discipline as
13353
+ * `LoadContribution.startedAtMs`.
13354
+ */
13355
+ sinceMs: number(),
13356
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13357
+ atMs: number(),
13358
+ /**
13359
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13360
+ * window. A failure count published without it is the mistake this schema
13361
+ * exists to make impossible.
13362
+ */
13363
+ attempts: number().int().nonnegative(),
13364
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13365
+ succeeded: number().int().nonnegative(),
13366
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13367
+ reasons: array(FailureReasonCountSchema).readonly()
13368
+ });
13369
+ method(_void(), array(FailureContributionSchema).readonly());
13370
+ /**
13216
13371
  * filesystem-browse — per-node capability for browsing the node's local
13217
13372
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13218
13373
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13734,6 +13889,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13734
13889
  kind: "mutation",
13735
13890
  auth: "admin"
13736
13891
  });
13892
+ var LoadContributionSchema = object({
13893
+ role: _enum([
13894
+ "decode",
13895
+ "transcode",
13896
+ "recording",
13897
+ "streaming",
13898
+ "detection"
13899
+ ]),
13900
+ /**
13901
+ * The NUMERIC device id — the same value every log line carries as
13902
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13903
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13904
+ * contributor that cannot name its camera must not emit the entry at all,
13905
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13906
+ * and would quietly turn one camera's cost into everybody's.
13907
+ */
13908
+ deviceId: number().int().positive().nullable(),
13909
+ attribution: _enum([
13910
+ "measured",
13911
+ "accounted",
13912
+ "unattributable"
13913
+ ]),
13914
+ /**
13915
+ * What ONE entry is, in the contributor's own words — `615/high`,
13916
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13917
+ * family and inventing a common one would lose the only information that
13918
+ * makes two entries for the same camera distinguishable.
13919
+ */
13920
+ unit: string(),
13921
+ /**
13922
+ * The OS process this cost lives in, when there is one. Present so a
13923
+ * consumer can (a) tell two generations of the same unit apart across a
13924
+ * restart, and (b) subtract claimed processes from the node's process
13925
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13926
+ * process of its own.
13927
+ */
13928
+ pid: number().int().positive().optional(),
13929
+ /**
13930
+ * When this generation started. The pid's incarnation marker: a consumer
13931
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13932
+ * window when this changes, because the counter restarted from zero in a new
13933
+ * process.
13934
+ */
13935
+ startedAtMs: number().optional(),
13936
+ /**
13937
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13938
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13939
+ * contribution is asked for.
13940
+ *
13941
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13942
+ * needs a sampler, and a new per-node sampler is the defect half of
13943
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13944
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13945
+ *
13946
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13947
+ * an entry with no process.
13948
+ */
13949
+ cpuSeconds: number().optional(),
13950
+ /** Resident bytes of this unit's process, same source and same rules. */
13951
+ rssBytes: number().optional()
13952
+ });
13953
+ method(_void(), array(LoadContributionSchema).readonly());
13737
13954
  /**
13738
13955
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13739
13956
  * through. It stores nothing.
@@ -13810,176 +14027,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13810
14027
  tags: record(string(), string()).optional()
13811
14028
  }), array(LogEntrySchema).readonly());
13812
14029
  /**
13813
- * `failure-contribution` — the capability an addon reports its OWN losses
13814
- * through, per camera, with the denominator attached. It stores nothing.
13815
- *
13816
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13817
- *
13818
- * `load-contribution` answers *what did this camera COST*. This answers *what
13819
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13820
- * copied: the contributor reports what it already knows, hub-main adds only
13821
- * `addonId`, nothing needs global knowledge, and there is no central list for
13822
- * somebody to forget to edit.
13823
- *
13824
- * They are not merged, because their invariants are opposites:
13825
- *
13826
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13827
- * claim a camera cost nothing, which is a measurement nobody made;
13828
- * - a `failure-contribution` zero is the **most valuable value on the
13829
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13830
- * and it is exactly what an absent entry cannot say.
13831
- *
13832
- * Putting a loss counter on a cost entry would also break the reconciliation
13833
- * that gives `load-contribution` its point: contributions are subtracted from
13834
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13835
- * has no process.
13836
- *
13837
- * ## Why not a log line, since the counters already exist
13838
- *
13839
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13840
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13841
- * ends in a log line, and a log line is the thing the operator asked to stop
13842
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13843
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13844
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13845
- * media blackout were both diagnosed. The counters stay; this is where they can
13846
- * be READ.
13847
- *
13848
- * ## The rate is served with its denominator or not at all
13849
- *
13850
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13851
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13852
- * than yesterday" and was **flat across twelve hours** once divided by the
13853
- * successes on the same path. A surface that publishes only the numerator
13854
- * reproduces that mistake on every read.
13855
- *
13856
- * ## Shape
13857
- *
13858
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13859
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13860
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13861
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13862
- * a forked runner's entries reach hub-main over transport that already exists.
13863
- * No new UDS message, no second registry (D3). The operator reads the assembled
13864
- * result through `system.getFailureContributions`.
13865
- */
13866
- var FailureReasonCountSchema = object({
13867
- /**
13868
- * Why the attempt did not land, in the contributor's own vocabulary —
13869
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13870
- * strings that already appear in this repo's logs and, where one exists, the
13871
- * same string the per-track `previewMissReason` records (D276): a second
13872
- * vocabulary for the same loss would make the row and the counter
13873
- * un-joinable.
13874
- */
13875
- reason: string(),
13876
- count: number().int().nonnegative()
13877
- });
13878
- var FailureContributionSchema = object({
13879
- /**
13880
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13881
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13882
- * `unit` free: the families are owned by different addons and a shared enum
13883
- * is a central list that rots invisibly.
13884
- */
13885
- family: string(),
13886
- /**
13887
- * The NUMERIC device id — the same value every log line carries as
13888
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13889
- * cannot name the camera must not emit the entry, because a fleet total
13890
- * cannot answer the only question anybody asks of this surface.
13891
- */
13892
- deviceId: number().int().positive(),
13893
- /**
13894
- * A second dimension inside the family: the model / step id for an inference
13895
- * timeout, so "which camera AND which model" is one read. Absent when the
13896
- * family has a single variant.
13897
- */
13898
- variant: string().optional(),
13899
- /**
13900
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13901
- * differencing two reads must drop the interval when it changes, because the
13902
- * counter restarted from zero in a respawned runner. Same discipline as
13903
- * `LoadContribution.startedAtMs`.
13904
- */
13905
- sinceMs: number(),
13906
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13907
- atMs: number(),
13908
- /**
13909
- * THE DENOMINATOR — every attempt on this path for this camera in the
13910
- * window. A failure count published without it is the mistake this schema
13911
- * exists to make impossible.
13912
- */
13913
- attempts: number().int().nonnegative(),
13914
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13915
- succeeded: number().int().nonnegative(),
13916
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13917
- reasons: array(FailureReasonCountSchema).readonly()
13918
- });
13919
- method(_void(), array(FailureContributionSchema).readonly());
13920
- var LoadContributionSchema = object({
13921
- role: _enum([
13922
- "decode",
13923
- "transcode",
13924
- "recording",
13925
- "streaming",
13926
- "detection"
13927
- ]),
13928
- /**
13929
- * The NUMERIC device id — the same value every log line carries as
13930
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13931
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13932
- * contributor that cannot name its camera must not emit the entry at all,
13933
- * because an unnamed per-camera entry is indistinguishable from a shared one
13934
- * and would quietly turn one camera's cost into everybody's.
13935
- */
13936
- deviceId: number().int().positive().nullable(),
13937
- attribution: _enum([
13938
- "measured",
13939
- "accounted",
13940
- "unattributable"
13941
- ]),
13942
- /**
13943
- * What ONE entry is, in the contributor's own words — `615/high`,
13944
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13945
- * family and inventing a common one would lose the only information that
13946
- * makes two entries for the same camera distinguishable.
13947
- */
13948
- unit: string(),
13949
- /**
13950
- * The OS process this cost lives in, when there is one. Present so a
13951
- * consumer can (a) tell two generations of the same unit apart across a
13952
- * restart, and (b) subtract claimed processes from the node's process
13953
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13954
- * process of its own.
13955
- */
13956
- pid: number().int().positive().optional(),
13957
- /**
13958
- * When this generation started. The pid's incarnation marker: a consumer
13959
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13960
- * window when this changes, because the counter restarted from zero in a new
13961
- * process.
13962
- */
13963
- startedAtMs: number().optional(),
13964
- /**
13965
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13966
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13967
- * contribution is asked for.
13968
- *
13969
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13970
- * needs a sampler, and a new per-node sampler is the defect half of
13971
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13972
- * by whoever already keeps a history; a rate cannot be un-averaged.
13973
- *
13974
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13975
- * an entry with no process.
13976
- */
13977
- cpuSeconds: number().optional(),
13978
- /** Resident bytes of this unit's process, same source and same rules. */
13979
- rssBytes: number().optional()
13980
- });
13981
- method(_void(), array(LoadContributionSchema).readonly());
13982
- /**
13983
14030
  * `login-method` — collection cap through which auth addons contribute
13984
14031
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13985
14032
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18717,12 +18764,53 @@ var MediaFileKindEnum = _enum([
18717
18764
  "keyFrameSmall",
18718
18765
  "thumbnailSmall"
18719
18766
  ]);
18767
+ /**
18768
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18769
+ * ARE — never the bytes themselves.
18770
+ *
18771
+ * ## Why `url` and not `base64`
18772
+ *
18773
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18774
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18775
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18776
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18777
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18778
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18779
+ *
18780
+ * `url` points at the `event-media` data plane
18781
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18782
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18783
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18784
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18785
+ * no less protected than they were inside a `view`-level cap response — see
18786
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18787
+ * (per-device scoping).
18788
+ *
18789
+ * The URL is built from the row's **stored** key, which is not always its
18790
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18791
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18792
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18793
+ *
18794
+ * ## `base64` is TRANSITIONAL and is going away
18795
+ *
18796
+ * It is still populated for one reason: the deployed viewer's track-detail
18797
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18798
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18799
+ * triangle — not as absence. Removing the field before that viewer ships is an
18800
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18801
+ * delete this line and the `withBytes` pass-through in
18802
+ * `analytics-query-facade.ts`; nothing else reads it.
18803
+ */
18720
18804
  var MediaFileSchema = object({
18721
18805
  key: string(),
18722
18806
  kind: MediaFileKindEnum,
18723
- base64: string(),
18724
18807
  sizeBytes: number(),
18725
18808
  timestamp: number()
18809
+ }).extend({
18810
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18811
+ url: string(),
18812
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18813
+ base64: string()
18726
18814
  });
18727
18815
  /**
18728
18816
  * One media row WITHOUT its bytes.
@@ -18734,7 +18822,9 @@ var MediaFileSchema = object({
18734
18822
  * blocks the whole view.
18735
18823
  *
18736
18824
  * `sizeBytes` is carried because it is what lets a client decide between the
18737
- * stored blob and a `?variant=thumb` rendering without fetching either.
18825
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18826
+ * `url` because a client that had to build the plane path itself is a second
18827
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18738
18828
  */
18739
18829
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18740
18830
  /**
@@ -19423,6 +19513,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19423
19513
  }), array(MediaFileSchema).readonly()), method(object({
19424
19514
  trackId: string(),
19425
19515
  deviceId: number()
19516
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19517
+ eventId: string(),
19518
+ deviceId: number()
19426
19519
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19427
19520
  kind: "mutation",
19428
19521
  auth: "admin"
@@ -24413,10 +24506,24 @@ var FaceClusterSchema = object({
24413
24506
  size: number().int(),
24414
24507
  cohesion: number()
24415
24508
  });
24509
+ /**
24510
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24511
+ * are — never the bytes.
24512
+ *
24513
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24514
+ * track/event contract) is still populated because a deployed viewer requires
24515
+ * the field to parse a row at all; this method has no such reader. Its ONE
24516
+ * caller is the admin UI's detail modal, which was building
24517
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24518
+ * dialog already rendering its key FRAME from the `event-media` plane.
24519
+ *
24520
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24521
+ * media key directly, so this needed no new plane and no new access decision.
24522
+ */
24416
24523
  var MediaFileLiteSchema$1 = object({
24417
24524
  key: string(),
24418
24525
  kind: string(),
24419
- base64: string(),
24526
+ url: string(),
24420
24527
  sizeBytes: number(),
24421
24528
  timestamp: number()
24422
24529
  });
@@ -27438,10 +27545,24 @@ var PlateInfoSchema = object({
27438
27545
  */
27439
27546
  cropUrl: string().optional()
27440
27547
  });
27548
+ /**
27549
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27550
+ * are — never the bytes.
27551
+ *
27552
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27553
+ * track/event contract) is still populated because a deployed viewer requires
27554
+ * the field to parse a row at all; this method has no such reader. Its ONE
27555
+ * caller is the admin UI's detail modal, which was building
27556
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27557
+ * dialog already rendering its key FRAME from the `event-media` plane.
27558
+ *
27559
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27560
+ * media key directly, so this needed no new plane and no new access decision.
27561
+ */
27441
27562
  var MediaFileLiteSchema = object({
27442
27563
  key: string(),
27443
27564
  kind: string(),
27444
- base64: string(),
27565
+ url: string(),
27445
27566
  sizeBytes: number(),
27446
27567
  timestamp: number()
27447
27568
  });
@@ -35338,6 +35459,12 @@ Object.freeze({
35338
35459
  addonId: null,
35339
35460
  access: "view"
35340
35461
  },
35462
+ "pipelineAnalytics.listEventMedia": {
35463
+ capName: "pipeline-analytics",
35464
+ capScope: "device",
35465
+ addonId: null,
35466
+ access: "view"
35467
+ },
35341
35468
  "pipelineAnalytics.listGroups": {
35342
35469
  capName: "pipeline-analytics",
35343
35470
  capScope: "device",
@@ -38961,6 +39088,11 @@ Object.freeze({
38961
39088
  form: "array",
38962
39089
  optional: false
38963
39090
  }],
39091
+ "pipelineAnalytics.listEventMedia": [{
39092
+ name: "deviceId",
39093
+ form: "single",
39094
+ optional: false
39095
+ }],
38964
39096
  "pipelineAnalytics.listGroups": [{
38965
39097
  name: "deviceIds",
38966
39098
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homematic",
3
- "version": "1.2.46",
3
+ "version": "1.2.48",
4
4
  "description": "Homematic / HomematicIP (CCU3 / RaspberryMatic) device-provider addon for CamStack — wraps the nodehomematic library",
5
5
  "keywords": [
6
6
  "camstack",