@camstack/addon-provider-reolink 1.2.69 → 1.2.71

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 +350 -193
  2. package/dist/addon.mjs +350 -193
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -13767,6 +13767,133 @@ method(object({
13767
13767
  height: number()
13768
13768
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13769
13769
  /**
13770
+ * `failure-contribution` — the capability an addon reports its OWN losses
13771
+ * through, per camera, with the denominator attached. It stores nothing.
13772
+ *
13773
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13774
+ *
13775
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13776
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13777
+ * copied: the contributor reports what it already knows, hub-main adds only
13778
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13779
+ * somebody to forget to edit.
13780
+ *
13781
+ * They are not merged, because their invariants are opposites:
13782
+ *
13783
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13784
+ * claim a camera cost nothing, which is a measurement nobody made;
13785
+ * - a `failure-contribution` zero is the **most valuable value on the
13786
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13787
+ * and it is exactly what an absent entry cannot say.
13788
+ *
13789
+ * Putting a loss counter on a cost entry would also break the reconciliation
13790
+ * that gives `load-contribution` its point: contributions are subtracted from
13791
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13792
+ * has no process.
13793
+ *
13794
+ * ## Why not a log line, since the counters already exist
13795
+ *
13796
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13797
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13798
+ * ends in a log line, and a log line is the thing the operator asked to stop
13799
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13800
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13801
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13802
+ * media blackout were both diagnosed. The counters stay; this is where they can
13803
+ * be READ.
13804
+ *
13805
+ * ## The rate is served with its denominator or not at all
13806
+ *
13807
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13808
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13809
+ * than yesterday" and was **flat across twelve hours** once divided by the
13810
+ * successes on the same path. A surface that publishes only the numerator
13811
+ * reproduces that mistake on every read.
13812
+ *
13813
+ * ## Shape
13814
+ *
13815
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13816
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13817
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13818
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13819
+ * a forked runner's entries reach hub-main over transport that already exists.
13820
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13821
+ * result through `system.getFailureContributions`.
13822
+ */
13823
+ var FailureReasonCountSchema = object({
13824
+ /**
13825
+ * Why the attempt did not land, in the contributor's own vocabulary —
13826
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13827
+ * strings that already appear in this repo's logs and, where one exists, the
13828
+ * same string the per-track `previewMissReason` records (D276): a second
13829
+ * vocabulary for the same loss would make the row and the counter
13830
+ * un-joinable.
13831
+ */
13832
+ reason: string(),
13833
+ count: number().int().nonnegative()
13834
+ });
13835
+ var FailureContributionSchema = object({
13836
+ /**
13837
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13838
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13839
+ * `unit` free: the families are owned by different addons and a shared enum
13840
+ * is a central list that rots invisibly.
13841
+ */
13842
+ family: string(),
13843
+ /**
13844
+ * The NUMERIC device id — the same value every log line carries as
13845
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13846
+ * cannot name the camera must not emit the entry, because a fleet total
13847
+ * cannot answer the only question anybody asks of this surface.
13848
+ */
13849
+ deviceId: number().int().positive(),
13850
+ /**
13851
+ * A second dimension inside the family: the model / step id for an inference
13852
+ * timeout, so "which camera AND which model" is one read. Absent when the
13853
+ * family has a single variant.
13854
+ */
13855
+ variant: string().optional(),
13856
+ /**
13857
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13858
+ * differencing two reads must drop the interval when it changes, because the
13859
+ * counter restarted from zero in a respawned runner. Same discipline as
13860
+ * `LoadContribution.startedAtMs`.
13861
+ */
13862
+ sinceMs: number(),
13863
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13864
+ atMs: number(),
13865
+ /**
13866
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13867
+ * window. A failure count published without it is the mistake this schema
13868
+ * exists to make impossible.
13869
+ */
13870
+ attempts: number().int().nonnegative(),
13871
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13872
+ succeeded: number().int().nonnegative(),
13873
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13874
+ reasons: array(FailureReasonCountSchema).readonly()
13875
+ });
13876
+ var failureContributionCapability = {
13877
+ name: "failure-contribution",
13878
+ scope: "system",
13879
+ mode: "collection",
13880
+ internal: true,
13881
+ methods: {
13882
+ /**
13883
+ * This addon's per-camera failure counters, read live from bounded in-RAM
13884
+ * state it already keeps. Inert: no persistence, no sampling, no timer.
13885
+ *
13886
+ * READING NEVER RESETS. The counters are CUMULATIVE since `sinceMs`, and a
13887
+ * consumer that wants a rate differences two reads. A draining read would
13888
+ * make two operators with the page open each destroy half of the other's
13889
+ * numbers, and `load-contribution` already settled the same question the
13890
+ * same way for `cpuSeconds`.
13891
+ */
13892
+ list: method(_void(), array(FailureContributionSchema).readonly()) },
13893
+ /** In-process only — enumerated through `addons.listCapabilityProviders`. */
13894
+ mount: { kind: "skip" }
13895
+ };
13896
+ /**
13770
13897
  * filesystem-browse — per-node capability for browsing the node's local
13771
13898
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13772
13899
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -14288,6 +14415,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
14288
14415
  kind: "mutation",
14289
14416
  auth: "admin"
14290
14417
  });
14418
+ var LoadContributionSchema = object({
14419
+ role: _enum([
14420
+ "decode",
14421
+ "transcode",
14422
+ "recording",
14423
+ "streaming",
14424
+ "detection"
14425
+ ]),
14426
+ /**
14427
+ * The NUMERIC device id — the same value every log line carries as
14428
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14429
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
14430
+ * contributor that cannot name its camera must not emit the entry at all,
14431
+ * because an unnamed per-camera entry is indistinguishable from a shared one
14432
+ * and would quietly turn one camera's cost into everybody's.
14433
+ */
14434
+ deviceId: number().int().positive().nullable(),
14435
+ attribution: _enum([
14436
+ "measured",
14437
+ "accounted",
14438
+ "unattributable"
14439
+ ]),
14440
+ /**
14441
+ * What ONE entry is, in the contributor's own words — `615/high`,
14442
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14443
+ * family and inventing a common one would lose the only information that
14444
+ * makes two entries for the same camera distinguishable.
14445
+ */
14446
+ unit: string(),
14447
+ /**
14448
+ * The OS process this cost lives in, when there is one. Present so a
14449
+ * consumer can (a) tell two generations of the same unit apart across a
14450
+ * restart, and (b) subtract claimed processes from the node's process
14451
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14452
+ * process of its own.
14453
+ */
14454
+ pid: number().int().positive().optional(),
14455
+ /**
14456
+ * When this generation started. The pid's incarnation marker: a consumer
14457
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14458
+ * window when this changes, because the counter restarted from zero in a new
14459
+ * process.
14460
+ */
14461
+ startedAtMs: number().optional(),
14462
+ /**
14463
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14464
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
14465
+ * contribution is asked for.
14466
+ *
14467
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
14468
+ * needs a sampler, and a new per-node sampler is the defect half of
14469
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14470
+ * by whoever already keeps a history; a rate cannot be un-averaged.
14471
+ *
14472
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14473
+ * an entry with no process.
14474
+ */
14475
+ cpuSeconds: number().optional(),
14476
+ /** Resident bytes of this unit's process, same source and same rules. */
14477
+ rssBytes: number().optional()
14478
+ });
14479
+ method(_void(), array(LoadContributionSchema).readonly());
14291
14480
  /**
14292
14481
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
14293
14482
  * through. It stores nothing.
@@ -14385,195 +14574,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
14385
14574
  tags: record(string(), string()).optional()
14386
14575
  }), array(LogEntrySchema).readonly());
14387
14576
  /**
14388
- * `failure-contribution` — the capability an addon reports its OWN losses
14389
- * through, per camera, with the denominator attached. It stores nothing.
14390
- *
14391
- * ## The twin of `load-contribution`, and why it is a twin and not a field
14392
- *
14393
- * `load-contribution` answers *what did this camera COST*. This answers *what
14394
- * did this camera LOSE*. The reporting discipline is identical and deliberately
14395
- * copied: the contributor reports what it already knows, hub-main adds only
14396
- * `addonId`, nothing needs global knowledge, and there is no central list for
14397
- * somebody to forget to edit.
14398
- *
14399
- * They are not merged, because their invariants are opposites:
14400
- *
14401
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
14402
- * claim a camera cost nothing, which is a measurement nobody made;
14403
- * - a `failure-contribution` zero is the **most valuable value on the
14404
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14405
- * and it is exactly what an absent entry cannot say.
14406
- *
14407
- * Putting a loss counter on a cost entry would also break the reconciliation
14408
- * that gives `load-contribution` its point: contributions are subtracted from
14409
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14410
- * has no process.
14411
- *
14412
- * ## Why not a log line, since the counters already exist
14413
- *
14414
- * Several of these paths already counted themselves — `CaptureScheduler`'s
14415
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14416
- * ends in a log line, and a log line is the thing the operator asked to stop
14417
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14418
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14419
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14420
- * media blackout were both diagnosed. The counters stay; this is where they can
14421
- * be READ.
14422
- *
14423
- * ## The rate is served with its denominator or not at all
14424
- *
14425
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
14426
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14427
- * than yesterday" and was **flat across twelve hours** once divided by the
14428
- * successes on the same path. A surface that publishes only the numerator
14429
- * reproduces that mistake on every read.
14430
- *
14431
- * ## Shape
14432
- *
14433
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14434
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14435
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14436
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14437
- * a forked runner's entries reach hub-main over transport that already exists.
14438
- * No new UDS message, no second registry (D3). The operator reads the assembled
14439
- * result through `system.getFailureContributions`.
14440
- */
14441
- var FailureReasonCountSchema = object({
14442
- /**
14443
- * Why the attempt did not land, in the contributor's own vocabulary —
14444
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14445
- * strings that already appear in this repo's logs and, where one exists, the
14446
- * same string the per-track `previewMissReason` records (D276): a second
14447
- * vocabulary for the same loss would make the row and the counter
14448
- * un-joinable.
14449
- */
14450
- reason: string(),
14451
- count: number().int().nonnegative()
14452
- });
14453
- var FailureContributionSchema = object({
14454
- /**
14455
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14456
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14457
- * `unit` free: the families are owned by different addons and a shared enum
14458
- * is a central list that rots invisibly.
14459
- */
14460
- family: string(),
14461
- /**
14462
- * The NUMERIC device id — the same value every log line carries as
14463
- * `tags.deviceId`. Never nullable and never absent: a contributor that
14464
- * cannot name the camera must not emit the entry, because a fleet total
14465
- * cannot answer the only question anybody asks of this surface.
14466
- */
14467
- deviceId: number().int().positive(),
14468
- /**
14469
- * A second dimension inside the family: the model / step id for an inference
14470
- * timeout, so "which camera AND which model" is one read. Absent when the
14471
- * family has a single variant.
14472
- */
14473
- variant: string().optional(),
14474
- /**
14475
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14476
- * differencing two reads must drop the interval when it changes, because the
14477
- * counter restarted from zero in a respawned runner. Same discipline as
14478
- * `LoadContribution.startedAtMs`.
14479
- */
14480
- sinceMs: number(),
14481
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14482
- atMs: number(),
14483
- /**
14484
- * THE DENOMINATOR — every attempt on this path for this camera in the
14485
- * window. A failure count published without it is the mistake this schema
14486
- * exists to make impossible.
14487
- */
14488
- attempts: number().int().nonnegative(),
14489
- /** Attempts that landed. `attempts - succeeded` is the loss. */
14490
- succeeded: number().int().nonnegative(),
14491
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
14492
- reasons: array(FailureReasonCountSchema).readonly()
14493
- });
14494
- var failureContributionCapability = {
14495
- name: "failure-contribution",
14496
- scope: "system",
14497
- mode: "collection",
14498
- internal: true,
14499
- methods: {
14500
- /**
14501
- * This addon's per-camera failure counters, read live from bounded in-RAM
14502
- * state it already keeps. Inert: no persistence, no sampling, no timer.
14503
- *
14504
- * READING NEVER RESETS. The counters are CUMULATIVE since `sinceMs`, and a
14505
- * consumer that wants a rate differences two reads. A draining read would
14506
- * make two operators with the page open each destroy half of the other's
14507
- * numbers, and `load-contribution` already settled the same question the
14508
- * same way for `cpuSeconds`.
14509
- */
14510
- list: method(_void(), array(FailureContributionSchema).readonly()) },
14511
- /** In-process only — enumerated through `addons.listCapabilityProviders`. */
14512
- mount: { kind: "skip" }
14513
- };
14514
- var LoadContributionSchema = object({
14515
- role: _enum([
14516
- "decode",
14517
- "transcode",
14518
- "recording",
14519
- "streaming",
14520
- "detection"
14521
- ]),
14522
- /**
14523
- * The NUMERIC device id — the same value every log line carries as
14524
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14525
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
14526
- * contributor that cannot name its camera must not emit the entry at all,
14527
- * because an unnamed per-camera entry is indistinguishable from a shared one
14528
- * and would quietly turn one camera's cost into everybody's.
14529
- */
14530
- deviceId: number().int().positive().nullable(),
14531
- attribution: _enum([
14532
- "measured",
14533
- "accounted",
14534
- "unattributable"
14535
- ]),
14536
- /**
14537
- * What ONE entry is, in the contributor's own words — `615/high`,
14538
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14539
- * family and inventing a common one would lose the only information that
14540
- * makes two entries for the same camera distinguishable.
14541
- */
14542
- unit: string(),
14543
- /**
14544
- * The OS process this cost lives in, when there is one. Present so a
14545
- * consumer can (a) tell two generations of the same unit apart across a
14546
- * restart, and (b) subtract claimed processes from the node's process
14547
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14548
- * process of its own.
14549
- */
14550
- pid: number().int().positive().optional(),
14551
- /**
14552
- * When this generation started. The pid's incarnation marker: a consumer
14553
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14554
- * window when this changes, because the counter restarted from zero in a new
14555
- * process.
14556
- */
14557
- startedAtMs: number().optional(),
14558
- /**
14559
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14560
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
14561
- * contribution is asked for.
14562
- *
14563
- * Cumulative and not a rate on purpose: a rate needs a window, a window
14564
- * needs a sampler, and a new per-node sampler is the defect half of
14565
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14566
- * by whoever already keeps a history; a rate cannot be un-averaged.
14567
- *
14568
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14569
- * an entry with no process.
14570
- */
14571
- cpuSeconds: number().optional(),
14572
- /** Resident bytes of this unit's process, same source and same rules. */
14573
- rssBytes: number().optional()
14574
- });
14575
- method(_void(), array(LoadContributionSchema).readonly());
14576
- /**
14577
14577
  * `login-method` — collection cap through which auth addons contribute
14578
14578
  * their pre-auth login surfaces to the login page. This is the SINGLE,
14579
14579
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -19311,12 +19311,53 @@ var MediaFileKindEnum = _enum([
19311
19311
  "keyFrameSmall",
19312
19312
  "thumbnailSmall"
19313
19313
  ]);
19314
+ /**
19315
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
19316
+ * ARE — never the bytes themselves.
19317
+ *
19318
+ * ## Why `url` and not `base64`
19319
+ *
19320
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
19321
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
19322
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
19323
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
19324
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
19325
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
19326
+ *
19327
+ * `url` points at the `event-media` data plane
19328
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
19329
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
19330
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
19331
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
19332
+ * no less protected than they were inside a `view`-level cap response — see
19333
+ * `data-plane-access.ts` for the rule and the one gap it does not close
19334
+ * (per-device scoping).
19335
+ *
19336
+ * The URL is built from the row's **stored** key, which is not always its
19337
+ * published `kind`: a track's face/plate crop is stored as `crop` under
19338
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
19339
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
19340
+ *
19341
+ * ## `base64` is TRANSITIONAL and is going away
19342
+ *
19343
+ * It is still populated for one reason: the deployed viewer's track-detail
19344
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
19345
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
19346
+ * triangle — not as absence. Removing the field before that viewer ships is an
19347
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
19348
+ * delete this line and the `withBytes` pass-through in
19349
+ * `analytics-query-facade.ts`; nothing else reads it.
19350
+ */
19314
19351
  var MediaFileSchema = object({
19315
19352
  key: string(),
19316
19353
  kind: MediaFileKindEnum,
19317
- base64: string(),
19318
19354
  sizeBytes: number(),
19319
19355
  timestamp: number()
19356
+ }).extend({
19357
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
19358
+ url: string(),
19359
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
19360
+ base64: string()
19320
19361
  });
19321
19362
  /**
19322
19363
  * One media row WITHOUT its bytes.
@@ -19328,7 +19369,9 @@ var MediaFileSchema = object({
19328
19369
  * blocks the whole view.
19329
19370
  *
19330
19371
  * `sizeBytes` is carried because it is what lets a client decide between the
19331
- * stored blob and a `?variant=thumb` rendering without fetching either.
19372
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
19373
+ * `url` because a client that had to build the plane path itself is a second
19374
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
19332
19375
  */
19333
19376
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
19334
19377
  /**
@@ -19675,6 +19718,50 @@ var EventStoreFootprintSchema = object({
19675
19718
  totalBytes: number().int(),
19676
19719
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19677
19720
  });
19721
+ /** Event-media footprint for one {@link MediaFileKind}. */
19722
+ var EventMediaKindFootprintSchema = object({
19723
+ kind: MediaFileKindEnum,
19724
+ /** Media rows of this kind. */
19725
+ rows: number().int(),
19726
+ /** Bytes on disk held by those rows. */
19727
+ bytes: number().int()
19728
+ });
19729
+ /**
19730
+ * The media footprint broken down by KIND — the axis a deletion decision
19731
+ * actually turns on.
19732
+ *
19733
+ * A byte total says how much there is; it cannot say what is safe to remove.
19734
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19735
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19736
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19737
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19738
+ * nothing else, so sizing a deletion means summing per kind.
19739
+ *
19740
+ * ## Why `unaccounted*` exists
19741
+ *
19742
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19743
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19744
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19745
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19746
+ * retired code path, or by a version that knew a kind this one does not) would
19747
+ * otherwise vanish from the total silently, and an operator would delete
19748
+ * against a denominator smaller than the disk.
19749
+ *
19750
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19751
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19752
+ */
19753
+ var EventMediaKindBreakdownSchema = object({
19754
+ /** Every media row in scope, from one unfiltered aggregate. */
19755
+ totalRows: number().int(),
19756
+ /** Every media byte in scope, from that same aggregate. */
19757
+ totalBytes: number().int(),
19758
+ /** Per-kind footprint, ordered by bytes descending. */
19759
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19760
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19761
+ unaccountedRows: number().int(),
19762
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19763
+ unaccountedBytes: number().int()
19764
+ });
19678
19765
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19679
19766
  var EventPruneCountsSchema = object({
19680
19767
  motion: number().int(),
@@ -19878,6 +19965,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19878
19965
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19879
19966
  kind: "query",
19880
19967
  auth: "admin"
19968
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19969
+ kind: "query",
19970
+ auth: "admin"
19881
19971
  }), method(object({
19882
19972
  olderThanMs: number(),
19883
19973
  reason: OpsLogReasonSchema.optional()
@@ -20017,6 +20107,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20017
20107
  }), array(MediaFileSchema).readonly()), method(object({
20018
20108
  trackId: string(),
20019
20109
  deviceId: number()
20110
+ }), array(MediaFileInfoSchema).readonly()), method(object({
20111
+ eventId: string(),
20112
+ deviceId: number()
20020
20113
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
20021
20114
  kind: "mutation",
20022
20115
  auth: "admin"
@@ -21930,6 +22023,20 @@ method(object({
21930
22023
  error: string().optional()
21931
22024
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21932
22025
  providerId: string(),
22026
+ /**
22027
+ * The location this config is an UNSAVED edit of, when there is one.
22028
+ *
22029
+ * `listLocations` replaces every declared secret with the redaction
22030
+ * sentinel, so the edit modal's form state holds the sentinel for any
22031
+ * credential the operator did not retype — and posting that here
22032
+ * without a way to resolve it makes the provider try to authenticate
22033
+ * as `__camstack_redacted__` and report the operator's own working
22034
+ * password as wrong. Given this id, the orchestrator restores each
22035
+ * sentinel from the stored config (same rule as `upsertLocation`)
22036
+ * before dispatching. Omitted by the "Add location" wizard, where
22037
+ * every value was typed just now and nothing is stored yet.
22038
+ */
22039
+ locationId: string().optional(),
21933
22040
  config: record(string(), unknown())
21934
22041
  }), object({
21935
22042
  ok: boolean(),
@@ -25111,10 +25218,24 @@ var FaceClusterSchema = object({
25111
25218
  size: number().int(),
25112
25219
  cohesion: number()
25113
25220
  });
25221
+ /**
25222
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25223
+ * are — never the bytes.
25224
+ *
25225
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25226
+ * track/event contract) is still populated because a deployed viewer requires
25227
+ * the field to parse a row at all; this method has no such reader. Its ONE
25228
+ * caller is the admin UI's detail modal, which was building
25229
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25230
+ * dialog already rendering its key FRAME from the `event-media` plane.
25231
+ *
25232
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25233
+ * media key directly, so this needed no new plane and no new access decision.
25234
+ */
25114
25235
  var MediaFileLiteSchema$1 = object({
25115
25236
  key: string(),
25116
25237
  kind: string(),
25117
- base64: string(),
25238
+ url: string(),
25118
25239
  sizeBytes: number(),
25119
25240
  timestamp: number()
25120
25241
  });
@@ -28136,10 +28257,24 @@ var PlateInfoSchema = object({
28136
28257
  */
28137
28258
  cropUrl: string().optional()
28138
28259
  });
28260
+ /**
28261
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
28262
+ * are — never the bytes.
28263
+ *
28264
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
28265
+ * track/event contract) is still populated because a deployed viewer requires
28266
+ * the field to parse a row at all; this method has no such reader. Its ONE
28267
+ * caller is the admin UI's detail modal, which was building
28268
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
28269
+ * dialog already rendering its key FRAME from the `event-media` plane.
28270
+ *
28271
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
28272
+ * media key directly, so this needed no new plane and no new access decision.
28273
+ */
28139
28274
  var MediaFileLiteSchema = object({
28140
28275
  key: string(),
28141
28276
  kind: string(),
28142
- base64: string(),
28277
+ url: string(),
28143
28278
  sizeBytes: number(),
28144
28279
  timestamp: number()
28145
28280
  });
@@ -36392,6 +36527,12 @@ Object.freeze({
36392
36527
  addonId: null,
36393
36528
  access: "view"
36394
36529
  },
36530
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
36531
+ capName: "pipeline-analytics",
36532
+ capScope: "device",
36533
+ addonId: null,
36534
+ access: "view"
36535
+ },
36395
36536
  "pipelineAnalytics.getEventStoreFootprint": {
36396
36537
  capName: "pipeline-analytics",
36397
36538
  capScope: "device",
@@ -36488,6 +36629,12 @@ Object.freeze({
36488
36629
  addonId: null,
36489
36630
  access: "view"
36490
36631
  },
36632
+ "pipelineAnalytics.listEventMedia": {
36633
+ capName: "pipeline-analytics",
36634
+ capScope: "device",
36635
+ addonId: null,
36636
+ access: "view"
36637
+ },
36491
36638
  "pipelineAnalytics.listGroups": {
36492
36639
  capName: "pipeline-analytics",
36493
36640
  capScope: "device",
@@ -40051,6 +40198,11 @@ Object.freeze({
40051
40198
  form: "single",
40052
40199
  optional: false
40053
40200
  }],
40201
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
40202
+ name: "deviceId",
40203
+ form: "single",
40204
+ optional: true
40205
+ }],
40054
40206
  "pipelineAnalytics.getGroup": [{
40055
40207
  name: "deviceId",
40056
40208
  form: "single",
@@ -40111,6 +40263,11 @@ Object.freeze({
40111
40263
  form: "array",
40112
40264
  optional: false
40113
40265
  }],
40266
+ "pipelineAnalytics.listEventMedia": [{
40267
+ name: "deviceId",
40268
+ form: "single",
40269
+ optional: false
40270
+ }],
40114
40271
  "pipelineAnalytics.listGroups": [{
40115
40272
  name: "deviceIds",
40116
40273
  form: "array",
package/dist/addon.mjs CHANGED
@@ -13762,6 +13762,133 @@ method(object({
13762
13762
  height: number()
13763
13763
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13764
13764
  /**
13765
+ * `failure-contribution` — the capability an addon reports its OWN losses
13766
+ * through, per camera, with the denominator attached. It stores nothing.
13767
+ *
13768
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13769
+ *
13770
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13771
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13772
+ * copied: the contributor reports what it already knows, hub-main adds only
13773
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13774
+ * somebody to forget to edit.
13775
+ *
13776
+ * They are not merged, because their invariants are opposites:
13777
+ *
13778
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13779
+ * claim a camera cost nothing, which is a measurement nobody made;
13780
+ * - a `failure-contribution` zero is the **most valuable value on the
13781
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13782
+ * and it is exactly what an absent entry cannot say.
13783
+ *
13784
+ * Putting a loss counter on a cost entry would also break the reconciliation
13785
+ * that gives `load-contribution` its point: contributions are subtracted from
13786
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13787
+ * has no process.
13788
+ *
13789
+ * ## Why not a log line, since the counters already exist
13790
+ *
13791
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13792
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13793
+ * ends in a log line, and a log line is the thing the operator asked to stop
13794
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13795
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13796
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13797
+ * media blackout were both diagnosed. The counters stay; this is where they can
13798
+ * be READ.
13799
+ *
13800
+ * ## The rate is served with its denominator or not at all
13801
+ *
13802
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13803
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13804
+ * than yesterday" and was **flat across twelve hours** once divided by the
13805
+ * successes on the same path. A surface that publishes only the numerator
13806
+ * reproduces that mistake on every read.
13807
+ *
13808
+ * ## Shape
13809
+ *
13810
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13811
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13812
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13813
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13814
+ * a forked runner's entries reach hub-main over transport that already exists.
13815
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13816
+ * result through `system.getFailureContributions`.
13817
+ */
13818
+ var FailureReasonCountSchema = object({
13819
+ /**
13820
+ * Why the attempt did not land, in the contributor's own vocabulary —
13821
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13822
+ * strings that already appear in this repo's logs and, where one exists, the
13823
+ * same string the per-track `previewMissReason` records (D276): a second
13824
+ * vocabulary for the same loss would make the row and the counter
13825
+ * un-joinable.
13826
+ */
13827
+ reason: string(),
13828
+ count: number().int().nonnegative()
13829
+ });
13830
+ var FailureContributionSchema = object({
13831
+ /**
13832
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13833
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13834
+ * `unit` free: the families are owned by different addons and a shared enum
13835
+ * is a central list that rots invisibly.
13836
+ */
13837
+ family: string(),
13838
+ /**
13839
+ * The NUMERIC device id — the same value every log line carries as
13840
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13841
+ * cannot name the camera must not emit the entry, because a fleet total
13842
+ * cannot answer the only question anybody asks of this surface.
13843
+ */
13844
+ deviceId: number().int().positive(),
13845
+ /**
13846
+ * A second dimension inside the family: the model / step id for an inference
13847
+ * timeout, so "which camera AND which model" is one read. Absent when the
13848
+ * family has a single variant.
13849
+ */
13850
+ variant: string().optional(),
13851
+ /**
13852
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13853
+ * differencing two reads must drop the interval when it changes, because the
13854
+ * counter restarted from zero in a respawned runner. Same discipline as
13855
+ * `LoadContribution.startedAtMs`.
13856
+ */
13857
+ sinceMs: number(),
13858
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13859
+ atMs: number(),
13860
+ /**
13861
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13862
+ * window. A failure count published without it is the mistake this schema
13863
+ * exists to make impossible.
13864
+ */
13865
+ attempts: number().int().nonnegative(),
13866
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13867
+ succeeded: number().int().nonnegative(),
13868
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13869
+ reasons: array(FailureReasonCountSchema).readonly()
13870
+ });
13871
+ var failureContributionCapability = {
13872
+ name: "failure-contribution",
13873
+ scope: "system",
13874
+ mode: "collection",
13875
+ internal: true,
13876
+ methods: {
13877
+ /**
13878
+ * This addon's per-camera failure counters, read live from bounded in-RAM
13879
+ * state it already keeps. Inert: no persistence, no sampling, no timer.
13880
+ *
13881
+ * READING NEVER RESETS. The counters are CUMULATIVE since `sinceMs`, and a
13882
+ * consumer that wants a rate differences two reads. A draining read would
13883
+ * make two operators with the page open each destroy half of the other's
13884
+ * numbers, and `load-contribution` already settled the same question the
13885
+ * same way for `cpuSeconds`.
13886
+ */
13887
+ list: method(_void(), array(FailureContributionSchema).readonly()) },
13888
+ /** In-process only — enumerated through `addons.listCapabilityProviders`. */
13889
+ mount: { kind: "skip" }
13890
+ };
13891
+ /**
13765
13892
  * filesystem-browse — per-node capability for browsing the node's local
13766
13893
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13767
13894
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -14283,6 +14410,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
14283
14410
  kind: "mutation",
14284
14411
  auth: "admin"
14285
14412
  });
14413
+ var LoadContributionSchema = object({
14414
+ role: _enum([
14415
+ "decode",
14416
+ "transcode",
14417
+ "recording",
14418
+ "streaming",
14419
+ "detection"
14420
+ ]),
14421
+ /**
14422
+ * The NUMERIC device id — the same value every log line carries as
14423
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14424
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
14425
+ * contributor that cannot name its camera must not emit the entry at all,
14426
+ * because an unnamed per-camera entry is indistinguishable from a shared one
14427
+ * and would quietly turn one camera's cost into everybody's.
14428
+ */
14429
+ deviceId: number().int().positive().nullable(),
14430
+ attribution: _enum([
14431
+ "measured",
14432
+ "accounted",
14433
+ "unattributable"
14434
+ ]),
14435
+ /**
14436
+ * What ONE entry is, in the contributor's own words — `615/high`,
14437
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14438
+ * family and inventing a common one would lose the only information that
14439
+ * makes two entries for the same camera distinguishable.
14440
+ */
14441
+ unit: string(),
14442
+ /**
14443
+ * The OS process this cost lives in, when there is one. Present so a
14444
+ * consumer can (a) tell two generations of the same unit apart across a
14445
+ * restart, and (b) subtract claimed processes from the node's process
14446
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14447
+ * process of its own.
14448
+ */
14449
+ pid: number().int().positive().optional(),
14450
+ /**
14451
+ * When this generation started. The pid's incarnation marker: a consumer
14452
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14453
+ * window when this changes, because the counter restarted from zero in a new
14454
+ * process.
14455
+ */
14456
+ startedAtMs: number().optional(),
14457
+ /**
14458
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14459
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
14460
+ * contribution is asked for.
14461
+ *
14462
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
14463
+ * needs a sampler, and a new per-node sampler is the defect half of
14464
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14465
+ * by whoever already keeps a history; a rate cannot be un-averaged.
14466
+ *
14467
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14468
+ * an entry with no process.
14469
+ */
14470
+ cpuSeconds: number().optional(),
14471
+ /** Resident bytes of this unit's process, same source and same rules. */
14472
+ rssBytes: number().optional()
14473
+ });
14474
+ method(_void(), array(LoadContributionSchema).readonly());
14286
14475
  /**
14287
14476
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
14288
14477
  * through. It stores nothing.
@@ -14380,195 +14569,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
14380
14569
  tags: record(string(), string()).optional()
14381
14570
  }), array(LogEntrySchema).readonly());
14382
14571
  /**
14383
- * `failure-contribution` — the capability an addon reports its OWN losses
14384
- * through, per camera, with the denominator attached. It stores nothing.
14385
- *
14386
- * ## The twin of `load-contribution`, and why it is a twin and not a field
14387
- *
14388
- * `load-contribution` answers *what did this camera COST*. This answers *what
14389
- * did this camera LOSE*. The reporting discipline is identical and deliberately
14390
- * copied: the contributor reports what it already knows, hub-main adds only
14391
- * `addonId`, nothing needs global knowledge, and there is no central list for
14392
- * somebody to forget to edit.
14393
- *
14394
- * They are not merged, because their invariants are opposites:
14395
- *
14396
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
14397
- * claim a camera cost nothing, which is a measurement nobody made;
14398
- * - a `failure-contribution` zero is the **most valuable value on the
14399
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14400
- * and it is exactly what an absent entry cannot say.
14401
- *
14402
- * Putting a loss counter on a cost entry would also break the reconciliation
14403
- * that gives `load-contribution` its point: contributions are subtracted from
14404
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14405
- * has no process.
14406
- *
14407
- * ## Why not a log line, since the counters already exist
14408
- *
14409
- * Several of these paths already counted themselves — `CaptureScheduler`'s
14410
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14411
- * ends in a log line, and a log line is the thing the operator asked to stop
14412
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14413
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14414
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14415
- * media blackout were both diagnosed. The counters stay; this is where they can
14416
- * be READ.
14417
- *
14418
- * ## The rate is served with its denominator or not at all
14419
- *
14420
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
14421
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14422
- * than yesterday" and was **flat across twelve hours** once divided by the
14423
- * successes on the same path. A surface that publishes only the numerator
14424
- * reproduces that mistake on every read.
14425
- *
14426
- * ## Shape
14427
- *
14428
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14429
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14430
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14431
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14432
- * a forked runner's entries reach hub-main over transport that already exists.
14433
- * No new UDS message, no second registry (D3). The operator reads the assembled
14434
- * result through `system.getFailureContributions`.
14435
- */
14436
- var FailureReasonCountSchema = object({
14437
- /**
14438
- * Why the attempt did not land, in the contributor's own vocabulary —
14439
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14440
- * strings that already appear in this repo's logs and, where one exists, the
14441
- * same string the per-track `previewMissReason` records (D276): a second
14442
- * vocabulary for the same loss would make the row and the counter
14443
- * un-joinable.
14444
- */
14445
- reason: string(),
14446
- count: number().int().nonnegative()
14447
- });
14448
- var FailureContributionSchema = object({
14449
- /**
14450
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14451
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14452
- * `unit` free: the families are owned by different addons and a shared enum
14453
- * is a central list that rots invisibly.
14454
- */
14455
- family: string(),
14456
- /**
14457
- * The NUMERIC device id — the same value every log line carries as
14458
- * `tags.deviceId`. Never nullable and never absent: a contributor that
14459
- * cannot name the camera must not emit the entry, because a fleet total
14460
- * cannot answer the only question anybody asks of this surface.
14461
- */
14462
- deviceId: number().int().positive(),
14463
- /**
14464
- * A second dimension inside the family: the model / step id for an inference
14465
- * timeout, so "which camera AND which model" is one read. Absent when the
14466
- * family has a single variant.
14467
- */
14468
- variant: string().optional(),
14469
- /**
14470
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14471
- * differencing two reads must drop the interval when it changes, because the
14472
- * counter restarted from zero in a respawned runner. Same discipline as
14473
- * `LoadContribution.startedAtMs`.
14474
- */
14475
- sinceMs: number(),
14476
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14477
- atMs: number(),
14478
- /**
14479
- * THE DENOMINATOR — every attempt on this path for this camera in the
14480
- * window. A failure count published without it is the mistake this schema
14481
- * exists to make impossible.
14482
- */
14483
- attempts: number().int().nonnegative(),
14484
- /** Attempts that landed. `attempts - succeeded` is the loss. */
14485
- succeeded: number().int().nonnegative(),
14486
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
14487
- reasons: array(FailureReasonCountSchema).readonly()
14488
- });
14489
- var failureContributionCapability = {
14490
- name: "failure-contribution",
14491
- scope: "system",
14492
- mode: "collection",
14493
- internal: true,
14494
- methods: {
14495
- /**
14496
- * This addon's per-camera failure counters, read live from bounded in-RAM
14497
- * state it already keeps. Inert: no persistence, no sampling, no timer.
14498
- *
14499
- * READING NEVER RESETS. The counters are CUMULATIVE since `sinceMs`, and a
14500
- * consumer that wants a rate differences two reads. A draining read would
14501
- * make two operators with the page open each destroy half of the other's
14502
- * numbers, and `load-contribution` already settled the same question the
14503
- * same way for `cpuSeconds`.
14504
- */
14505
- list: method(_void(), array(FailureContributionSchema).readonly()) },
14506
- /** In-process only — enumerated through `addons.listCapabilityProviders`. */
14507
- mount: { kind: "skip" }
14508
- };
14509
- var LoadContributionSchema = object({
14510
- role: _enum([
14511
- "decode",
14512
- "transcode",
14513
- "recording",
14514
- "streaming",
14515
- "detection"
14516
- ]),
14517
- /**
14518
- * The NUMERIC device id — the same value every log line carries as
14519
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14520
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
14521
- * contributor that cannot name its camera must not emit the entry at all,
14522
- * because an unnamed per-camera entry is indistinguishable from a shared one
14523
- * and would quietly turn one camera's cost into everybody's.
14524
- */
14525
- deviceId: number().int().positive().nullable(),
14526
- attribution: _enum([
14527
- "measured",
14528
- "accounted",
14529
- "unattributable"
14530
- ]),
14531
- /**
14532
- * What ONE entry is, in the contributor's own words — `615/high`,
14533
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14534
- * family and inventing a common one would lose the only information that
14535
- * makes two entries for the same camera distinguishable.
14536
- */
14537
- unit: string(),
14538
- /**
14539
- * The OS process this cost lives in, when there is one. Present so a
14540
- * consumer can (a) tell two generations of the same unit apart across a
14541
- * restart, and (b) subtract claimed processes from the node's process
14542
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14543
- * process of its own.
14544
- */
14545
- pid: number().int().positive().optional(),
14546
- /**
14547
- * When this generation started. The pid's incarnation marker: a consumer
14548
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14549
- * window when this changes, because the counter restarted from zero in a new
14550
- * process.
14551
- */
14552
- startedAtMs: number().optional(),
14553
- /**
14554
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14555
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
14556
- * contribution is asked for.
14557
- *
14558
- * Cumulative and not a rate on purpose: a rate needs a window, a window
14559
- * needs a sampler, and a new per-node sampler is the defect half of
14560
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14561
- * by whoever already keeps a history; a rate cannot be un-averaged.
14562
- *
14563
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14564
- * an entry with no process.
14565
- */
14566
- cpuSeconds: number().optional(),
14567
- /** Resident bytes of this unit's process, same source and same rules. */
14568
- rssBytes: number().optional()
14569
- });
14570
- method(_void(), array(LoadContributionSchema).readonly());
14571
- /**
14572
14572
  * `login-method` — collection cap through which auth addons contribute
14573
14573
  * their pre-auth login surfaces to the login page. This is the SINGLE,
14574
14574
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -19306,12 +19306,53 @@ var MediaFileKindEnum = _enum([
19306
19306
  "keyFrameSmall",
19307
19307
  "thumbnailSmall"
19308
19308
  ]);
19309
+ /**
19310
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
19311
+ * ARE — never the bytes themselves.
19312
+ *
19313
+ * ## Why `url` and not `base64`
19314
+ *
19315
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
19316
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
19317
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
19318
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
19319
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
19320
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
19321
+ *
19322
+ * `url` points at the `event-media` data plane
19323
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
19324
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
19325
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
19326
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
19327
+ * no less protected than they were inside a `view`-level cap response — see
19328
+ * `data-plane-access.ts` for the rule and the one gap it does not close
19329
+ * (per-device scoping).
19330
+ *
19331
+ * The URL is built from the row's **stored** key, which is not always its
19332
+ * published `kind`: a track's face/plate crop is stored as `crop` under
19333
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
19334
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
19335
+ *
19336
+ * ## `base64` is TRANSITIONAL and is going away
19337
+ *
19338
+ * It is still populated for one reason: the deployed viewer's track-detail
19339
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
19340
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
19341
+ * triangle — not as absence. Removing the field before that viewer ships is an
19342
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
19343
+ * delete this line and the `withBytes` pass-through in
19344
+ * `analytics-query-facade.ts`; nothing else reads it.
19345
+ */
19309
19346
  var MediaFileSchema = object({
19310
19347
  key: string(),
19311
19348
  kind: MediaFileKindEnum,
19312
- base64: string(),
19313
19349
  sizeBytes: number(),
19314
19350
  timestamp: number()
19351
+ }).extend({
19352
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
19353
+ url: string(),
19354
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
19355
+ base64: string()
19315
19356
  });
19316
19357
  /**
19317
19358
  * One media row WITHOUT its bytes.
@@ -19323,7 +19364,9 @@ var MediaFileSchema = object({
19323
19364
  * blocks the whole view.
19324
19365
  *
19325
19366
  * `sizeBytes` is carried because it is what lets a client decide between the
19326
- * stored blob and a `?variant=thumb` rendering without fetching either.
19367
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
19368
+ * `url` because a client that had to build the plane path itself is a second
19369
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
19327
19370
  */
19328
19371
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
19329
19372
  /**
@@ -19670,6 +19713,50 @@ var EventStoreFootprintSchema = object({
19670
19713
  totalBytes: number().int(),
19671
19714
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19672
19715
  });
19716
+ /** Event-media footprint for one {@link MediaFileKind}. */
19717
+ var EventMediaKindFootprintSchema = object({
19718
+ kind: MediaFileKindEnum,
19719
+ /** Media rows of this kind. */
19720
+ rows: number().int(),
19721
+ /** Bytes on disk held by those rows. */
19722
+ bytes: number().int()
19723
+ });
19724
+ /**
19725
+ * The media footprint broken down by KIND — the axis a deletion decision
19726
+ * actually turns on.
19727
+ *
19728
+ * A byte total says how much there is; it cannot say what is safe to remove.
19729
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19730
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19731
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19732
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19733
+ * nothing else, so sizing a deletion means summing per kind.
19734
+ *
19735
+ * ## Why `unaccounted*` exists
19736
+ *
19737
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19738
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19739
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19740
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19741
+ * retired code path, or by a version that knew a kind this one does not) would
19742
+ * otherwise vanish from the total silently, and an operator would delete
19743
+ * against a denominator smaller than the disk.
19744
+ *
19745
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19746
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19747
+ */
19748
+ var EventMediaKindBreakdownSchema = object({
19749
+ /** Every media row in scope, from one unfiltered aggregate. */
19750
+ totalRows: number().int(),
19751
+ /** Every media byte in scope, from that same aggregate. */
19752
+ totalBytes: number().int(),
19753
+ /** Per-kind footprint, ordered by bytes descending. */
19754
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19755
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19756
+ unaccountedRows: number().int(),
19757
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19758
+ unaccountedBytes: number().int()
19759
+ });
19673
19760
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19674
19761
  var EventPruneCountsSchema = object({
19675
19762
  motion: number().int(),
@@ -19873,6 +19960,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19873
19960
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19874
19961
  kind: "query",
19875
19962
  auth: "admin"
19963
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19964
+ kind: "query",
19965
+ auth: "admin"
19876
19966
  }), method(object({
19877
19967
  olderThanMs: number(),
19878
19968
  reason: OpsLogReasonSchema.optional()
@@ -20012,6 +20102,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20012
20102
  }), array(MediaFileSchema).readonly()), method(object({
20013
20103
  trackId: string(),
20014
20104
  deviceId: number()
20105
+ }), array(MediaFileInfoSchema).readonly()), method(object({
20106
+ eventId: string(),
20107
+ deviceId: number()
20015
20108
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
20016
20109
  kind: "mutation",
20017
20110
  auth: "admin"
@@ -21925,6 +22018,20 @@ method(object({
21925
22018
  error: string().optional()
21926
22019
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21927
22020
  providerId: string(),
22021
+ /**
22022
+ * The location this config is an UNSAVED edit of, when there is one.
22023
+ *
22024
+ * `listLocations` replaces every declared secret with the redaction
22025
+ * sentinel, so the edit modal's form state holds the sentinel for any
22026
+ * credential the operator did not retype — and posting that here
22027
+ * without a way to resolve it makes the provider try to authenticate
22028
+ * as `__camstack_redacted__` and report the operator's own working
22029
+ * password as wrong. Given this id, the orchestrator restores each
22030
+ * sentinel from the stored config (same rule as `upsertLocation`)
22031
+ * before dispatching. Omitted by the "Add location" wizard, where
22032
+ * every value was typed just now and nothing is stored yet.
22033
+ */
22034
+ locationId: string().optional(),
21928
22035
  config: record(string(), unknown())
21929
22036
  }), object({
21930
22037
  ok: boolean(),
@@ -25106,10 +25213,24 @@ var FaceClusterSchema = object({
25106
25213
  size: number().int(),
25107
25214
  cohesion: number()
25108
25215
  });
25216
+ /**
25217
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25218
+ * are — never the bytes.
25219
+ *
25220
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25221
+ * track/event contract) is still populated because a deployed viewer requires
25222
+ * the field to parse a row at all; this method has no such reader. Its ONE
25223
+ * caller is the admin UI's detail modal, which was building
25224
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25225
+ * dialog already rendering its key FRAME from the `event-media` plane.
25226
+ *
25227
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25228
+ * media key directly, so this needed no new plane and no new access decision.
25229
+ */
25109
25230
  var MediaFileLiteSchema$1 = object({
25110
25231
  key: string(),
25111
25232
  kind: string(),
25112
- base64: string(),
25233
+ url: string(),
25113
25234
  sizeBytes: number(),
25114
25235
  timestamp: number()
25115
25236
  });
@@ -28131,10 +28252,24 @@ var PlateInfoSchema = object({
28131
28252
  */
28132
28253
  cropUrl: string().optional()
28133
28254
  });
28255
+ /**
28256
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
28257
+ * are — never the bytes.
28258
+ *
28259
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
28260
+ * track/event contract) is still populated because a deployed viewer requires
28261
+ * the field to parse a row at all; this method has no such reader. Its ONE
28262
+ * caller is the admin UI's detail modal, which was building
28263
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
28264
+ * dialog already rendering its key FRAME from the `event-media` plane.
28265
+ *
28266
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
28267
+ * media key directly, so this needed no new plane and no new access decision.
28268
+ */
28134
28269
  var MediaFileLiteSchema = object({
28135
28270
  key: string(),
28136
28271
  kind: string(),
28137
- base64: string(),
28272
+ url: string(),
28138
28273
  sizeBytes: number(),
28139
28274
  timestamp: number()
28140
28275
  });
@@ -36387,6 +36522,12 @@ Object.freeze({
36387
36522
  addonId: null,
36388
36523
  access: "view"
36389
36524
  },
36525
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
36526
+ capName: "pipeline-analytics",
36527
+ capScope: "device",
36528
+ addonId: null,
36529
+ access: "view"
36530
+ },
36390
36531
  "pipelineAnalytics.getEventStoreFootprint": {
36391
36532
  capName: "pipeline-analytics",
36392
36533
  capScope: "device",
@@ -36483,6 +36624,12 @@ Object.freeze({
36483
36624
  addonId: null,
36484
36625
  access: "view"
36485
36626
  },
36627
+ "pipelineAnalytics.listEventMedia": {
36628
+ capName: "pipeline-analytics",
36629
+ capScope: "device",
36630
+ addonId: null,
36631
+ access: "view"
36632
+ },
36486
36633
  "pipelineAnalytics.listGroups": {
36487
36634
  capName: "pipeline-analytics",
36488
36635
  capScope: "device",
@@ -40046,6 +40193,11 @@ Object.freeze({
40046
40193
  form: "single",
40047
40194
  optional: false
40048
40195
  }],
40196
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
40197
+ name: "deviceId",
40198
+ form: "single",
40199
+ optional: true
40200
+ }],
40049
40201
  "pipelineAnalytics.getGroup": [{
40050
40202
  name: "deviceId",
40051
40203
  form: "single",
@@ -40106,6 +40258,11 @@ Object.freeze({
40106
40258
  form: "array",
40107
40259
  optional: false
40108
40260
  }],
40261
+ "pipelineAnalytics.listEventMedia": [{
40262
+ name: "deviceId",
40263
+ form: "single",
40264
+ optional: false
40265
+ }],
40109
40266
  "pipelineAnalytics.listGroups": [{
40110
40267
  name: "deviceIds",
40111
40268
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.69",
3
+ "version": "1.2.71",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",