@camstack/addon-tailscale 1.2.46 → 1.2.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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-CJS8Mo7l.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-BO7-LQSt.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
  /**
@@ -19086,6 +19129,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19086
19129
  }), array(MediaFileSchema).readonly()), method(object({
19087
19130
  trackId: string(),
19088
19131
  deviceId: number()
19132
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19133
+ eventId: string(),
19134
+ deviceId: number()
19089
19135
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19090
19136
  kind: "mutation",
19091
19137
  auth: "admin"
@@ -23346,10 +23392,24 @@ var FaceClusterSchema = object({
23346
23392
  size: number().int(),
23347
23393
  cohesion: number()
23348
23394
  });
23395
+ /**
23396
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23397
+ * are — never the bytes.
23398
+ *
23399
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23400
+ * track/event contract) is still populated because a deployed viewer requires
23401
+ * the field to parse a row at all; this method has no such reader. Its ONE
23402
+ * caller is the admin UI's detail modal, which was building
23403
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23404
+ * dialog already rendering its key FRAME from the `event-media` plane.
23405
+ *
23406
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23407
+ * media key directly, so this needed no new plane and no new access decision.
23408
+ */
23349
23409
  var MediaFileLiteSchema$1 = object({
23350
23410
  key: string(),
23351
23411
  kind: string(),
23352
- base64: string(),
23412
+ url: string(),
23353
23413
  sizeBytes: number(),
23354
23414
  timestamp: number()
23355
23415
  });
@@ -25662,10 +25722,24 @@ var PlateInfoSchema = object({
25662
25722
  */
25663
25723
  cropUrl: string().optional()
25664
25724
  });
25725
+ /**
25726
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25727
+ * are — never the bytes.
25728
+ *
25729
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25730
+ * track/event contract) is still populated because a deployed viewer requires
25731
+ * the field to parse a row at all; this method has no such reader. Its ONE
25732
+ * caller is the admin UI's detail modal, which was building
25733
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25734
+ * dialog already rendering its key FRAME from the `event-media` plane.
25735
+ *
25736
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25737
+ * media key directly, so this needed no new plane and no new access decision.
25738
+ */
25665
25739
  var MediaFileLiteSchema = object({
25666
25740
  key: string(),
25667
25741
  kind: string(),
25668
- base64: string(),
25742
+ url: string(),
25669
25743
  sizeBytes: number(),
25670
25744
  timestamp: number()
25671
25745
  });
@@ -31678,6 +31752,12 @@ Object.freeze({
31678
31752
  addonId: null,
31679
31753
  access: "view"
31680
31754
  },
31755
+ "pipelineAnalytics.listEventMedia": {
31756
+ capName: "pipeline-analytics",
31757
+ capScope: "device",
31758
+ addonId: null,
31759
+ access: "view"
31760
+ },
31681
31761
  "pipelineAnalytics.listGroups": {
31682
31762
  capName: "pipeline-analytics",
31683
31763
  capScope: "device",
@@ -35301,6 +35381,11 @@ Object.freeze({
35301
35381
  form: "array",
35302
35382
  optional: false
35303
35383
  }],
35384
+ "pipelineAnalytics.listEventMedia": [{
35385
+ name: "deviceId",
35386
+ form: "single",
35387
+ optional: false
35388
+ }],
35304
35389
  "pipelineAnalytics.listGroups": [{
35305
35390
  name: "deviceIds",
35306
35391
  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
  /**
@@ -19086,6 +19129,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19086
19129
  }), array(MediaFileSchema).readonly()), method(object({
19087
19130
  trackId: string(),
19088
19131
  deviceId: number()
19132
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19133
+ eventId: string(),
19134
+ deviceId: number()
19089
19135
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19090
19136
  kind: "mutation",
19091
19137
  auth: "admin"
@@ -23346,10 +23392,24 @@ var FaceClusterSchema = object({
23346
23392
  size: number().int(),
23347
23393
  cohesion: number()
23348
23394
  });
23395
+ /**
23396
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23397
+ * are — never the bytes.
23398
+ *
23399
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23400
+ * track/event contract) is still populated because a deployed viewer requires
23401
+ * the field to parse a row at all; this method has no such reader. Its ONE
23402
+ * caller is the admin UI's detail modal, which was building
23403
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23404
+ * dialog already rendering its key FRAME from the `event-media` plane.
23405
+ *
23406
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23407
+ * media key directly, so this needed no new plane and no new access decision.
23408
+ */
23349
23409
  var MediaFileLiteSchema$1 = object({
23350
23410
  key: string(),
23351
23411
  kind: string(),
23352
- base64: string(),
23412
+ url: string(),
23353
23413
  sizeBytes: number(),
23354
23414
  timestamp: number()
23355
23415
  });
@@ -25662,10 +25722,24 @@ var PlateInfoSchema = object({
25662
25722
  */
25663
25723
  cropUrl: string().optional()
25664
25724
  });
25725
+ /**
25726
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25727
+ * are — never the bytes.
25728
+ *
25729
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25730
+ * track/event contract) is still populated because a deployed viewer requires
25731
+ * the field to parse a row at all; this method has no such reader. Its ONE
25732
+ * caller is the admin UI's detail modal, which was building
25733
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25734
+ * dialog already rendering its key FRAME from the `event-media` plane.
25735
+ *
25736
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25737
+ * media key directly, so this needed no new plane and no new access decision.
25738
+ */
25665
25739
  var MediaFileLiteSchema = object({
25666
25740
  key: string(),
25667
25741
  kind: string(),
25668
- base64: string(),
25742
+ url: string(),
25669
25743
  sizeBytes: number(),
25670
25744
  timestamp: number()
25671
25745
  });
@@ -31678,6 +31752,12 @@ Object.freeze({
31678
31752
  addonId: null,
31679
31753
  access: "view"
31680
31754
  },
31755
+ "pipelineAnalytics.listEventMedia": {
31756
+ capName: "pipeline-analytics",
31757
+ capScope: "device",
31758
+ addonId: null,
31759
+ access: "view"
31760
+ },
31681
31761
  "pipelineAnalytics.listGroups": {
31682
31762
  capName: "pipeline-analytics",
31683
31763
  capScope: "device",
@@ -35301,6 +35381,11 @@ Object.freeze({
35301
35381
  form: "array",
35302
35382
  optional: false
35303
35383
  }],
35384
+ "pipelineAnalytics.listEventMedia": [{
35385
+ name: "deviceId",
35386
+ form: "single",
35387
+ optional: false
35388
+ }],
35304
35389
  "pipelineAnalytics.listGroups": [{
35305
35390
  name: "deviceIds",
35306
35391
  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-CJS8Mo7l.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-BO7-LQSt.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.47",
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",