@camstack/addon-osd-manager 0.1.38 → 0.1.40

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 (18) hide show
  1. package/dist/{MotionZonesSettings-emdN0UNh.mjs → MotionZonesSettings-BTOqeXRd.mjs} +2 -2
  2. package/dist/{PrivacyMaskSettings-CrGo7BYZ.mjs → PrivacyMaskSettings-BgmE0tQu.mjs} +4 -4
  3. package/dist/{SceneMonitorEditor-_K9y826_.mjs → SceneMonitorEditor-D4oig7v9.mjs} +3 -3
  4. package/dist/_stub.js +11 -11
  5. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-C1HPQGS9.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-C4iHNnmw.mjs} +4 -4
  6. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Blhr4EyN.mjs +26 -0
  7. package/dist/{hostInit-D49Ppk5T.mjs → hostInit-DzLQY4CO.mjs} +3 -3
  8. package/dist/index.js +417 -227
  9. package/dist/index.mjs +417 -227
  10. package/dist/{player-overlays-DURd3AGI.mjs → player-overlays-DaucHym7.mjs} +1 -1
  11. package/dist/remoteEntry.js +1 -1
  12. package/dist/{responsive-DIYWmiTV.mjs → responsive-DC2YNF2r.mjs} +1 -1
  13. package/dist/{square-pJqxPNpb.mjs → square-DuMw_5RG.mjs} +1 -1
  14. package/dist/{trash-2-DgzntFMe.mjs → trash-2-C54qifas.mjs} +1 -1
  15. package/dist/{use-device-snapshot-C-TbesfB.mjs → use-device-snapshot-BVTu9rWV.mjs} +1 -1
  16. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-ku4Amehr.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-B-XuFKdU.mjs} +1 -1
  17. package/package.json +1 -1
  18. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DuDBHu79.mjs +0 -26
package/dist/index.js CHANGED
@@ -8102,6 +8102,21 @@ var RelocateJobSchema = object({
8102
8102
  bytesMoved: number().int(),
8103
8103
  /** Total files discovered up front; null while (or when) unknown. */
8104
8104
  filesTotal: number().int().nullable(),
8105
+ /**
8106
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8107
+ * made that nobody asked for, so it is reported where the operator reads the
8108
+ * job rather than only in a log line.
8109
+ *
8110
+ * A footage segment records its byte count in its own NAME, and the durable
8111
+ * hour row derives its aggregates from those names. A file that does not
8112
+ * match its name therefore makes the ledger's sums — and with them quota and
8113
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8114
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8115
+ *
8116
+ * Absent on lanes where the question has no meaning: a media blob's size is
8117
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8118
+ */
8119
+ rowsReconciled: number().int().nonnegative().optional(),
8105
8120
  startedAt: number(),
8106
8121
  finishedAt: number().nullable(),
8107
8122
  error: string().nullable()
@@ -8170,14 +8185,42 @@ var RelocateMediaInputSchema = object({
8170
8185
  /** Omitted = `move`, the pre-existing behaviour. */
8171
8186
  mode: MediaRelocateModeSchema.optional()
8172
8187
  });
8173
- /** How many rows still carry NO `locationId` — the population a repoint would
8174
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8175
- * value that permits a non-blocking `eventMedia` cutover. */
8176
- var UnstampedEventMediaCountSchema = object({
8177
- media: number().int().nonnegative(),
8178
- retrainFrames: number().int().nonnegative(),
8179
- total: number().int().nonnegative()
8188
+ /**
8189
+ * The unstamped population of ONE collection split, because the gate and the
8190
+ * operator ask two different questions and only one of them has to be cheap.
8191
+ *
8192
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8193
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8194
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8195
+ * that matters — after a seal, when the population is empty.
8196
+ *
8197
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8198
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8199
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8200
+ * and useful answer: "there are some, and this read could not say how many"
8201
+ * still refuses the cutover, which is the whole job.
8202
+ */
8203
+ var UnstampedRowsSchema = object({
8204
+ present: boolean(),
8205
+ rows: number().int().nonnegative().nullable()
8180
8206
  });
8207
+ /**
8208
+ * How many rows still carry NO `locationId` — the population a repoint would
8209
+ * silently re-aim at a disk that does not hold their bytes.
8210
+ *
8211
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8212
+ * over. The gate opens on a measured absence and on nothing else; an unread
8213
+ * collection and an empty one are different facts, and this repo has already
8214
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8215
+ */
8216
+ var UnstampedEventMediaCountSchema = object({
8217
+ media: UnstampedRowsSchema,
8218
+ retrainFrames: UnstampedRowsSchema,
8219
+ /** True when EITHER collection holds one. The refusal reads this. */
8220
+ anyPresent: boolean(),
8221
+ /** Sum across both, or `null` when either lane could not be counted. */
8222
+ total: number().int().nonnegative().nullable()
8223
+ }).nullable();
8181
8224
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8182
8225
  /** The independently selectable logical storage classes — every class
8183
8226
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8288,6 +8331,10 @@ var StorageMigrationMoveProgressSchema = object({
8288
8331
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8289
8332
  filesTotal: number().int().nonnegative().nullable(),
8290
8333
  bytesMoved: number().int().nonnegative(),
8334
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8335
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8336
+ * rows would be the same failure as one that silently skipped them. */
8337
+ rowsReconciled: number().int().nonnegative().optional(),
8291
8338
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8292
8339
  * crash gets a new mover, and a rate computed from the migration's start
8293
8340
  * would silently average in the time nothing was running. */
@@ -14244,6 +14291,15 @@ var deviceManagerCapability = {
14244
14291
  * calls are sync. Bindings change rarely (only on wrapper toggle or
14245
14292
  * device add/remove) — clients invalidate via the
14246
14293
  * `capability.binding-changed` event.
14294
+ *
14295
+ * "A single round-trip" describes the CLIENT's side and used not to
14296
+ * describe the server's: until 2026-08-30 the resolver read the persisted
14297
+ * wrapper activations once per device, so answering this cost one
14298
+ * settings-door RPC per device — 1 020 on the live 1 019-device hub, and
14299
+ * it did not return in 240 s against `SystemMirror.init`'s 15 s budget.
14300
+ * The server side is now two reads for the whole fleet. Anything PERIODIC
14301
+ * still belongs on `getBindings` / `getBindingsBatch` (D12); this remains
14302
+ * a warm seed.
14247
14303
  */
14248
14304
  getAllBindings: method(object({}), array(DeviceBindingsForDeviceSchema)),
14249
14305
  /**
@@ -14672,6 +14728,133 @@ var embeddingEncoderCapability = {
14672
14728
  }
14673
14729
  };
14674
14730
  /**
14731
+ * `failure-contribution` — the capability an addon reports its OWN losses
14732
+ * through, per camera, with the denominator attached. It stores nothing.
14733
+ *
14734
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
14735
+ *
14736
+ * `load-contribution` answers *what did this camera COST*. This answers *what
14737
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
14738
+ * copied: the contributor reports what it already knows, hub-main adds only
14739
+ * `addonId`, nothing needs global knowledge, and there is no central list for
14740
+ * somebody to forget to edit.
14741
+ *
14742
+ * They are not merged, because their invariants are opposites:
14743
+ *
14744
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
14745
+ * claim a camera cost nothing, which is a measurement nobody made;
14746
+ * - a `failure-contribution` zero is the **most valuable value on the
14747
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14748
+ * and it is exactly what an absent entry cannot say.
14749
+ *
14750
+ * Putting a loss counter on a cost entry would also break the reconciliation
14751
+ * that gives `load-contribution` its point: contributions are subtracted from
14752
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14753
+ * has no process.
14754
+ *
14755
+ * ## Why not a log line, since the counters already exist
14756
+ *
14757
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
14758
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14759
+ * ends in a log line, and a log line is the thing the operator asked to stop
14760
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14761
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14762
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14763
+ * media blackout were both diagnosed. The counters stay; this is where they can
14764
+ * be READ.
14765
+ *
14766
+ * ## The rate is served with its denominator or not at all
14767
+ *
14768
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
14769
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14770
+ * than yesterday" and was **flat across twelve hours** once divided by the
14771
+ * successes on the same path. A surface that publishes only the numerator
14772
+ * reproduces that mistake on every read.
14773
+ *
14774
+ * ## Shape
14775
+ *
14776
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14777
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14778
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14779
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14780
+ * a forked runner's entries reach hub-main over transport that already exists.
14781
+ * No new UDS message, no second registry (D3). The operator reads the assembled
14782
+ * result through `system.getFailureContributions`.
14783
+ */
14784
+ var FailureReasonCountSchema = object({
14785
+ /**
14786
+ * Why the attempt did not land, in the contributor's own vocabulary —
14787
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14788
+ * strings that already appear in this repo's logs and, where one exists, the
14789
+ * same string the per-track `previewMissReason` records (D276): a second
14790
+ * vocabulary for the same loss would make the row and the counter
14791
+ * un-joinable.
14792
+ */
14793
+ reason: string(),
14794
+ count: number().int().nonnegative()
14795
+ });
14796
+ var FailureContributionSchema = object({
14797
+ /**
14798
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14799
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14800
+ * `unit` free: the families are owned by different addons and a shared enum
14801
+ * is a central list that rots invisibly.
14802
+ */
14803
+ family: string(),
14804
+ /**
14805
+ * The NUMERIC device id — the same value every log line carries as
14806
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
14807
+ * cannot name the camera must not emit the entry, because a fleet total
14808
+ * cannot answer the only question anybody asks of this surface.
14809
+ */
14810
+ deviceId: number().int().positive(),
14811
+ /**
14812
+ * A second dimension inside the family: the model / step id for an inference
14813
+ * timeout, so "which camera AND which model" is one read. Absent when the
14814
+ * family has a single variant.
14815
+ */
14816
+ variant: string().optional(),
14817
+ /**
14818
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14819
+ * differencing two reads must drop the interval when it changes, because the
14820
+ * counter restarted from zero in a respawned runner. Same discipline as
14821
+ * `LoadContribution.startedAtMs`.
14822
+ */
14823
+ sinceMs: number(),
14824
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14825
+ atMs: number(),
14826
+ /**
14827
+ * THE DENOMINATOR — every attempt on this path for this camera in the
14828
+ * window. A failure count published without it is the mistake this schema
14829
+ * exists to make impossible.
14830
+ */
14831
+ attempts: number().int().nonnegative(),
14832
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
14833
+ succeeded: number().int().nonnegative(),
14834
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
14835
+ reasons: array(FailureReasonCountSchema).readonly()
14836
+ });
14837
+ var failureContributionCapability = {
14838
+ name: "failure-contribution",
14839
+ scope: "system",
14840
+ mode: "collection",
14841
+ internal: true,
14842
+ methods: {
14843
+ /**
14844
+ * This addon's per-camera failure counters, read live from bounded in-RAM
14845
+ * state it already keeps. Inert: no persistence, no sampling, no timer.
14846
+ *
14847
+ * READING NEVER RESETS. The counters are CUMULATIVE since `sinceMs`, and a
14848
+ * consumer that wants a rate differences two reads. A draining read would
14849
+ * make two operators with the page open each destroy half of the other's
14850
+ * numbers, and `load-contribution` already settled the same question the
14851
+ * same way for `cpuSeconds`.
14852
+ */
14853
+ list: method(_void(), array(FailureContributionSchema).readonly()) },
14854
+ /** In-process only — enumerated through `addons.listCapabilityProviders`. */
14855
+ mount: { kind: "skip" }
14856
+ };
14857
+ /**
14675
14858
  * filesystem-browse — per-node capability for browsing the node's local
14676
14859
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
14677
14860
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -15282,6 +15465,82 @@ var llmCapability = {
15282
15465
  })
15283
15466
  }
15284
15467
  };
15468
+ var LoadContributionSchema = object({
15469
+ role: _enum([
15470
+ "decode",
15471
+ "transcode",
15472
+ "recording",
15473
+ "streaming",
15474
+ "detection"
15475
+ ]),
15476
+ /**
15477
+ * The NUMERIC device id — the same value every log line carries as
15478
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
15479
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
15480
+ * contributor that cannot name its camera must not emit the entry at all,
15481
+ * because an unnamed per-camera entry is indistinguishable from a shared one
15482
+ * and would quietly turn one camera's cost into everybody's.
15483
+ */
15484
+ deviceId: number().int().positive().nullable(),
15485
+ attribution: _enum([
15486
+ "measured",
15487
+ "accounted",
15488
+ "unattributable"
15489
+ ]),
15490
+ /**
15491
+ * What ONE entry is, in the contributor's own words — `615/high`,
15492
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
15493
+ * family and inventing a common one would lose the only information that
15494
+ * makes two entries for the same camera distinguishable.
15495
+ */
15496
+ unit: string(),
15497
+ /**
15498
+ * The OS process this cost lives in, when there is one. Present so a
15499
+ * consumer can (a) tell two generations of the same unit apart across a
15500
+ * restart, and (b) subtract claimed processes from the node's process
15501
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
15502
+ * process of its own.
15503
+ */
15504
+ pid: number().int().positive().optional(),
15505
+ /**
15506
+ * When this generation started. The pid's incarnation marker: a consumer
15507
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
15508
+ * window when this changes, because the counter restarted from zero in a new
15509
+ * process.
15510
+ */
15511
+ startedAtMs: number().optional(),
15512
+ /**
15513
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
15514
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
15515
+ * contribution is asked for.
15516
+ *
15517
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
15518
+ * needs a sampler, and a new per-node sampler is the defect half of
15519
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
15520
+ * by whoever already keeps a history; a rate cannot be un-averaged.
15521
+ *
15522
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
15523
+ * an entry with no process.
15524
+ */
15525
+ cpuSeconds: number().optional(),
15526
+ /** Resident bytes of this unit's process, same source and same rules. */
15527
+ rssBytes: number().optional()
15528
+ });
15529
+ var loadContributionCapability = {
15530
+ name: "load-contribution",
15531
+ scope: "system",
15532
+ mode: "collection",
15533
+ internal: true,
15534
+ methods: {
15535
+ /**
15536
+ * This addon's own cost entries, computed live from state it already
15537
+ * holds. Inert: no persistence, no sampling, no timer. It is answered on
15538
+ * whatever beat the caller already has.
15539
+ */
15540
+ list: method(_void(), array(LoadContributionSchema).readonly()) },
15541
+ /** In-process only — enumerated through `addons.listCapabilityProviders`. */
15542
+ mount: { kind: "skip" }
15543
+ };
15285
15544
  /**
15286
15545
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
15287
15546
  * through. It stores nothing.
@@ -15390,209 +15649,6 @@ var logDestinationCapability = {
15390
15649
  mount: { kind: "skip" }
15391
15650
  };
15392
15651
  /**
15393
- * `failure-contribution` — the capability an addon reports its OWN losses
15394
- * through, per camera, with the denominator attached. It stores nothing.
15395
- *
15396
- * ## The twin of `load-contribution`, and why it is a twin and not a field
15397
- *
15398
- * `load-contribution` answers *what did this camera COST*. This answers *what
15399
- * did this camera LOSE*. The reporting discipline is identical and deliberately
15400
- * copied: the contributor reports what it already knows, hub-main adds only
15401
- * `addonId`, nothing needs global knowledge, and there is no central list for
15402
- * somebody to forget to edit.
15403
- *
15404
- * They are not merged, because their invariants are opposites:
15405
- *
15406
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
15407
- * claim a camera cost nothing, which is a measurement nobody made;
15408
- * - a `failure-contribution` zero is the **most valuable value on the
15409
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
15410
- * and it is exactly what an absent entry cannot say.
15411
- *
15412
- * Putting a loss counter on a cost entry would also break the reconciliation
15413
- * that gives `load-contribution` its point: contributions are subtracted from
15414
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
15415
- * has no process.
15416
- *
15417
- * ## Why not a log line, since the counters already exist
15418
- *
15419
- * Several of these paths already counted themselves — `CaptureScheduler`'s
15420
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
15421
- * ends in a log line, and a log line is the thing the operator asked to stop
15422
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
15423
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
15424
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
15425
- * media blackout were both diagnosed. The counters stay; this is where they can
15426
- * be READ.
15427
- *
15428
- * ## The rate is served with its denominator or not at all
15429
- *
15430
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
15431
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
15432
- * than yesterday" and was **flat across twelve hours** once divided by the
15433
- * successes on the same path. A surface that publishes only the numerator
15434
- * reproduces that mistake on every read.
15435
- *
15436
- * ## Shape
15437
- *
15438
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
15439
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
15440
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
15441
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
15442
- * a forked runner's entries reach hub-main over transport that already exists.
15443
- * No new UDS message, no second registry (D3). The operator reads the assembled
15444
- * result through `system.getFailureContributions`.
15445
- */
15446
- var FailureReasonCountSchema = object({
15447
- /**
15448
- * Why the attempt did not land, in the contributor's own vocabulary —
15449
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
15450
- * strings that already appear in this repo's logs and, where one exists, the
15451
- * same string the per-track `previewMissReason` records (D276): a second
15452
- * vocabulary for the same loss would make the row and the counter
15453
- * un-joinable.
15454
- */
15455
- reason: string(),
15456
- count: number().int().nonnegative()
15457
- });
15458
- var FailureContributionSchema = object({
15459
- /**
15460
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
15461
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
15462
- * `unit` free: the families are owned by different addons and a shared enum
15463
- * is a central list that rots invisibly.
15464
- */
15465
- family: string(),
15466
- /**
15467
- * The NUMERIC device id — the same value every log line carries as
15468
- * `tags.deviceId`. Never nullable and never absent: a contributor that
15469
- * cannot name the camera must not emit the entry, because a fleet total
15470
- * cannot answer the only question anybody asks of this surface.
15471
- */
15472
- deviceId: number().int().positive(),
15473
- /**
15474
- * A second dimension inside the family: the model / step id for an inference
15475
- * timeout, so "which camera AND which model" is one read. Absent when the
15476
- * family has a single variant.
15477
- */
15478
- variant: string().optional(),
15479
- /**
15480
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
15481
- * differencing two reads must drop the interval when it changes, because the
15482
- * counter restarted from zero in a respawned runner. Same discipline as
15483
- * `LoadContribution.startedAtMs`.
15484
- */
15485
- sinceMs: number(),
15486
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
15487
- atMs: number(),
15488
- /**
15489
- * THE DENOMINATOR — every attempt on this path for this camera in the
15490
- * window. A failure count published without it is the mistake this schema
15491
- * exists to make impossible.
15492
- */
15493
- attempts: number().int().nonnegative(),
15494
- /** Attempts that landed. `attempts - succeeded` is the loss. */
15495
- succeeded: number().int().nonnegative(),
15496
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
15497
- reasons: array(FailureReasonCountSchema).readonly()
15498
- });
15499
- var failureContributionCapability = {
15500
- name: "failure-contribution",
15501
- scope: "system",
15502
- mode: "collection",
15503
- internal: true,
15504
- methods: {
15505
- /**
15506
- * This addon's per-camera failure counters, read live from bounded in-RAM
15507
- * state it already keeps. Inert: no persistence, no sampling, no timer.
15508
- *
15509
- * READING NEVER RESETS. The counters are CUMULATIVE since `sinceMs`, and a
15510
- * consumer that wants a rate differences two reads. A draining read would
15511
- * make two operators with the page open each destroy half of the other's
15512
- * numbers, and `load-contribution` already settled the same question the
15513
- * same way for `cpuSeconds`.
15514
- */
15515
- list: method(_void(), array(FailureContributionSchema).readonly()) },
15516
- /** In-process only — enumerated through `addons.listCapabilityProviders`. */
15517
- mount: { kind: "skip" }
15518
- };
15519
- var LoadContributionSchema = object({
15520
- role: _enum([
15521
- "decode",
15522
- "transcode",
15523
- "recording",
15524
- "streaming",
15525
- "detection"
15526
- ]),
15527
- /**
15528
- * The NUMERIC device id — the same value every log line carries as
15529
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
15530
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
15531
- * contributor that cannot name its camera must not emit the entry at all,
15532
- * because an unnamed per-camera entry is indistinguishable from a shared one
15533
- * and would quietly turn one camera's cost into everybody's.
15534
- */
15535
- deviceId: number().int().positive().nullable(),
15536
- attribution: _enum([
15537
- "measured",
15538
- "accounted",
15539
- "unattributable"
15540
- ]),
15541
- /**
15542
- * What ONE entry is, in the contributor's own words — `615/high`,
15543
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
15544
- * family and inventing a common one would lose the only information that
15545
- * makes two entries for the same camera distinguishable.
15546
- */
15547
- unit: string(),
15548
- /**
15549
- * The OS process this cost lives in, when there is one. Present so a
15550
- * consumer can (a) tell two generations of the same unit apart across a
15551
- * restart, and (b) subtract claimed processes from the node's process
15552
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
15553
- * process of its own.
15554
- */
15555
- pid: number().int().positive().optional(),
15556
- /**
15557
- * When this generation started. The pid's incarnation marker: a consumer
15558
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
15559
- * window when this changes, because the counter restarted from zero in a new
15560
- * process.
15561
- */
15562
- startedAtMs: number().optional(),
15563
- /**
15564
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
15565
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
15566
- * contribution is asked for.
15567
- *
15568
- * Cumulative and not a rate on purpose: a rate needs a window, a window
15569
- * needs a sampler, and a new per-node sampler is the defect half of
15570
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
15571
- * by whoever already keeps a history; a rate cannot be un-averaged.
15572
- *
15573
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
15574
- * an entry with no process.
15575
- */
15576
- cpuSeconds: number().optional(),
15577
- /** Resident bytes of this unit's process, same source and same rules. */
15578
- rssBytes: number().optional()
15579
- });
15580
- var loadContributionCapability = {
15581
- name: "load-contribution",
15582
- scope: "system",
15583
- mode: "collection",
15584
- internal: true,
15585
- methods: {
15586
- /**
15587
- * This addon's own cost entries, computed live from state it already
15588
- * holds. Inert: no persistence, no sampling, no timer. It is answered on
15589
- * whatever beat the caller already has.
15590
- */
15591
- list: method(_void(), array(LoadContributionSchema).readonly()) },
15592
- /** In-process only — enumerated through `addons.listCapabilityProviders`. */
15593
- mount: { kind: "skip" }
15594
- };
15595
- /**
15596
15652
  * `login-method` — collection cap through which auth addons contribute
15597
15653
  * their pre-auth login surfaces to the login page. This is the SINGLE,
15598
15654
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -21252,12 +21308,53 @@ var MediaFileKindEnum = _enum([
21252
21308
  "keyFrameSmall",
21253
21309
  "thumbnailSmall"
21254
21310
  ]);
21311
+ /**
21312
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
21313
+ * ARE — never the bytes themselves.
21314
+ *
21315
+ * ## Why `url` and not `base64`
21316
+ *
21317
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
21318
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
21319
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
21320
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
21321
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
21322
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
21323
+ *
21324
+ * `url` points at the `event-media` data plane
21325
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
21326
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
21327
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
21328
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
21329
+ * no less protected than they were inside a `view`-level cap response — see
21330
+ * `data-plane-access.ts` for the rule and the one gap it does not close
21331
+ * (per-device scoping).
21332
+ *
21333
+ * The URL is built from the row's **stored** key, which is not always its
21334
+ * published `kind`: a track's face/plate crop is stored as `crop` under
21335
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
21336
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
21337
+ *
21338
+ * ## `base64` is TRANSITIONAL and is going away
21339
+ *
21340
+ * It is still populated for one reason: the deployed viewer's track-detail
21341
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
21342
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
21343
+ * triangle — not as absence. Removing the field before that viewer ships is an
21344
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
21345
+ * delete this line and the `withBytes` pass-through in
21346
+ * `analytics-query-facade.ts`; nothing else reads it.
21347
+ */
21255
21348
  var MediaFileSchema = object({
21256
21349
  key: string(),
21257
21350
  kind: MediaFileKindEnum,
21258
- base64: string(),
21259
21351
  sizeBytes: number(),
21260
21352
  timestamp: number()
21353
+ }).extend({
21354
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
21355
+ url: string(),
21356
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
21357
+ base64: string()
21261
21358
  });
21262
21359
  /**
21263
21360
  * One media row WITHOUT its bytes.
@@ -21269,7 +21366,9 @@ var MediaFileSchema = object({
21269
21366
  * blocks the whole view.
21270
21367
  *
21271
21368
  * `sizeBytes` is carried because it is what lets a client decide between the
21272
- * stored blob and a `?variant=thumb` rendering without fetching either.
21369
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
21370
+ * `url` because a client that had to build the plane path itself is a second
21371
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
21273
21372
  */
21274
21373
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
21275
21374
  /**
@@ -22067,6 +22166,17 @@ var pipelineAnalyticsCapability = {
22067
22166
  * happens to stamp it. This count is what the migration planner's
22068
22167
  * non-blocking gate reads; `relocateMedia({ mode: 'seal' })` is what drives
22069
22168
  * it to zero.
22169
+ *
22170
+ * TWO indexed statements per collection, not a walk. It used to page the
22171
+ * whole collection at 200 rows per RPC ordered by an unindexed column, so
22172
+ * on the live hub — 1 254 576 rows — it hit the 60 s RPC deadline every
22173
+ * time it was called, and the migration it gates could never start. The
22174
+ * cheap question (`present`: is there at least one) is asked first and
22175
+ * separately from the expensive one (`rows`), because only the first has
22176
+ * to be answerable for the gate to do its job.
22177
+ *
22178
+ * **`null` is "not measurable", never zero** — at either level. An
22179
+ * unreadable collection must not read as a sealed one.
22070
22180
  */
22071
22181
  countUnstampedEventMedia: method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }),
22072
22182
  /**
@@ -22390,6 +22500,26 @@ var pipelineAnalyticsCapability = {
22390
22500
  deviceId: number()
22391
22501
  }), array(MediaFileInfoSchema).readonly()),
22392
22502
  /**
22503
+ * What media an EVENT has, without any of it — the twin `getEventMedia`
22504
+ * never had.
22505
+ *
22506
+ * `listTrackMedia` above got this treatment because a track's media is
22507
+ * 5-8 MB. An event's is worse per row, not better: an old-style event owns
22508
+ * a `crop` AND a native-resolution `fullFrameBoxed`, and the track DETAIL
22509
+ * modal — the one surface that legitimately shows the big kinds — unions
22510
+ * both listings to build its filmstrip. It then renders every tile from
22511
+ * the `event-media` plane by key and throws the bytes away. Measured on
22512
+ * the live hub: one event's `fullFrameBoxed` is 2 824 077 B, base64'd to
22513
+ * ~3.8 MB, allocated whole in hub-main's heap, for a list of keys.
22514
+ *
22515
+ * Same `deviceId` authorization subject as `getEventMedia`, and the same
22516
+ * rows — this is a projection of that method, never a different question.
22517
+ */
22518
+ listEventMedia: method(object({
22519
+ eventId: string(),
22520
+ deviceId: number()
22521
+ }), array(MediaFileInfoSchema).readonly()),
22522
+ /**
22393
22523
  * Search object events by text query using CLIP cosine similarity.
22394
22524
  * Encodes `text` via the `embedding-encoder` cap, queries the
22395
22525
  * `ObjectEmbeddingStore` with optional prefilters, ranks all matching
@@ -28736,10 +28866,24 @@ var FaceClusterSchema = object({
28736
28866
  size: number().int(),
28737
28867
  cohesion: number()
28738
28868
  });
28869
+ /**
28870
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
28871
+ * are — never the bytes.
28872
+ *
28873
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
28874
+ * track/event contract) is still populated because a deployed viewer requires
28875
+ * the field to parse a row at all; this method has no such reader. Its ONE
28876
+ * caller is the admin UI's detail modal, which was building
28877
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
28878
+ * dialog already rendering its key FRAME from the `event-media` plane.
28879
+ *
28880
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
28881
+ * media key directly, so this needed no new plane and no new access decision.
28882
+ */
28739
28883
  var MediaFileLiteSchema$1 = object({
28740
28884
  key: string(),
28741
28885
  kind: string(),
28742
- base64: string(),
28886
+ url: string(),
28743
28887
  sizeBytes: number(),
28744
28888
  timestamp: number()
28745
28889
  });
@@ -32129,10 +32273,24 @@ var PlateInfoSchema = object({
32129
32273
  */
32130
32274
  cropUrl: string().optional()
32131
32275
  });
32276
+ /**
32277
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
32278
+ * are — never the bytes.
32279
+ *
32280
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
32281
+ * track/event contract) is still populated because a deployed viewer requires
32282
+ * the field to parse a row at all; this method has no such reader. Its ONE
32283
+ * caller is the admin UI's detail modal, which was building
32284
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
32285
+ * dialog already rendering its key FRAME from the `event-media` plane.
32286
+ *
32287
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
32288
+ * media key directly, so this needed no new plane and no new access decision.
32289
+ */
32132
32290
  var MediaFileLiteSchema = object({
32133
32291
  key: string(),
32134
32292
  kind: string(),
32135
- base64: string(),
32293
+ url: string(),
32136
32294
  sizeBytes: number(),
32137
32295
  timestamp: number()
32138
32296
  });
@@ -39622,6 +39780,12 @@ Object.freeze({
39622
39780
  addonId: null,
39623
39781
  access: "view"
39624
39782
  },
39783
+ "pipelineAnalytics.listEventMedia": {
39784
+ capName: "pipeline-analytics",
39785
+ capScope: "device",
39786
+ addonId: null,
39787
+ access: "view"
39788
+ },
39625
39789
  "pipelineAnalytics.listGroups": {
39626
39790
  capName: "pipeline-analytics",
39627
39791
  capScope: "device",
@@ -43245,6 +43409,11 @@ Object.freeze({
43245
43409
  form: "array",
43246
43410
  optional: false
43247
43411
  }],
43412
+ "pipelineAnalytics.listEventMedia": [{
43413
+ name: "deviceId",
43414
+ form: "single",
43415
+ optional: false
43416
+ }],
43248
43417
  "pipelineAnalytics.listGroups": [{
43249
43418
  name: "deviceIds",
43250
43419
  form: "array",
@@ -45283,16 +45452,7 @@ var OsdManager = class {
45283
45452
  };
45284
45453
  getSourceCatalog = async ({ deviceId }) => {
45285
45454
  const devices = await this.safeListDevices();
45286
- const capsByDevice = /* @__PURE__ */ new Map();
45287
- for (const device of devices) try {
45288
- capsByDevice.set(device.id, await this.deps.listDeviceCaps(device.id));
45289
- } catch (err) {
45290
- this.logger.warn("osd-manager: cap enumeration failed — device omitted from catalog", {
45291
- tags: { deviceId: device.id },
45292
- meta: { error: String(err) }
45293
- });
45294
- }
45295
- return { sources: sourceOptionsFor(deviceId, devices, capsByDevice) };
45455
+ return { sources: sourceOptionsFor(deviceId, devices, await this.safeListDeviceCaps(deviceId, devices)) };
45296
45456
  };
45297
45457
  getConditionSupport = async () => {
45298
45458
  return {
@@ -45562,6 +45722,36 @@ var OsdManager = class {
45562
45722
  return [];
45563
45723
  }
45564
45724
  }
45725
+ /**
45726
+ * Every listed device's bound cap names, in ONE round trip.
45727
+ *
45728
+ * This used to be `listDeviceCaps(device.id)` inside the catalog loop, with a
45729
+ * per-device `catch` that omitted the device — 1 + 1 019 sequential
45730
+ * cross-process cap calls on the live hub 2026-08-30, behind an operator
45731
+ * opening a slot editor. The batch has no partial failure mode: it answers
45732
+ * for the whole set or it throws, and a throw lands where every per-device
45733
+ * read failing already landed — an empty map, hence a catalog with no
45734
+ * bindable sources. One log line instead of a thousand.
45735
+ *
45736
+ * `tags.deviceId` is the CAMERA whose editor is open, because that is the
45737
+ * question an operator asks ("why is 615's picker empty?"); the size of the
45738
+ * set that went unanswered rides in `meta`.
45739
+ */
45740
+ async safeListDeviceCaps(cameraDeviceId, devices) {
45741
+ if (devices.length === 0) return /* @__PURE__ */ new Map();
45742
+ try {
45743
+ return await this.deps.listDeviceCapsBatch(devices.map((d) => d.id));
45744
+ } catch (err) {
45745
+ this.logger.warn("osd-manager: cap enumeration failed — the source catalog has no bindable values", {
45746
+ tags: { deviceId: cameraDeviceId },
45747
+ meta: {
45748
+ devices: devices.length,
45749
+ error: String(err)
45750
+ }
45751
+ });
45752
+ return /* @__PURE__ */ new Map();
45753
+ }
45754
+ }
45565
45755
  };
45566
45756
  function cacheKey(deviceId, slotId) {
45567
45757
  return `${deviceId}:${slotId}`;
@@ -45646,9 +45836,9 @@ var OsdManagerAddon = class extends BaseAddon {
45646
45836
  name: d.name
45647
45837
  }));
45648
45838
  },
45649
- listDeviceCaps: async (deviceId) => {
45650
- const bindings = await ctx.api.deviceManager.getBindings.query({ deviceId });
45651
- return [...new Set(bindings.entries.map((e) => e.capName))];
45839
+ listDeviceCapsBatch: async (deviceIds) => {
45840
+ const rows = await ctx.api.deviceManager.getBindingsBatch.query({ deviceIds: [...deviceIds] });
45841
+ return new Map(rows.map((row) => [row.deviceId, [...new Set(row.entries.map((e) => e.capName))]]));
45652
45842
  },
45653
45843
  readLatestRecognitions: async (deviceId) => {
45654
45844
  const [faces, plates] = await Promise.all([ctx.api.faceGallery.listRecentFaces.query({