@camstack/addon-tailscale 1.2.46 → 1.2.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  enumerable: true
25
25
  }) : target, mod));
26
26
  //#endregion
27
- const require_dist = require("../dist-B81Rw6ym.js");
27
+ const require_dist = require("../dist-D4Kv7xGB.js");
28
28
  let node_child_process = require("node:child_process");
29
29
  let node_util = require("node:util");
30
30
  let node_fs = require("node:fs");
@@ -1,4 +1,4 @@
1
- import { a as EventCategory, i as BaseAddon, n as networkAccessCapability, r as BOOT_RECOVERY_BACKOFF_MS, t as meshNetworkCapability } from "../dist-Tyu2rQxG.mjs";
1
+ import { a as EventCategory, i as BaseAddon, n as networkAccessCapability, r as BOOT_RECOVERY_BACKOFF_MS, t as meshNetworkCapability } from "../dist-r9nSS25r.mjs";
2
2
  import { execFile, spawn } from "node:child_process";
3
3
  import { promisify } from "node:util";
4
4
  import * as fs from "node:fs";
@@ -12937,6 +12937,114 @@ method(object({
12937
12937
  height: number()
12938
12938
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
12939
12939
  /**
12940
+ * `failure-contribution` — the capability an addon reports its OWN losses
12941
+ * through, per camera, with the denominator attached. It stores nothing.
12942
+ *
12943
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
12944
+ *
12945
+ * `load-contribution` answers *what did this camera COST*. This answers *what
12946
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
12947
+ * copied: the contributor reports what it already knows, hub-main adds only
12948
+ * `addonId`, nothing needs global knowledge, and there is no central list for
12949
+ * somebody to forget to edit.
12950
+ *
12951
+ * They are not merged, because their invariants are opposites:
12952
+ *
12953
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
12954
+ * claim a camera cost nothing, which is a measurement nobody made;
12955
+ * - a `failure-contribution` zero is the **most valuable value on the
12956
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
12957
+ * and it is exactly what an absent entry cannot say.
12958
+ *
12959
+ * Putting a loss counter on a cost entry would also break the reconciliation
12960
+ * that gives `load-contribution` its point: contributions are subtracted from
12961
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
12962
+ * has no process.
12963
+ *
12964
+ * ## Why not a log line, since the counters already exist
12965
+ *
12966
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
12967
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
12968
+ * ends in a log line, and a log line is the thing the operator asked to stop
12969
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
12970
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
12971
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
12972
+ * media blackout were both diagnosed. The counters stay; this is where they can
12973
+ * be READ.
12974
+ *
12975
+ * ## The rate is served with its denominator or not at all
12976
+ *
12977
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
12978
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
12979
+ * than yesterday" and was **flat across twelve hours** once divided by the
12980
+ * successes on the same path. A surface that publishes only the numerator
12981
+ * reproduces that mistake on every read.
12982
+ *
12983
+ * ## Shape
12984
+ *
12985
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
12986
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
12987
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
12988
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
12989
+ * a forked runner's entries reach hub-main over transport that already exists.
12990
+ * No new UDS message, no second registry (D3). The operator reads the assembled
12991
+ * result through `system.getFailureContributions`.
12992
+ */
12993
+ var FailureReasonCountSchema = object({
12994
+ /**
12995
+ * Why the attempt did not land, in the contributor's own vocabulary —
12996
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
12997
+ * strings that already appear in this repo's logs and, where one exists, the
12998
+ * same string the per-track `previewMissReason` records (D276): a second
12999
+ * vocabulary for the same loss would make the row and the counter
13000
+ * un-joinable.
13001
+ */
13002
+ reason: string(),
13003
+ count: number().int().nonnegative()
13004
+ });
13005
+ var FailureContributionSchema = object({
13006
+ /**
13007
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13008
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13009
+ * `unit` free: the families are owned by different addons and a shared enum
13010
+ * is a central list that rots invisibly.
13011
+ */
13012
+ family: string(),
13013
+ /**
13014
+ * The NUMERIC device id — the same value every log line carries as
13015
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13016
+ * cannot name the camera must not emit the entry, because a fleet total
13017
+ * cannot answer the only question anybody asks of this surface.
13018
+ */
13019
+ deviceId: number().int().positive(),
13020
+ /**
13021
+ * A second dimension inside the family: the model / step id for an inference
13022
+ * timeout, so "which camera AND which model" is one read. Absent when the
13023
+ * family has a single variant.
13024
+ */
13025
+ variant: string().optional(),
13026
+ /**
13027
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13028
+ * differencing two reads must drop the interval when it changes, because the
13029
+ * counter restarted from zero in a respawned runner. Same discipline as
13030
+ * `LoadContribution.startedAtMs`.
13031
+ */
13032
+ sinceMs: number(),
13033
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13034
+ atMs: number(),
13035
+ /**
13036
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13037
+ * window. A failure count published without it is the mistake this schema
13038
+ * exists to make impossible.
13039
+ */
13040
+ attempts: number().int().nonnegative(),
13041
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13042
+ succeeded: number().int().nonnegative(),
13043
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13044
+ reasons: array(FailureReasonCountSchema).readonly()
13045
+ });
13046
+ method(_void(), array(FailureContributionSchema).readonly());
13047
+ /**
12940
13048
  * filesystem-browse — per-node capability for browsing the node's local
12941
13049
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
12942
13050
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13458,6 +13566,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13458
13566
  kind: "mutation",
13459
13567
  auth: "admin"
13460
13568
  });
13569
+ var LoadContributionSchema = object({
13570
+ role: _enum([
13571
+ "decode",
13572
+ "transcode",
13573
+ "recording",
13574
+ "streaming",
13575
+ "detection"
13576
+ ]),
13577
+ /**
13578
+ * The NUMERIC device id — the same value every log line carries as
13579
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13580
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13581
+ * contributor that cannot name its camera must not emit the entry at all,
13582
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13583
+ * and would quietly turn one camera's cost into everybody's.
13584
+ */
13585
+ deviceId: number().int().positive().nullable(),
13586
+ attribution: _enum([
13587
+ "measured",
13588
+ "accounted",
13589
+ "unattributable"
13590
+ ]),
13591
+ /**
13592
+ * What ONE entry is, in the contributor's own words — `615/high`,
13593
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13594
+ * family and inventing a common one would lose the only information that
13595
+ * makes two entries for the same camera distinguishable.
13596
+ */
13597
+ unit: string(),
13598
+ /**
13599
+ * The OS process this cost lives in, when there is one. Present so a
13600
+ * consumer can (a) tell two generations of the same unit apart across a
13601
+ * restart, and (b) subtract claimed processes from the node's process
13602
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13603
+ * process of its own.
13604
+ */
13605
+ pid: number().int().positive().optional(),
13606
+ /**
13607
+ * When this generation started. The pid's incarnation marker: a consumer
13608
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13609
+ * window when this changes, because the counter restarted from zero in a new
13610
+ * process.
13611
+ */
13612
+ startedAtMs: number().optional(),
13613
+ /**
13614
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13615
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13616
+ * contribution is asked for.
13617
+ *
13618
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13619
+ * needs a sampler, and a new per-node sampler is the defect half of
13620
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13621
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13622
+ *
13623
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13624
+ * an entry with no process.
13625
+ */
13626
+ cpuSeconds: number().optional(),
13627
+ /** Resident bytes of this unit's process, same source and same rules. */
13628
+ rssBytes: number().optional()
13629
+ });
13630
+ method(_void(), array(LoadContributionSchema).readonly());
13461
13631
  /**
13462
13632
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13463
13633
  * through. It stores nothing.
@@ -13534,176 +13704,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13534
13704
  tags: record(string(), string()).optional()
13535
13705
  }), array(LogEntrySchema).readonly());
13536
13706
  /**
13537
- * `failure-contribution` — the capability an addon reports its OWN losses
13538
- * through, per camera, with the denominator attached. It stores nothing.
13539
- *
13540
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13541
- *
13542
- * `load-contribution` answers *what did this camera COST*. This answers *what
13543
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13544
- * copied: the contributor reports what it already knows, hub-main adds only
13545
- * `addonId`, nothing needs global knowledge, and there is no central list for
13546
- * somebody to forget to edit.
13547
- *
13548
- * They are not merged, because their invariants are opposites:
13549
- *
13550
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13551
- * claim a camera cost nothing, which is a measurement nobody made;
13552
- * - a `failure-contribution` zero is the **most valuable value on the
13553
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13554
- * and it is exactly what an absent entry cannot say.
13555
- *
13556
- * Putting a loss counter on a cost entry would also break the reconciliation
13557
- * that gives `load-contribution` its point: contributions are subtracted from
13558
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13559
- * has no process.
13560
- *
13561
- * ## Why not a log line, since the counters already exist
13562
- *
13563
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13564
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13565
- * ends in a log line, and a log line is the thing the operator asked to stop
13566
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13567
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13568
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13569
- * media blackout were both diagnosed. The counters stay; this is where they can
13570
- * be READ.
13571
- *
13572
- * ## The rate is served with its denominator or not at all
13573
- *
13574
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13575
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13576
- * than yesterday" and was **flat across twelve hours** once divided by the
13577
- * successes on the same path. A surface that publishes only the numerator
13578
- * reproduces that mistake on every read.
13579
- *
13580
- * ## Shape
13581
- *
13582
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13583
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13584
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13585
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13586
- * a forked runner's entries reach hub-main over transport that already exists.
13587
- * No new UDS message, no second registry (D3). The operator reads the assembled
13588
- * result through `system.getFailureContributions`.
13589
- */
13590
- var FailureReasonCountSchema = object({
13591
- /**
13592
- * Why the attempt did not land, in the contributor's own vocabulary —
13593
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13594
- * strings that already appear in this repo's logs and, where one exists, the
13595
- * same string the per-track `previewMissReason` records (D276): a second
13596
- * vocabulary for the same loss would make the row and the counter
13597
- * un-joinable.
13598
- */
13599
- reason: string(),
13600
- count: number().int().nonnegative()
13601
- });
13602
- var FailureContributionSchema = object({
13603
- /**
13604
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13605
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13606
- * `unit` free: the families are owned by different addons and a shared enum
13607
- * is a central list that rots invisibly.
13608
- */
13609
- family: string(),
13610
- /**
13611
- * The NUMERIC device id — the same value every log line carries as
13612
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13613
- * cannot name the camera must not emit the entry, because a fleet total
13614
- * cannot answer the only question anybody asks of this surface.
13615
- */
13616
- deviceId: number().int().positive(),
13617
- /**
13618
- * A second dimension inside the family: the model / step id for an inference
13619
- * timeout, so "which camera AND which model" is one read. Absent when the
13620
- * family has a single variant.
13621
- */
13622
- variant: string().optional(),
13623
- /**
13624
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13625
- * differencing two reads must drop the interval when it changes, because the
13626
- * counter restarted from zero in a respawned runner. Same discipline as
13627
- * `LoadContribution.startedAtMs`.
13628
- */
13629
- sinceMs: number(),
13630
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13631
- atMs: number(),
13632
- /**
13633
- * THE DENOMINATOR — every attempt on this path for this camera in the
13634
- * window. A failure count published without it is the mistake this schema
13635
- * exists to make impossible.
13636
- */
13637
- attempts: number().int().nonnegative(),
13638
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13639
- succeeded: number().int().nonnegative(),
13640
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13641
- reasons: array(FailureReasonCountSchema).readonly()
13642
- });
13643
- method(_void(), array(FailureContributionSchema).readonly());
13644
- var LoadContributionSchema = object({
13645
- role: _enum([
13646
- "decode",
13647
- "transcode",
13648
- "recording",
13649
- "streaming",
13650
- "detection"
13651
- ]),
13652
- /**
13653
- * The NUMERIC device id — the same value every log line carries as
13654
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13655
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13656
- * contributor that cannot name its camera must not emit the entry at all,
13657
- * because an unnamed per-camera entry is indistinguishable from a shared one
13658
- * and would quietly turn one camera's cost into everybody's.
13659
- */
13660
- deviceId: number().int().positive().nullable(),
13661
- attribution: _enum([
13662
- "measured",
13663
- "accounted",
13664
- "unattributable"
13665
- ]),
13666
- /**
13667
- * What ONE entry is, in the contributor's own words — `615/high`,
13668
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13669
- * family and inventing a common one would lose the only information that
13670
- * makes two entries for the same camera distinguishable.
13671
- */
13672
- unit: string(),
13673
- /**
13674
- * The OS process this cost lives in, when there is one. Present so a
13675
- * consumer can (a) tell two generations of the same unit apart across a
13676
- * restart, and (b) subtract claimed processes from the node's process
13677
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13678
- * process of its own.
13679
- */
13680
- pid: number().int().positive().optional(),
13681
- /**
13682
- * When this generation started. The pid's incarnation marker: a consumer
13683
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13684
- * window when this changes, because the counter restarted from zero in a new
13685
- * process.
13686
- */
13687
- startedAtMs: number().optional(),
13688
- /**
13689
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13690
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13691
- * contribution is asked for.
13692
- *
13693
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13694
- * needs a sampler, and a new per-node sampler is the defect half of
13695
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13696
- * by whoever already keeps a history; a rate cannot be un-averaged.
13697
- *
13698
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13699
- * an entry with no process.
13700
- */
13701
- cpuSeconds: number().optional(),
13702
- /** Resident bytes of this unit's process, same source and same rules. */
13703
- rssBytes: number().optional()
13704
- });
13705
- method(_void(), array(LoadContributionSchema).readonly());
13706
- /**
13707
13707
  * `login-method` — collection cap through which auth addons contribute
13708
13708
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13709
13709
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18380,12 +18380,53 @@ var MediaFileKindEnum = _enum([
18380
18380
  "keyFrameSmall",
18381
18381
  "thumbnailSmall"
18382
18382
  ]);
18383
+ /**
18384
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18385
+ * ARE — never the bytes themselves.
18386
+ *
18387
+ * ## Why `url` and not `base64`
18388
+ *
18389
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18390
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18391
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18392
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18393
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18394
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18395
+ *
18396
+ * `url` points at the `event-media` data plane
18397
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18398
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18399
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18400
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18401
+ * no less protected than they were inside a `view`-level cap response — see
18402
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18403
+ * (per-device scoping).
18404
+ *
18405
+ * The URL is built from the row's **stored** key, which is not always its
18406
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18407
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18408
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18409
+ *
18410
+ * ## `base64` is TRANSITIONAL and is going away
18411
+ *
18412
+ * It is still populated for one reason: the deployed viewer's track-detail
18413
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18414
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18415
+ * triangle — not as absence. Removing the field before that viewer ships is an
18416
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18417
+ * delete this line and the `withBytes` pass-through in
18418
+ * `analytics-query-facade.ts`; nothing else reads it.
18419
+ */
18383
18420
  var MediaFileSchema = object({
18384
18421
  key: string(),
18385
18422
  kind: MediaFileKindEnum,
18386
- base64: string(),
18387
18423
  sizeBytes: number(),
18388
18424
  timestamp: number()
18425
+ }).extend({
18426
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18427
+ url: string(),
18428
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18429
+ base64: string()
18389
18430
  });
18390
18431
  /**
18391
18432
  * One media row WITHOUT its bytes.
@@ -18397,7 +18438,9 @@ var MediaFileSchema = object({
18397
18438
  * blocks the whole view.
18398
18439
  *
18399
18440
  * `sizeBytes` is carried because it is what lets a client decide between the
18400
- * stored blob and a `?variant=thumb` rendering without fetching either.
18441
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18442
+ * `url` because a client that had to build the plane path itself is a second
18443
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18401
18444
  */
18402
18445
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18403
18446
  /**
@@ -18744,6 +18787,50 @@ var EventStoreFootprintSchema = object({
18744
18787
  totalBytes: number().int(),
18745
18788
  devices: array(EventStoreDeviceFootprintSchema).readonly()
18746
18789
  });
18790
+ /** Event-media footprint for one {@link MediaFileKind}. */
18791
+ var EventMediaKindFootprintSchema = object({
18792
+ kind: MediaFileKindEnum,
18793
+ /** Media rows of this kind. */
18794
+ rows: number().int(),
18795
+ /** Bytes on disk held by those rows. */
18796
+ bytes: number().int()
18797
+ });
18798
+ /**
18799
+ * The media footprint broken down by KIND — the axis a deletion decision
18800
+ * actually turns on.
18801
+ *
18802
+ * A byte total says how much there is; it cannot say what is safe to remove.
18803
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
18804
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
18805
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
18806
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
18807
+ * nothing else, so sizing a deletion means summing per kind.
18808
+ *
18809
+ * ## Why `unaccounted*` exists
18810
+ *
18811
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
18812
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
18813
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
18814
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
18815
+ * retired code path, or by a version that knew a kind this one does not) would
18816
+ * otherwise vanish from the total silently, and an operator would delete
18817
+ * against a denominator smaller than the disk.
18818
+ *
18819
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
18820
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
18821
+ */
18822
+ var EventMediaKindBreakdownSchema = object({
18823
+ /** Every media row in scope, from one unfiltered aggregate. */
18824
+ totalRows: number().int(),
18825
+ /** Every media byte in scope, from that same aggregate. */
18826
+ totalBytes: number().int(),
18827
+ /** Per-kind footprint, ordered by bytes descending. */
18828
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
18829
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
18830
+ unaccountedRows: number().int(),
18831
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
18832
+ unaccountedBytes: number().int()
18833
+ });
18747
18834
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
18748
18835
  var EventPruneCountsSchema = object({
18749
18836
  motion: number().int(),
@@ -18947,6 +19034,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18947
19034
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
18948
19035
  kind: "query",
18949
19036
  auth: "admin"
19037
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19038
+ kind: "query",
19039
+ auth: "admin"
18950
19040
  }), method(object({
18951
19041
  olderThanMs: number(),
18952
19042
  reason: OpsLogReasonSchema.optional()
@@ -19086,6 +19176,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19086
19176
  }), array(MediaFileSchema).readonly()), method(object({
19087
19177
  trackId: string(),
19088
19178
  deviceId: number()
19179
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19180
+ eventId: string(),
19181
+ deviceId: number()
19089
19182
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19090
19183
  kind: "mutation",
19091
19184
  auth: "admin"
@@ -20895,6 +20988,20 @@ method(object({
20895
20988
  error: string().optional()
20896
20989
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
20897
20990
  providerId: string(),
20991
+ /**
20992
+ * The location this config is an UNSAVED edit of, when there is one.
20993
+ *
20994
+ * `listLocations` replaces every declared secret with the redaction
20995
+ * sentinel, so the edit modal's form state holds the sentinel for any
20996
+ * credential the operator did not retype — and posting that here
20997
+ * without a way to resolve it makes the provider try to authenticate
20998
+ * as `__camstack_redacted__` and report the operator's own working
20999
+ * password as wrong. Given this id, the orchestrator restores each
21000
+ * sentinel from the stored config (same rule as `upsertLocation`)
21001
+ * before dispatching. Omitted by the "Add location" wizard, where
21002
+ * every value was typed just now and nothing is stored yet.
21003
+ */
21004
+ locationId: string().optional(),
20898
21005
  config: record(string(), unknown())
20899
21006
  }), object({
20900
21007
  ok: boolean(),
@@ -23346,10 +23453,24 @@ var FaceClusterSchema = object({
23346
23453
  size: number().int(),
23347
23454
  cohesion: number()
23348
23455
  });
23456
+ /**
23457
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23458
+ * are — never the bytes.
23459
+ *
23460
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23461
+ * track/event contract) is still populated because a deployed viewer requires
23462
+ * the field to parse a row at all; this method has no such reader. Its ONE
23463
+ * caller is the admin UI's detail modal, which was building
23464
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23465
+ * dialog already rendering its key FRAME from the `event-media` plane.
23466
+ *
23467
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23468
+ * media key directly, so this needed no new plane and no new access decision.
23469
+ */
23349
23470
  var MediaFileLiteSchema$1 = object({
23350
23471
  key: string(),
23351
23472
  kind: string(),
23352
- base64: string(),
23473
+ url: string(),
23353
23474
  sizeBytes: number(),
23354
23475
  timestamp: number()
23355
23476
  });
@@ -25662,10 +25783,24 @@ var PlateInfoSchema = object({
25662
25783
  */
25663
25784
  cropUrl: string().optional()
25664
25785
  });
25786
+ /**
25787
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25788
+ * are — never the bytes.
25789
+ *
25790
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25791
+ * track/event contract) is still populated because a deployed viewer requires
25792
+ * the field to parse a row at all; this method has no such reader. Its ONE
25793
+ * caller is the admin UI's detail modal, which was building
25794
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25795
+ * dialog already rendering its key FRAME from the `event-media` plane.
25796
+ *
25797
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25798
+ * media key directly, so this needed no new plane and no new access decision.
25799
+ */
25665
25800
  var MediaFileLiteSchema = object({
25666
25801
  key: string(),
25667
25802
  kind: string(),
25668
- base64: string(),
25803
+ url: string(),
25669
25804
  sizeBytes: number(),
25670
25805
  timestamp: number()
25671
25806
  });
@@ -31582,6 +31717,12 @@ Object.freeze({
31582
31717
  addonId: null,
31583
31718
  access: "view"
31584
31719
  },
31720
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
31721
+ capName: "pipeline-analytics",
31722
+ capScope: "device",
31723
+ addonId: null,
31724
+ access: "view"
31725
+ },
31585
31726
  "pipelineAnalytics.getEventStoreFootprint": {
31586
31727
  capName: "pipeline-analytics",
31587
31728
  capScope: "device",
@@ -31678,6 +31819,12 @@ Object.freeze({
31678
31819
  addonId: null,
31679
31820
  access: "view"
31680
31821
  },
31822
+ "pipelineAnalytics.listEventMedia": {
31823
+ capName: "pipeline-analytics",
31824
+ capScope: "device",
31825
+ addonId: null,
31826
+ access: "view"
31827
+ },
31681
31828
  "pipelineAnalytics.listGroups": {
31682
31829
  capName: "pipeline-analytics",
31683
31830
  capScope: "device",
@@ -35241,6 +35388,11 @@ Object.freeze({
35241
35388
  form: "single",
35242
35389
  optional: false
35243
35390
  }],
35391
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
35392
+ name: "deviceId",
35393
+ form: "single",
35394
+ optional: true
35395
+ }],
35244
35396
  "pipelineAnalytics.getGroup": [{
35245
35397
  name: "deviceId",
35246
35398
  form: "single",
@@ -35301,6 +35453,11 @@ Object.freeze({
35301
35453
  form: "array",
35302
35454
  optional: false
35303
35455
  }],
35456
+ "pipelineAnalytics.listEventMedia": [{
35457
+ name: "deviceId",
35458
+ form: "single",
35459
+ optional: false
35460
+ }],
35304
35461
  "pipelineAnalytics.listGroups": [{
35305
35462
  name: "deviceIds",
35306
35463
  form: "array",
@@ -12937,6 +12937,114 @@ method(object({
12937
12937
  height: number()
12938
12938
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
12939
12939
  /**
12940
+ * `failure-contribution` — the capability an addon reports its OWN losses
12941
+ * through, per camera, with the denominator attached. It stores nothing.
12942
+ *
12943
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
12944
+ *
12945
+ * `load-contribution` answers *what did this camera COST*. This answers *what
12946
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
12947
+ * copied: the contributor reports what it already knows, hub-main adds only
12948
+ * `addonId`, nothing needs global knowledge, and there is no central list for
12949
+ * somebody to forget to edit.
12950
+ *
12951
+ * They are not merged, because their invariants are opposites:
12952
+ *
12953
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
12954
+ * claim a camera cost nothing, which is a measurement nobody made;
12955
+ * - a `failure-contribution` zero is the **most valuable value on the
12956
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
12957
+ * and it is exactly what an absent entry cannot say.
12958
+ *
12959
+ * Putting a loss counter on a cost entry would also break the reconciliation
12960
+ * that gives `load-contribution` its point: contributions are subtracted from
12961
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
12962
+ * has no process.
12963
+ *
12964
+ * ## Why not a log line, since the counters already exist
12965
+ *
12966
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
12967
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
12968
+ * ends in a log line, and a log line is the thing the operator asked to stop
12969
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
12970
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
12971
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
12972
+ * media blackout were both diagnosed. The counters stay; this is where they can
12973
+ * be READ.
12974
+ *
12975
+ * ## The rate is served with its denominator or not at all
12976
+ *
12977
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
12978
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
12979
+ * than yesterday" and was **flat across twelve hours** once divided by the
12980
+ * successes on the same path. A surface that publishes only the numerator
12981
+ * reproduces that mistake on every read.
12982
+ *
12983
+ * ## Shape
12984
+ *
12985
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
12986
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
12987
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
12988
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
12989
+ * a forked runner's entries reach hub-main over transport that already exists.
12990
+ * No new UDS message, no second registry (D3). The operator reads the assembled
12991
+ * result through `system.getFailureContributions`.
12992
+ */
12993
+ var FailureReasonCountSchema = object({
12994
+ /**
12995
+ * Why the attempt did not land, in the contributor's own vocabulary —
12996
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
12997
+ * strings that already appear in this repo's logs and, where one exists, the
12998
+ * same string the per-track `previewMissReason` records (D276): a second
12999
+ * vocabulary for the same loss would make the row and the counter
13000
+ * un-joinable.
13001
+ */
13002
+ reason: string(),
13003
+ count: number().int().nonnegative()
13004
+ });
13005
+ var FailureContributionSchema = object({
13006
+ /**
13007
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13008
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13009
+ * `unit` free: the families are owned by different addons and a shared enum
13010
+ * is a central list that rots invisibly.
13011
+ */
13012
+ family: string(),
13013
+ /**
13014
+ * The NUMERIC device id — the same value every log line carries as
13015
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13016
+ * cannot name the camera must not emit the entry, because a fleet total
13017
+ * cannot answer the only question anybody asks of this surface.
13018
+ */
13019
+ deviceId: number().int().positive(),
13020
+ /**
13021
+ * A second dimension inside the family: the model / step id for an inference
13022
+ * timeout, so "which camera AND which model" is one read. Absent when the
13023
+ * family has a single variant.
13024
+ */
13025
+ variant: string().optional(),
13026
+ /**
13027
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13028
+ * differencing two reads must drop the interval when it changes, because the
13029
+ * counter restarted from zero in a respawned runner. Same discipline as
13030
+ * `LoadContribution.startedAtMs`.
13031
+ */
13032
+ sinceMs: number(),
13033
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13034
+ atMs: number(),
13035
+ /**
13036
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13037
+ * window. A failure count published without it is the mistake this schema
13038
+ * exists to make impossible.
13039
+ */
13040
+ attempts: number().int().nonnegative(),
13041
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13042
+ succeeded: number().int().nonnegative(),
13043
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13044
+ reasons: array(FailureReasonCountSchema).readonly()
13045
+ });
13046
+ method(_void(), array(FailureContributionSchema).readonly());
13047
+ /**
12940
13048
  * filesystem-browse — per-node capability for browsing the node's local
12941
13049
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
12942
13050
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13458,6 +13566,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13458
13566
  kind: "mutation",
13459
13567
  auth: "admin"
13460
13568
  });
13569
+ var LoadContributionSchema = object({
13570
+ role: _enum([
13571
+ "decode",
13572
+ "transcode",
13573
+ "recording",
13574
+ "streaming",
13575
+ "detection"
13576
+ ]),
13577
+ /**
13578
+ * The NUMERIC device id — the same value every log line carries as
13579
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13580
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13581
+ * contributor that cannot name its camera must not emit the entry at all,
13582
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13583
+ * and would quietly turn one camera's cost into everybody's.
13584
+ */
13585
+ deviceId: number().int().positive().nullable(),
13586
+ attribution: _enum([
13587
+ "measured",
13588
+ "accounted",
13589
+ "unattributable"
13590
+ ]),
13591
+ /**
13592
+ * What ONE entry is, in the contributor's own words — `615/high`,
13593
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13594
+ * family and inventing a common one would lose the only information that
13595
+ * makes two entries for the same camera distinguishable.
13596
+ */
13597
+ unit: string(),
13598
+ /**
13599
+ * The OS process this cost lives in, when there is one. Present so a
13600
+ * consumer can (a) tell two generations of the same unit apart across a
13601
+ * restart, and (b) subtract claimed processes from the node's process
13602
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13603
+ * process of its own.
13604
+ */
13605
+ pid: number().int().positive().optional(),
13606
+ /**
13607
+ * When this generation started. The pid's incarnation marker: a consumer
13608
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13609
+ * window when this changes, because the counter restarted from zero in a new
13610
+ * process.
13611
+ */
13612
+ startedAtMs: number().optional(),
13613
+ /**
13614
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13615
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13616
+ * contribution is asked for.
13617
+ *
13618
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13619
+ * needs a sampler, and a new per-node sampler is the defect half of
13620
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13621
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13622
+ *
13623
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13624
+ * an entry with no process.
13625
+ */
13626
+ cpuSeconds: number().optional(),
13627
+ /** Resident bytes of this unit's process, same source and same rules. */
13628
+ rssBytes: number().optional()
13629
+ });
13630
+ method(_void(), array(LoadContributionSchema).readonly());
13461
13631
  /**
13462
13632
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13463
13633
  * through. It stores nothing.
@@ -13534,176 +13704,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13534
13704
  tags: record(string(), string()).optional()
13535
13705
  }), array(LogEntrySchema).readonly());
13536
13706
  /**
13537
- * `failure-contribution` — the capability an addon reports its OWN losses
13538
- * through, per camera, with the denominator attached. It stores nothing.
13539
- *
13540
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13541
- *
13542
- * `load-contribution` answers *what did this camera COST*. This answers *what
13543
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13544
- * copied: the contributor reports what it already knows, hub-main adds only
13545
- * `addonId`, nothing needs global knowledge, and there is no central list for
13546
- * somebody to forget to edit.
13547
- *
13548
- * They are not merged, because their invariants are opposites:
13549
- *
13550
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13551
- * claim a camera cost nothing, which is a measurement nobody made;
13552
- * - a `failure-contribution` zero is the **most valuable value on the
13553
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13554
- * and it is exactly what an absent entry cannot say.
13555
- *
13556
- * Putting a loss counter on a cost entry would also break the reconciliation
13557
- * that gives `load-contribution` its point: contributions are subtracted from
13558
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13559
- * has no process.
13560
- *
13561
- * ## Why not a log line, since the counters already exist
13562
- *
13563
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13564
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13565
- * ends in a log line, and a log line is the thing the operator asked to stop
13566
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13567
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13568
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13569
- * media blackout were both diagnosed. The counters stay; this is where they can
13570
- * be READ.
13571
- *
13572
- * ## The rate is served with its denominator or not at all
13573
- *
13574
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13575
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13576
- * than yesterday" and was **flat across twelve hours** once divided by the
13577
- * successes on the same path. A surface that publishes only the numerator
13578
- * reproduces that mistake on every read.
13579
- *
13580
- * ## Shape
13581
- *
13582
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13583
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13584
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13585
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13586
- * a forked runner's entries reach hub-main over transport that already exists.
13587
- * No new UDS message, no second registry (D3). The operator reads the assembled
13588
- * result through `system.getFailureContributions`.
13589
- */
13590
- var FailureReasonCountSchema = object({
13591
- /**
13592
- * Why the attempt did not land, in the contributor's own vocabulary —
13593
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13594
- * strings that already appear in this repo's logs and, where one exists, the
13595
- * same string the per-track `previewMissReason` records (D276): a second
13596
- * vocabulary for the same loss would make the row and the counter
13597
- * un-joinable.
13598
- */
13599
- reason: string(),
13600
- count: number().int().nonnegative()
13601
- });
13602
- var FailureContributionSchema = object({
13603
- /**
13604
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13605
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13606
- * `unit` free: the families are owned by different addons and a shared enum
13607
- * is a central list that rots invisibly.
13608
- */
13609
- family: string(),
13610
- /**
13611
- * The NUMERIC device id — the same value every log line carries as
13612
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13613
- * cannot name the camera must not emit the entry, because a fleet total
13614
- * cannot answer the only question anybody asks of this surface.
13615
- */
13616
- deviceId: number().int().positive(),
13617
- /**
13618
- * A second dimension inside the family: the model / step id for an inference
13619
- * timeout, so "which camera AND which model" is one read. Absent when the
13620
- * family has a single variant.
13621
- */
13622
- variant: string().optional(),
13623
- /**
13624
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13625
- * differencing two reads must drop the interval when it changes, because the
13626
- * counter restarted from zero in a respawned runner. Same discipline as
13627
- * `LoadContribution.startedAtMs`.
13628
- */
13629
- sinceMs: number(),
13630
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13631
- atMs: number(),
13632
- /**
13633
- * THE DENOMINATOR — every attempt on this path for this camera in the
13634
- * window. A failure count published without it is the mistake this schema
13635
- * exists to make impossible.
13636
- */
13637
- attempts: number().int().nonnegative(),
13638
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13639
- succeeded: number().int().nonnegative(),
13640
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13641
- reasons: array(FailureReasonCountSchema).readonly()
13642
- });
13643
- method(_void(), array(FailureContributionSchema).readonly());
13644
- var LoadContributionSchema = object({
13645
- role: _enum([
13646
- "decode",
13647
- "transcode",
13648
- "recording",
13649
- "streaming",
13650
- "detection"
13651
- ]),
13652
- /**
13653
- * The NUMERIC device id — the same value every log line carries as
13654
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13655
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13656
- * contributor that cannot name its camera must not emit the entry at all,
13657
- * because an unnamed per-camera entry is indistinguishable from a shared one
13658
- * and would quietly turn one camera's cost into everybody's.
13659
- */
13660
- deviceId: number().int().positive().nullable(),
13661
- attribution: _enum([
13662
- "measured",
13663
- "accounted",
13664
- "unattributable"
13665
- ]),
13666
- /**
13667
- * What ONE entry is, in the contributor's own words — `615/high`,
13668
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13669
- * family and inventing a common one would lose the only information that
13670
- * makes two entries for the same camera distinguishable.
13671
- */
13672
- unit: string(),
13673
- /**
13674
- * The OS process this cost lives in, when there is one. Present so a
13675
- * consumer can (a) tell two generations of the same unit apart across a
13676
- * restart, and (b) subtract claimed processes from the node's process
13677
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13678
- * process of its own.
13679
- */
13680
- pid: number().int().positive().optional(),
13681
- /**
13682
- * When this generation started. The pid's incarnation marker: a consumer
13683
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13684
- * window when this changes, because the counter restarted from zero in a new
13685
- * process.
13686
- */
13687
- startedAtMs: number().optional(),
13688
- /**
13689
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13690
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13691
- * contribution is asked for.
13692
- *
13693
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13694
- * needs a sampler, and a new per-node sampler is the defect half of
13695
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13696
- * by whoever already keeps a history; a rate cannot be un-averaged.
13697
- *
13698
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13699
- * an entry with no process.
13700
- */
13701
- cpuSeconds: number().optional(),
13702
- /** Resident bytes of this unit's process, same source and same rules. */
13703
- rssBytes: number().optional()
13704
- });
13705
- method(_void(), array(LoadContributionSchema).readonly());
13706
- /**
13707
13707
  * `login-method` — collection cap through which auth addons contribute
13708
13708
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13709
13709
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18380,12 +18380,53 @@ var MediaFileKindEnum = _enum([
18380
18380
  "keyFrameSmall",
18381
18381
  "thumbnailSmall"
18382
18382
  ]);
18383
+ /**
18384
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18385
+ * ARE — never the bytes themselves.
18386
+ *
18387
+ * ## Why `url` and not `base64`
18388
+ *
18389
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18390
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18391
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18392
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18393
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18394
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18395
+ *
18396
+ * `url` points at the `event-media` data plane
18397
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18398
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18399
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18400
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18401
+ * no less protected than they were inside a `view`-level cap response — see
18402
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18403
+ * (per-device scoping).
18404
+ *
18405
+ * The URL is built from the row's **stored** key, which is not always its
18406
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18407
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18408
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18409
+ *
18410
+ * ## `base64` is TRANSITIONAL and is going away
18411
+ *
18412
+ * It is still populated for one reason: the deployed viewer's track-detail
18413
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18414
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18415
+ * triangle — not as absence. Removing the field before that viewer ships is an
18416
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18417
+ * delete this line and the `withBytes` pass-through in
18418
+ * `analytics-query-facade.ts`; nothing else reads it.
18419
+ */
18383
18420
  var MediaFileSchema = object({
18384
18421
  key: string(),
18385
18422
  kind: MediaFileKindEnum,
18386
- base64: string(),
18387
18423
  sizeBytes: number(),
18388
18424
  timestamp: number()
18425
+ }).extend({
18426
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18427
+ url: string(),
18428
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18429
+ base64: string()
18389
18430
  });
18390
18431
  /**
18391
18432
  * One media row WITHOUT its bytes.
@@ -18397,7 +18438,9 @@ var MediaFileSchema = object({
18397
18438
  * blocks the whole view.
18398
18439
  *
18399
18440
  * `sizeBytes` is carried because it is what lets a client decide between the
18400
- * stored blob and a `?variant=thumb` rendering without fetching either.
18441
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18442
+ * `url` because a client that had to build the plane path itself is a second
18443
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18401
18444
  */
18402
18445
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18403
18446
  /**
@@ -18744,6 +18787,50 @@ var EventStoreFootprintSchema = object({
18744
18787
  totalBytes: number().int(),
18745
18788
  devices: array(EventStoreDeviceFootprintSchema).readonly()
18746
18789
  });
18790
+ /** Event-media footprint for one {@link MediaFileKind}. */
18791
+ var EventMediaKindFootprintSchema = object({
18792
+ kind: MediaFileKindEnum,
18793
+ /** Media rows of this kind. */
18794
+ rows: number().int(),
18795
+ /** Bytes on disk held by those rows. */
18796
+ bytes: number().int()
18797
+ });
18798
+ /**
18799
+ * The media footprint broken down by KIND — the axis a deletion decision
18800
+ * actually turns on.
18801
+ *
18802
+ * A byte total says how much there is; it cannot say what is safe to remove.
18803
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
18804
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
18805
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
18806
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
18807
+ * nothing else, so sizing a deletion means summing per kind.
18808
+ *
18809
+ * ## Why `unaccounted*` exists
18810
+ *
18811
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
18812
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
18813
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
18814
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
18815
+ * retired code path, or by a version that knew a kind this one does not) would
18816
+ * otherwise vanish from the total silently, and an operator would delete
18817
+ * against a denominator smaller than the disk.
18818
+ *
18819
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
18820
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
18821
+ */
18822
+ var EventMediaKindBreakdownSchema = object({
18823
+ /** Every media row in scope, from one unfiltered aggregate. */
18824
+ totalRows: number().int(),
18825
+ /** Every media byte in scope, from that same aggregate. */
18826
+ totalBytes: number().int(),
18827
+ /** Per-kind footprint, ordered by bytes descending. */
18828
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
18829
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
18830
+ unaccountedRows: number().int(),
18831
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
18832
+ unaccountedBytes: number().int()
18833
+ });
18747
18834
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
18748
18835
  var EventPruneCountsSchema = object({
18749
18836
  motion: number().int(),
@@ -18947,6 +19034,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18947
19034
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
18948
19035
  kind: "query",
18949
19036
  auth: "admin"
19037
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19038
+ kind: "query",
19039
+ auth: "admin"
18950
19040
  }), method(object({
18951
19041
  olderThanMs: number(),
18952
19042
  reason: OpsLogReasonSchema.optional()
@@ -19086,6 +19176,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19086
19176
  }), array(MediaFileSchema).readonly()), method(object({
19087
19177
  trackId: string(),
19088
19178
  deviceId: number()
19179
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19180
+ eventId: string(),
19181
+ deviceId: number()
19089
19182
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19090
19183
  kind: "mutation",
19091
19184
  auth: "admin"
@@ -20895,6 +20988,20 @@ method(object({
20895
20988
  error: string().optional()
20896
20989
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
20897
20990
  providerId: string(),
20991
+ /**
20992
+ * The location this config is an UNSAVED edit of, when there is one.
20993
+ *
20994
+ * `listLocations` replaces every declared secret with the redaction
20995
+ * sentinel, so the edit modal's form state holds the sentinel for any
20996
+ * credential the operator did not retype — and posting that here
20997
+ * without a way to resolve it makes the provider try to authenticate
20998
+ * as `__camstack_redacted__` and report the operator's own working
20999
+ * password as wrong. Given this id, the orchestrator restores each
21000
+ * sentinel from the stored config (same rule as `upsertLocation`)
21001
+ * before dispatching. Omitted by the "Add location" wizard, where
21002
+ * every value was typed just now and nothing is stored yet.
21003
+ */
21004
+ locationId: string().optional(),
20898
21005
  config: record(string(), unknown())
20899
21006
  }), object({
20900
21007
  ok: boolean(),
@@ -23346,10 +23453,24 @@ var FaceClusterSchema = object({
23346
23453
  size: number().int(),
23347
23454
  cohesion: number()
23348
23455
  });
23456
+ /**
23457
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23458
+ * are — never the bytes.
23459
+ *
23460
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23461
+ * track/event contract) is still populated because a deployed viewer requires
23462
+ * the field to parse a row at all; this method has no such reader. Its ONE
23463
+ * caller is the admin UI's detail modal, which was building
23464
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23465
+ * dialog already rendering its key FRAME from the `event-media` plane.
23466
+ *
23467
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23468
+ * media key directly, so this needed no new plane and no new access decision.
23469
+ */
23349
23470
  var MediaFileLiteSchema$1 = object({
23350
23471
  key: string(),
23351
23472
  kind: string(),
23352
- base64: string(),
23473
+ url: string(),
23353
23474
  sizeBytes: number(),
23354
23475
  timestamp: number()
23355
23476
  });
@@ -25662,10 +25783,24 @@ var PlateInfoSchema = object({
25662
25783
  */
25663
25784
  cropUrl: string().optional()
25664
25785
  });
25786
+ /**
25787
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25788
+ * are — never the bytes.
25789
+ *
25790
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25791
+ * track/event contract) is still populated because a deployed viewer requires
25792
+ * the field to parse a row at all; this method has no such reader. Its ONE
25793
+ * caller is the admin UI's detail modal, which was building
25794
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25795
+ * dialog already rendering its key FRAME from the `event-media` plane.
25796
+ *
25797
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25798
+ * media key directly, so this needed no new plane and no new access decision.
25799
+ */
25665
25800
  var MediaFileLiteSchema = object({
25666
25801
  key: string(),
25667
25802
  kind: string(),
25668
- base64: string(),
25803
+ url: string(),
25669
25804
  sizeBytes: number(),
25670
25805
  timestamp: number()
25671
25806
  });
@@ -31582,6 +31717,12 @@ Object.freeze({
31582
31717
  addonId: null,
31583
31718
  access: "view"
31584
31719
  },
31720
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
31721
+ capName: "pipeline-analytics",
31722
+ capScope: "device",
31723
+ addonId: null,
31724
+ access: "view"
31725
+ },
31585
31726
  "pipelineAnalytics.getEventStoreFootprint": {
31586
31727
  capName: "pipeline-analytics",
31587
31728
  capScope: "device",
@@ -31678,6 +31819,12 @@ Object.freeze({
31678
31819
  addonId: null,
31679
31820
  access: "view"
31680
31821
  },
31822
+ "pipelineAnalytics.listEventMedia": {
31823
+ capName: "pipeline-analytics",
31824
+ capScope: "device",
31825
+ addonId: null,
31826
+ access: "view"
31827
+ },
31681
31828
  "pipelineAnalytics.listGroups": {
31682
31829
  capName: "pipeline-analytics",
31683
31830
  capScope: "device",
@@ -35241,6 +35388,11 @@ Object.freeze({
35241
35388
  form: "single",
35242
35389
  optional: false
35243
35390
  }],
35391
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
35392
+ name: "deviceId",
35393
+ form: "single",
35394
+ optional: true
35395
+ }],
35244
35396
  "pipelineAnalytics.getGroup": [{
35245
35397
  name: "deviceId",
35246
35398
  form: "single",
@@ -35301,6 +35453,11 @@ Object.freeze({
35301
35453
  form: "array",
35302
35454
  optional: false
35303
35455
  }],
35456
+ "pipelineAnalytics.listEventMedia": [{
35457
+ name: "deviceId",
35458
+ form: "single",
35459
+ optional: false
35460
+ }],
35304
35461
  "pipelineAnalytics.listGroups": [{
35305
35462
  name: "deviceIds",
35306
35463
  form: "array",
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-B81Rw6ym.js");
5
+ const require_dist = require("../dist-D4Kv7xGB.js");
6
6
  let node_crypto = require("node:crypto");
7
7
  let node_child_process = require("node:child_process");
8
8
  //#region src/ingress/tailscale-cli.ts
@@ -1,4 +1,4 @@
1
- import { a as EventCategory, i as BaseAddon, n as networkAccessCapability } from "../dist-Tyu2rQxG.mjs";
1
+ import { a as EventCategory, i as BaseAddon, n as networkAccessCapability } from "../dist-r9nSS25r.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { execFile } from "node:child_process";
4
4
  import { promisify } from "node:util";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-tailscale",
3
- "version": "1.2.46",
3
+ "version": "1.2.48",
4
4
  "description": "Tailscale bundle for CamStack — joins the host to a tailnet (client) and exposes the hub via Serve/Funnel (ingress). Multi-entry npm package shipping 2 addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",