@camstack/addon-post-analysis 1.2.151 → 1.2.152

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.
@@ -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-DpQno5NK.js");
5
+ const require_dist = require("../dist-Bw6cidiJ.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -5413,12 +5413,6 @@ function buildAlarmButtons(input) {
5413
5413
  }];
5414
5414
  }
5415
5415
  //#endregion
5416
- //#region src/notification-center/cap-caller.ts
5417
- function requireCapCaller(method, caller) {
5418
- if (caller === void 0 || typeof caller.userId !== "string" || caller.userId.length === 0) throw new Error(`notification-rules.${method} declares caller: 'required' and this call carried no caller identity — an addon-to-addon cap call has no authenticated user`);
5419
- return caller;
5420
- }
5421
- //#endregion
5422
5416
  //#region src/notification-center/audio-rule-matcher.ts
5423
5417
  /**
5424
5418
  * AudioWatcher — the Notification Center's matcher for audio rules
@@ -5661,6 +5655,12 @@ var AudioWatcher = class {
5661
5655
  }
5662
5656
  };
5663
5657
  //#endregion
5658
+ //#region src/notification-center/cap-caller.ts
5659
+ function requireCapCaller(method, caller) {
5660
+ if (caller === void 0 || typeof caller.userId !== "string" || caller.userId.length === 0) throw new Error(`notification-rules.${method} declares caller: 'required' and this call carried no caller identity — an addon-to-addon cap call has no authenticated user`);
5661
+ return caller;
5662
+ }
5663
+ //#endregion
5664
5664
  //#region src/shared/llm-vision/prompt-hygiene.ts
5665
5665
  /**
5666
5666
  * What a PIPELINE VALUE is allowed to look like once it is inside a prompt.
@@ -6295,25 +6295,46 @@ var ADDON_UPDATE_KINDS = new Set(["addon-update-available"]);
6295
6295
  /** Kinds whose action is "go and apply a NODE's own (`@camstack/server`) update". */
6296
6296
  var NODE_UPDATE_KINDS = new Set(["server-update-available"]);
6297
6297
  /**
6298
- * Kinds whose action is "go and look at why this camera stopped producing".
6298
+ * Kinds whose action is "go and look at this device's stream".
6299
+ *
6300
+ * BOTH DIRECTIONS. A recovery lands on the same page as the failure it undoes,
6301
+ * because "did it really come back, and what did it do while it was gone" is
6302
+ * an action and the page that answers it is the one the down-notification
6303
+ * already opened. On 2026-08-30 the operator's phone showed "Il dispositivo
6304
+ * Videocamera sala feste è online" with `tapAction.value: null` — the same
6305
+ * camera's `device-offline` an hour earlier opened the Stream Broker tab, and
6306
+ * the recovery opened nothing, purely because `device-online` had no entry
6307
+ * here.
6299
6308
  *
6300
- * The legacy `camera-offline` is here for the same reason it is still in the
6301
- * enum: outbox and history rows enqueued before the rename carry it, and an
6309
+ * The legacy `camera-*` pair is here for the same reason both are still in the
6310
+ * enum: outbox and history rows enqueued before the rename carry them, and an
6302
6311
  * old row is exactly the one whose action has to keep working.
6312
+ *
6313
+ * Kinds NOT here are kinds whose DOWN direction has no action either
6314
+ * (`node-online`/`node-offline`, `device-enabled`/`device-disabled`) — a
6315
+ * recovery does not earn a destination its failure was never given.
6303
6316
  */
6304
- var DEVICE_DOWN_KINDS = new Set([
6317
+ var DEVICE_STREAM_KINDS = new Set([
6305
6318
  "device-offline",
6319
+ "device-online",
6306
6320
  "stream-offline",
6307
- "camera-offline"
6321
+ "stream-online",
6322
+ "camera-offline",
6323
+ "camera-online"
6308
6324
  ]);
6309
6325
  /**
6310
6326
  * Kinds whose subject is a camera BY CONSTRUCTION, whatever the row says.
6311
6327
  *
6312
- * A stream belongs to a camera and nothing else. The legacy `camera-offline`
6313
- * is one too — that name is precisely the claim it made, and it is why the
6314
- * kind was split into `device-offline` + a `deviceTypes` condition.
6328
+ * A stream belongs to a camera and nothing else. The legacy `camera-*` pair is
6329
+ * too — that name is precisely the claim it made, and it is why the kind was
6330
+ * split into `device-*` + a `deviceTypes` condition.
6315
6331
  */
6316
- var CAMERA_BY_CONSTRUCTION_KINDS = new Set(["stream-offline", "camera-offline"]);
6332
+ var CAMERA_BY_CONSTRUCTION_KINDS = new Set([
6333
+ "stream-offline",
6334
+ "stream-online",
6335
+ "camera-offline",
6336
+ "camera-online"
6337
+ ]);
6317
6338
  function withParam(path, key, value) {
6318
6339
  if (value === void 0 || value.length === 0) return path;
6319
6340
  return `${path}?${key}=${encodeURIComponent(value)}`;
@@ -6328,7 +6349,7 @@ function withParam(path, key, value) {
6328
6349
  function systemEventTapPath(subject) {
6329
6350
  if (ADDON_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_ADDONS_PATH, NODE_PARAM, subject.nodeId);
6330
6351
  if (NODE_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_CLUSTER_PATH, NODE_PARAM, subject.nodeId);
6331
- if (DEVICE_DOWN_KINDS.has(subject.kind)) {
6352
+ if (DEVICE_STREAM_KINDS.has(subject.kind)) {
6332
6353
  const deviceId = subject.deviceId;
6333
6354
  if (deviceId === void 0 || !Number.isInteger(deviceId) || deviceId <= 0) return null;
6334
6355
  const devicePath = `${ADMIN_DEVICES_PATH}/${String(deviceId)}`;
@@ -10730,16 +10751,26 @@ function incomingFromUpdateAvailable(facts, data) {
10730
10751
  ...nodeIds.length > 0 ? { nodeIds } : {}
10731
10752
  });
10732
10753
  }
10733
- /** Version-bearing `addon.updated` only — settings saves carry neither. */
10754
+ /**
10755
+ * Version-bearing `addon.updated` only — a settings save carries no versions
10756
+ * at all (`BaseAddon.updateGlobalSettings`).
10757
+ *
10758
+ * `toVersion` IS the discriminator, and `fromVersion` is not: the emitter
10759
+ * omits the predecessor when it cannot read what was installed (a first
10760
+ * install, an unreadable tree). Requiring both dropped those updates on the
10761
+ * floor entirely — the operator got no notification for an update that
10762
+ * happened — which is the price that was being paid to keep `'0.0.0'`
10763
+ * out. Report the half that is known; say nothing about the half that is not.
10764
+ */
10734
10765
  function parseAddonUpdateComplete(data) {
10735
10766
  const packageName = stringField(data, "packageName");
10736
10767
  const fromVersion = stringField(data, "fromVersion");
10737
10768
  const toVersion = stringField(data, "toVersion");
10738
- if (packageName === void 0 || fromVersion === void 0 || toVersion === void 0) return null;
10769
+ if (packageName === void 0 || toVersion === void 0) return null;
10739
10770
  const nodeId = stringField(data, "agent") ?? stringField(data, "nodeId");
10740
10771
  return {
10741
10772
  packageName,
10742
- fromVersion,
10773
+ ...fromVersion !== void 0 ? { fromVersion } : {},
10743
10774
  toVersion,
10744
10775
  ...nodeId !== void 0 ? { nodeId } : {}
10745
10776
  };
@@ -10749,7 +10780,10 @@ function incomingFromAddonUpdates(facts, updates) {
10749
10780
  const packageNames = unique(updates.map((update) => update.packageName));
10750
10781
  const nodeIds = unique(updates.map((update) => update.nodeId).filter((id) => id !== void 0));
10751
10782
  const signature = updates.map((update) => `${update.nodeId ?? ""}:${update.packageName}@${update.toVersion}`).toSorted().join(",");
10752
- const list = updates.map((update) => update.nodeId !== void 0 ? `${update.packageName} ${update.fromVersion} → ${update.toVersion} (${update.nodeId})` : `${update.packageName} ${update.fromVersion} → ${update.toVersion}`).join("\n");
10783
+ const list = updates.map((update) => {
10784
+ const transition = update.fromVersion !== void 0 ? `${update.fromVersion} → ${update.toVersion}` : `→ ${update.toVersion}`;
10785
+ return update.nodeId !== void 0 ? `${update.packageName} ${transition} (${update.nodeId})` : `${update.packageName} ${transition}`;
10786
+ }).join("\n");
10753
10787
  const head = updates[0];
10754
10788
  return systemSubject({
10755
10789
  facts,
@@ -23586,17 +23620,17 @@ function planRuleTest(input) {
23586
23620
  deviceName: `camera ${String(systemScope.deviceId ?? deviceId)}`,
23587
23621
  nodeId: systemScope.nodeId ?? "hub",
23588
23622
  packageName: systemScope.packageName ?? "@camstack/server",
23589
- currentVersion: "0.0.0",
23590
- latestVersion: "0.0.0",
23623
+ currentVersion: "1.0.0",
23624
+ latestVersion: "1.0.1",
23591
23625
  profile: "low",
23592
23626
  seconds: "30",
23593
23627
  devices: "cuda:0",
23594
23628
  minutes: "15",
23595
23629
  addons: TEST_CRASHED_RUNNER,
23596
- list: "@camstack/addon-pipeline 1.0.0 (current 0.0.0)",
23630
+ list: "@camstack/addon-pipeline 1.0.1 (current 1.0.0)",
23597
23631
  nodes: "hub",
23598
- fromVersion: "0.0.0",
23599
- toVersion: "0.0.1",
23632
+ fromVersion: "1.0.0",
23633
+ toVersion: "1.0.1",
23600
23634
  title: "Export",
23601
23635
  count: "1"
23602
23636
  },
@@ -25746,7 +25780,7 @@ var MediaStore = class {
25746
25780
  });
25747
25781
  }
25748
25782
  /** Persist a media blob and register its metadata row. Returns the
25749
- * collection key (MediaFile.key) that callers store as reference. */
25783
+ * collection key (MediaFileRef.key) that callers store as reference. */
25750
25784
  async put(params) {
25751
25785
  if (this.writeGate) return this.writeGate.run(() => this.putUnchecked(params));
25752
25786
  return this.putUnchecked(params);
@@ -26068,8 +26102,20 @@ var MediaStore = class {
26068
26102
  * its device at write time, so scoping the query by it makes a media read
26069
26103
  * for a device the caller cannot name return nothing — the DB itself
26070
26104
  * enforces the boundary, no post-hoc ownership check to forget.
26105
+ *
26106
+ * `kinds` is the WORK filter, and it belongs here rather than on the answer.
26107
+ * Every row this returns has had its blob read off disk and base64-encoded,
26108
+ * so a caller that filters afterwards has already paid for every kind it is
26109
+ * about to discard: measured on the live hub, one track's six kinds are
26110
+ * 3 597 219 bytes and `thumbnailSmall` is 31 155 of them, and asking for the
26111
+ * small one alone cost exactly as much as asking for all six. `kind` is a
26112
+ * real column (see {@link MEDIA_COLUMNS}), so the narrowing happens in the
26113
+ * query and the blobs of the other kinds are never opened.
26114
+ *
26115
+ * Absent (or empty) means every kind — the back-compat default, and the
26116
+ * honest answer for a caller that has not decided what it needs.
26071
26117
  */
26072
- async listByOwner(ownerKind, ownerId, deviceId) {
26118
+ async listByOwner(ownerKind, ownerId, deviceId, kinds) {
26073
26119
  const rows = await this.store.query.query({
26074
26120
  collection: MEDIA_COLLECTION,
26075
26121
  filter: {
@@ -26081,6 +26127,7 @@ var MediaStore = class {
26081
26127
  ownerId,
26082
26128
  deviceId
26083
26129
  },
26130
+ ...kinds !== void 0 && kinds.length > 0 ? { whereIn: { kind: [...kinds] } } : {},
26084
26131
  orderBy: {
26085
26132
  field: "timestamp",
26086
26133
  direction: "asc"
@@ -27997,6 +28044,63 @@ var embeddingActions = require_dist.defineCustomActions({
27997
28044
  "embedding.reembedFacesStatus": require_dist.customAction(NoInputSchema$1, ReembedFacesStatusSchema)
27998
28045
  });
27999
28046
  //#endregion
28047
+ //#region src/pipeline-analytics/media-file-url.ts
28048
+ /**
28049
+ * The plane path for one stored media key.
28050
+ *
28051
+ * `prefix` is `/addon/<addonId>/event-media` — a pure function of the addon
28052
+ * id, so it exists whether or not `dataPlane.serve` succeeded. A row that
28053
+ * named no address at all would be the silent drop this repo keeps paying for:
28054
+ * a client cannot tell "no bytes" from "no plane", and renders neither.
28055
+ */
28056
+ function mediaFileUrl(prefix, key) {
28057
+ return `${prefix}/${encodeURIComponent(key)}`;
28058
+ }
28059
+ /**
28060
+ * A stored row as the wire sees it: the ref, the address, and — transitionally
28061
+ * — the bytes.
28062
+ *
28063
+ * `base64` is copied through ONLY because the deployed viewer's track-detail
28064
+ * hero still requires the field to parse a row at all. See the `MediaFile`
28065
+ * docblock in `pipeline-analytics.cap.ts`; deleting the field is a one-line
28066
+ * change here and one in the schema.
28067
+ *
28068
+ * `provenance` is deliberately NOT spread: it is store-internal, and the
28069
+ * previous `{ ...row, kind }` shape published it by accident.
28070
+ */
28071
+ function toWireMediaFile(row, prefix) {
28072
+ return {
28073
+ key: row.key,
28074
+ kind: row.kind,
28075
+ sizeBytes: row.sizeBytes,
28076
+ timestamp: row.timestamp,
28077
+ url: mediaFileUrl(prefix, row.key),
28078
+ base64: row.base64
28079
+ };
28080
+ }
28081
+ /** The same row without its bytes — the manifest shape. Carries the SAME
28082
+ * `url` as the byte read, so a client never builds the plane path itself. */
28083
+ function toWireMediaFileInfo(row, prefix) {
28084
+ return {
28085
+ key: row.key,
28086
+ kind: row.kind,
28087
+ sizeBytes: row.sizeBytes,
28088
+ timestamp: row.timestamp,
28089
+ url: mediaFileUrl(prefix, row.key)
28090
+ };
28091
+ }
28092
+ //#endregion
28093
+ //#region src/pipeline-analytics/gallery-media-row.ts
28094
+ function toGalleryMediaRow(row, prefix) {
28095
+ return {
28096
+ key: row.key,
28097
+ kind: row.kind,
28098
+ url: mediaFileUrl(prefix, row.key),
28099
+ sizeBytes: row.sizeBytes,
28100
+ timestamp: row.timestamp
28101
+ };
28102
+ }
28103
+ //#endregion
28000
28104
  //#region src/pipeline-analytics/include-crops-default.ts
28001
28105
  function resolveIncludeCrops(input) {
28002
28106
  return input?.includeCrops === true;
@@ -29656,6 +29760,7 @@ var FaceGalleryProvider = class {
29656
29760
  logger;
29657
29761
  refreshGallery;
29658
29762
  eventMediaBaseUrl;
29763
+ eventMediaPathPrefix;
29659
29764
  emitFaceGalleryChanged;
29660
29765
  constructor(deps) {
29661
29766
  this.identityStore = deps.identityStore;
@@ -29666,6 +29771,7 @@ var FaceGalleryProvider = class {
29666
29771
  this.logger = deps.logger;
29667
29772
  this.refreshGallery = deps.refreshGallery;
29668
29773
  this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
29774
+ this.eventMediaPathPrefix = deps.eventMediaPathPrefix;
29669
29775
  this.emitFaceGalleryChanged = deps.emitFaceGalleryChanged;
29670
29776
  }
29671
29777
  /** Best-effort emit — never allowed to fail the mutation it follows. */
@@ -29853,13 +29959,7 @@ var FaceGalleryProvider = class {
29853
29959
  * the enrolled identity copies — those live under identity owner).
29854
29960
  */
29855
29961
  async getFaceMedia(input) {
29856
- return (await this.mediaStore.listByOwner("face", input.faceId)).map((m) => ({
29857
- key: m.key,
29858
- kind: m.kind,
29859
- base64: m.base64,
29860
- sizeBytes: m.sizeBytes,
29861
- timestamp: m.timestamp
29862
- }));
29962
+ return (await this.mediaStore.listByOwner("face", input.faceId)).map((m) => toGalleryMediaRow(m, this.eventMediaPathPrefix()));
29863
29963
  }
29864
29964
  /**
29865
29965
  * Assign a buffered face to an identity:
@@ -32572,50 +32672,136 @@ var FrameProcessor = class {
32572
32672
  };
32573
32673
  }
32574
32674
  };
32575
- //#endregion
32576
- //#region src/pipeline-analytics/replay/frame-processor-pass.ts
32675
+ /** Bounded trackId → pending-upgrade map. Insertion-ordered, so the cap
32676
+ * evicts the oldest record. */
32677
+ var PendingPreviewUpgrades = class {
32678
+ entries = /* @__PURE__ */ new Map();
32679
+ ttlMs;
32680
+ maxEntries;
32681
+ now;
32682
+ constructor(options = {}) {
32683
+ this.ttlMs = options.ttlMs ?? 9e5;
32684
+ this.maxEntries = Math.max(1, options.maxEntries ?? 512);
32685
+ this.now = options.now ?? (() => Date.now());
32686
+ }
32687
+ /** Record (or replace) a closed track's pending upgrade. Prunes expired
32688
+ * entries first, then evicts the oldest while over the cap. */
32689
+ record(entry) {
32690
+ this.prune();
32691
+ this.entries.delete(entry.trackId);
32692
+ this.entries.set(entry.trackId, entry);
32693
+ while (this.entries.size > this.maxEntries) {
32694
+ const oldest = this.entries.keys().next();
32695
+ if (oldest.done === true) break;
32696
+ this.entries.delete(oldest.value);
32697
+ }
32698
+ }
32699
+ /** Claim a track's pending upgrade — removes it, so a second landing (or a
32700
+ * concurrent racer) finds nothing and the tile is rewritten at most once. */
32701
+ take(trackId) {
32702
+ this.prune();
32703
+ const entry = this.entries.get(trackId);
32704
+ if (entry === void 0) return void 0;
32705
+ this.entries.delete(trackId);
32706
+ return entry;
32707
+ }
32708
+ get size() {
32709
+ return this.entries.size;
32710
+ }
32711
+ prune() {
32712
+ const cutoff = this.now() - this.ttlMs;
32713
+ for (const [trackId, entry] of this.entries) if (entry.recordedAtMs < cutoff) this.entries.delete(trackId);
32714
+ }
32715
+ };
32577
32716
  /**
32578
- * Run a FRESH `FrameProcessor` over a fixed, ordered sequence of frames and
32579
- * return the tracks it produced.
32580
- *
32581
- * No `TrackStore`, no bus, no media — a track here is a summary built
32582
- * ENTIRELY from `FrameProcessor.process()`'s in-memory return value.
32717
+ * Redeem a closed track's pending preview off a keyFrame that landed late.
32718
+ * Every non-`upgraded` outcome that had a pending entry logs itself — a
32719
+ * branch that leaves the stand-in in place must say so.
32583
32720
  */
32584
- function runFrameProcessorPass(input) {
32585
- const processor = new FrameProcessor(input.deviceId, {}, {}, {}, input.source);
32586
- if (input.zones !== void 0) processor.setZones(input.zones);
32587
- if (input.detectionRules !== void 0) processor.setDetectionRules(input.detectionRules);
32588
- if (input.zoneMembershipMinOverlap !== void 0) processor.setZoneMembershipMinOverlap(input.zoneMembershipMinOverlap);
32589
- const accum = /* @__PURE__ */ new Map();
32590
- for (const { timestamp, frame } of input.frames) {
32591
- const result = processor.process({
32592
- timestamp,
32593
- frame
32721
+ async function runPreviewUpgrade(deps, input) {
32722
+ const { deviceId, trackId } = input;
32723
+ const pending = deps.takePending(trackId);
32724
+ if (pending === void 0) return "no-pending";
32725
+ const held = await deps.heldThumbnailProvenance(trackId);
32726
+ if (held !== null && mediaProvenanceRank(held) >= mediaProvenanceRank("native")) {
32727
+ deps.logger.info("late keyFrame: preview already native — no upgrade", {
32728
+ tags: { deviceId },
32729
+ meta: { trackId }
32594
32730
  });
32595
- for (const td of result.tracked) {
32596
- const existing = accum.get(td.trackId);
32597
- const matched = td.matchedThisFrame ? 1 : 0;
32598
- if (existing === void 0) accum.set(td.trackId, {
32599
- className: td.className,
32600
- firstSeenMs: timestamp,
32601
- lastSeenMs: timestamp,
32602
- bbox: { ...td.bbox },
32603
- framesMatched: matched
32604
- });
32605
- else {
32606
- existing.className = td.className;
32607
- existing.lastSeenMs = timestamp;
32608
- existing.framesMatched += matched;
32731
+ return "held-native";
32732
+ }
32733
+ if (pending.bestSeen === void 0) {
32734
+ deps.logger.info("late keyFrame landed but no subject bbox was ever seen — tile kept as-is", {
32735
+ tags: { deviceId },
32736
+ meta: {
32737
+ trackId,
32738
+ heldProvenance: held
32609
32739
  }
32610
- }
32740
+ });
32741
+ return "no-bbox";
32611
32742
  }
32612
- return { tracks: [...accum.values()].map((t) => ({
32613
- className: t.className,
32614
- firstSeenMs: t.firstSeenMs,
32615
- lastSeenMs: t.lastSeenMs,
32616
- bbox: t.bbox,
32617
- framesMatched: t.framesMatched
32618
- })) };
32743
+ if (pending.bestSeen.timestamp === void 0) {
32744
+ deps.logger.info("late keyFrame: pending entry predates the motion guard — tile kept as-is", {
32745
+ tags: { deviceId },
32746
+ meta: {
32747
+ trackId,
32748
+ keyFrameKey: input.keyFrameKey,
32749
+ heldProvenance: held
32750
+ }
32751
+ });
32752
+ return "no-bbox";
32753
+ }
32754
+ const derived = await deps.derive({
32755
+ deviceId,
32756
+ mediaKey: input.keyFrameKey,
32757
+ bbox: {
32758
+ x: pending.bestSeen.x,
32759
+ y: pending.bestSeen.y,
32760
+ w: pending.bestSeen.w,
32761
+ h: pending.bestSeen.h
32762
+ },
32763
+ frameWidth: pending.bestSeen.frameWidth,
32764
+ frameHeight: pending.bestSeen.frameHeight,
32765
+ timestamp: pending.bestSeen.timestamp
32766
+ });
32767
+ if (derived === null) {
32768
+ deps.logger.info("late keyFrame preview upgrade failed (derive miss) — tile kept as-is", {
32769
+ tags: { deviceId },
32770
+ meta: {
32771
+ trackId,
32772
+ keyFrameKey: input.keyFrameKey,
32773
+ heldProvenance: held
32774
+ }
32775
+ });
32776
+ return "derive-miss";
32777
+ }
32778
+ await deps.put({
32779
+ deviceId,
32780
+ trackId,
32781
+ kind: "thumbnail",
32782
+ timestamp: input.timestamp,
32783
+ data: derived.thumbnail,
32784
+ provenance: "native"
32785
+ });
32786
+ await deps.put({
32787
+ deviceId,
32788
+ trackId,
32789
+ kind: "thumbnailSmall",
32790
+ timestamp: input.timestamp,
32791
+ data: derived.thumbnailSmall,
32792
+ provenance: "native"
32793
+ });
32794
+ await deps.clearPreviewMiss(deviceId, trackId);
32795
+ deps.logger.info("best-shot upgraded from late keyFrame (stand-in replaced)", {
32796
+ tags: { deviceId },
32797
+ meta: {
32798
+ trackId,
32799
+ keyFrameKey: input.keyFrameKey,
32800
+ replaced: held ?? "none",
32801
+ skewMs: derived.skewMs
32802
+ }
32803
+ });
32804
+ return "upgraded";
32619
32805
  }
32620
32806
  function isPlausibleThumbnailBox(bbox, frameWidth, frameHeight, score) {
32621
32807
  if (frameWidth <= 0 || frameHeight <= 0) return true;
@@ -32775,137 +32961,6 @@ function promoteSemanticBestFrame(deps, input) {
32775
32961
  captured: true
32776
32962
  };
32777
32963
  }
32778
- /** Bounded trackId → pending-upgrade map. Insertion-ordered, so the cap
32779
- * evicts the oldest record. */
32780
- var PendingPreviewUpgrades = class {
32781
- entries = /* @__PURE__ */ new Map();
32782
- ttlMs;
32783
- maxEntries;
32784
- now;
32785
- constructor(options = {}) {
32786
- this.ttlMs = options.ttlMs ?? 9e5;
32787
- this.maxEntries = Math.max(1, options.maxEntries ?? 512);
32788
- this.now = options.now ?? (() => Date.now());
32789
- }
32790
- /** Record (or replace) a closed track's pending upgrade. Prunes expired
32791
- * entries first, then evicts the oldest while over the cap. */
32792
- record(entry) {
32793
- this.prune();
32794
- this.entries.delete(entry.trackId);
32795
- this.entries.set(entry.trackId, entry);
32796
- while (this.entries.size > this.maxEntries) {
32797
- const oldest = this.entries.keys().next();
32798
- if (oldest.done === true) break;
32799
- this.entries.delete(oldest.value);
32800
- }
32801
- }
32802
- /** Claim a track's pending upgrade — removes it, so a second landing (or a
32803
- * concurrent racer) finds nothing and the tile is rewritten at most once. */
32804
- take(trackId) {
32805
- this.prune();
32806
- const entry = this.entries.get(trackId);
32807
- if (entry === void 0) return void 0;
32808
- this.entries.delete(trackId);
32809
- return entry;
32810
- }
32811
- get size() {
32812
- return this.entries.size;
32813
- }
32814
- prune() {
32815
- const cutoff = this.now() - this.ttlMs;
32816
- for (const [trackId, entry] of this.entries) if (entry.recordedAtMs < cutoff) this.entries.delete(trackId);
32817
- }
32818
- };
32819
- /**
32820
- * Redeem a closed track's pending preview off a keyFrame that landed late.
32821
- * Every non-`upgraded` outcome that had a pending entry logs itself — a
32822
- * branch that leaves the stand-in in place must say so.
32823
- */
32824
- async function runPreviewUpgrade(deps, input) {
32825
- const { deviceId, trackId } = input;
32826
- const pending = deps.takePending(trackId);
32827
- if (pending === void 0) return "no-pending";
32828
- const held = await deps.heldThumbnailProvenance(trackId);
32829
- if (held !== null && mediaProvenanceRank(held) >= mediaProvenanceRank("native")) {
32830
- deps.logger.info("late keyFrame: preview already native — no upgrade", {
32831
- tags: { deviceId },
32832
- meta: { trackId }
32833
- });
32834
- return "held-native";
32835
- }
32836
- if (pending.bestSeen === void 0) {
32837
- deps.logger.info("late keyFrame landed but no subject bbox was ever seen — tile kept as-is", {
32838
- tags: { deviceId },
32839
- meta: {
32840
- trackId,
32841
- heldProvenance: held
32842
- }
32843
- });
32844
- return "no-bbox";
32845
- }
32846
- if (pending.bestSeen.timestamp === void 0) {
32847
- deps.logger.info("late keyFrame: pending entry predates the motion guard — tile kept as-is", {
32848
- tags: { deviceId },
32849
- meta: {
32850
- trackId,
32851
- keyFrameKey: input.keyFrameKey,
32852
- heldProvenance: held
32853
- }
32854
- });
32855
- return "no-bbox";
32856
- }
32857
- const derived = await deps.derive({
32858
- deviceId,
32859
- mediaKey: input.keyFrameKey,
32860
- bbox: {
32861
- x: pending.bestSeen.x,
32862
- y: pending.bestSeen.y,
32863
- w: pending.bestSeen.w,
32864
- h: pending.bestSeen.h
32865
- },
32866
- frameWidth: pending.bestSeen.frameWidth,
32867
- frameHeight: pending.bestSeen.frameHeight,
32868
- timestamp: pending.bestSeen.timestamp
32869
- });
32870
- if (derived === null) {
32871
- deps.logger.info("late keyFrame preview upgrade failed (derive miss) — tile kept as-is", {
32872
- tags: { deviceId },
32873
- meta: {
32874
- trackId,
32875
- keyFrameKey: input.keyFrameKey,
32876
- heldProvenance: held
32877
- }
32878
- });
32879
- return "derive-miss";
32880
- }
32881
- await deps.put({
32882
- deviceId,
32883
- trackId,
32884
- kind: "thumbnail",
32885
- timestamp: input.timestamp,
32886
- data: derived.thumbnail,
32887
- provenance: "native"
32888
- });
32889
- await deps.put({
32890
- deviceId,
32891
- trackId,
32892
- kind: "thumbnailSmall",
32893
- timestamp: input.timestamp,
32894
- data: derived.thumbnailSmall,
32895
- provenance: "native"
32896
- });
32897
- await deps.clearPreviewMiss(deviceId, trackId);
32898
- deps.logger.info("best-shot upgraded from late keyFrame (stand-in replaced)", {
32899
- tags: { deviceId },
32900
- meta: {
32901
- trackId,
32902
- keyFrameKey: input.keyFrameKey,
32903
- replaced: held ?? "none",
32904
- skewMs: derived.skewMs
32905
- }
32906
- });
32907
- return "upgraded";
32908
- }
32909
32964
  //#endregion
32910
32965
  //#region src/pipeline-analytics/store/zone-geometry.ts
32911
32966
  /**
@@ -34644,6 +34699,7 @@ var PlateGalleryProvider = class {
34644
34699
  refreshGallery;
34645
34700
  emitPlateGalleryChanged;
34646
34701
  eventMediaBaseUrl;
34702
+ eventMediaPathPrefix;
34647
34703
  constructor(deps) {
34648
34704
  this.plateStore = deps.plateStore;
34649
34705
  this.vehicleStore = deps.vehicleStore;
@@ -34654,6 +34710,7 @@ var PlateGalleryProvider = class {
34654
34710
  this.refreshGallery = deps.refreshGallery;
34655
34711
  this.emitPlateGalleryChanged = deps.emitPlateGalleryChanged;
34656
34712
  this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
34713
+ this.eventMediaPathPrefix = deps.eventMediaPathPrefix;
34657
34714
  }
34658
34715
  /** Best-effort emit — never allowed to fail the mutation it follows. */
34659
34716
  safeEmitPlateGalleryChanged(payload) {
@@ -34724,13 +34781,7 @@ var PlateGalleryProvider = class {
34724
34781
  return this.toPlateInfo(plate, nameMap, { includeCrops: false });
34725
34782
  }
34726
34783
  async getPlateMedia(input) {
34727
- return (await this.mediaStore.listByOwner("plate", input.plateId)).map((m) => ({
34728
- key: m.key,
34729
- kind: m.kind,
34730
- base64: m.base64,
34731
- sizeBytes: m.sizeBytes,
34732
- timestamp: m.timestamp
34733
- }));
34784
+ return (await this.mediaStore.listByOwner("plate", input.plateId)).map((m) => toGalleryMediaRow(m, this.eventMediaPathPrefix()));
34734
34785
  }
34735
34786
  async searchPlates(input) {
34736
34787
  const maxDistance = input.maxDistance ?? 1;
@@ -34973,6 +35024,51 @@ var PlateGalleryProvider = class {
34973
35024
  }
34974
35025
  };
34975
35026
  //#endregion
35027
+ //#region src/pipeline-analytics/replay/frame-processor-pass.ts
35028
+ /**
35029
+ * Run a FRESH `FrameProcessor` over a fixed, ordered sequence of frames and
35030
+ * return the tracks it produced.
35031
+ *
35032
+ * No `TrackStore`, no bus, no media — a track here is a summary built
35033
+ * ENTIRELY from `FrameProcessor.process()`'s in-memory return value.
35034
+ */
35035
+ function runFrameProcessorPass(input) {
35036
+ const processor = new FrameProcessor(input.deviceId, {}, {}, {}, input.source);
35037
+ if (input.zones !== void 0) processor.setZones(input.zones);
35038
+ if (input.detectionRules !== void 0) processor.setDetectionRules(input.detectionRules);
35039
+ if (input.zoneMembershipMinOverlap !== void 0) processor.setZoneMembershipMinOverlap(input.zoneMembershipMinOverlap);
35040
+ const accum = /* @__PURE__ */ new Map();
35041
+ for (const { timestamp, frame } of input.frames) {
35042
+ const result = processor.process({
35043
+ timestamp,
35044
+ frame
35045
+ });
35046
+ for (const td of result.tracked) {
35047
+ const existing = accum.get(td.trackId);
35048
+ const matched = td.matchedThisFrame ? 1 : 0;
35049
+ if (existing === void 0) accum.set(td.trackId, {
35050
+ className: td.className,
35051
+ firstSeenMs: timestamp,
35052
+ lastSeenMs: timestamp,
35053
+ bbox: { ...td.bbox },
35054
+ framesMatched: matched
35055
+ });
35056
+ else {
35057
+ existing.className = td.className;
35058
+ existing.lastSeenMs = timestamp;
35059
+ existing.framesMatched += matched;
35060
+ }
35061
+ }
35062
+ }
35063
+ return { tracks: [...accum.values()].map((t) => ({
35064
+ className: t.className,
35065
+ firstSeenMs: t.firstSeenMs,
35066
+ lastSeenMs: t.lastSeenMs,
35067
+ bbox: t.bbox,
35068
+ framesMatched: t.framesMatched
35069
+ })) };
35070
+ }
35071
+ //#endregion
34976
35072
  //#region src/pipeline-analytics/retention/disk-reconcile.ts
34977
35073
  /**
34978
35074
  * Disk-wins reconcile for pipeline analytics.
@@ -35037,93 +35133,6 @@ async function reconcileDeviceFromDisk(deps, deviceId) {
35037
35133
  };
35038
35134
  }
35039
35135
  //#endregion
35040
- //#region src/pipeline-analytics/retention/media-reclaim-actions.ts
35041
- /**
35042
- * The operator button for the debug-media reclaim.
35043
- *
35044
- * The mechanism ships armed with nothing: **arming it is the operator's
35045
- * call**. `mode` omitted means `report` — it counts, logs one line per camera
35046
- * and returns rows/bytes per device, and deletes nothing. `reclaim` is a word
35047
- * that has to be typed.
35048
- *
35049
- * Everything about the shape is borrowed from `orphan-audit-actions.ts`, which
35050
- * earned it: dry-run default, explicit pacing, explicit blast radius, and a
35051
- * report you can read instead of a log line you have to find.
35052
- */
35053
- /**
35054
- * `report` counts; `reclaim` deletes.
35055
- *
35056
- * `.default('report')` is load-bearing: the settings form sends `{}` for a
35057
- * button with no fields, and the safe reading of an unstated intent is the one
35058
- * that destroys nothing.
35059
- */
35060
- var MediaReclaimActionModeSchema = require_dist._enum(["report", "reclaim"]).default("report");
35061
- var MediaReclaimScopeSchema = require_dist._enum(["motion-still", "track-filmstrip"]);
35062
- var MediaReclaimActionInputSchema = require_dist.object({
35063
- mode: MediaReclaimActionModeSchema,
35064
- /** Which populations to walk. Omitted = both. */
35065
- scopes: require_dist.array(MediaReclaimScopeSchema).min(1).optional(),
35066
- /** Restrict to these cameras. Omitted = every camera in retention scope. */
35067
- deviceIds: require_dist.array(require_dist.number().int()).min(1).optional(),
35068
- /** Start every device from the top instead of resuming its cursor. */
35069
- restart: require_dist.boolean().optional(),
35070
- /** Rows read per statement. Smaller = more yields = a gentler pass. */
35071
- pageSize: require_dist.number().int().min(50).max(5e3).optional(),
35072
- /** Rows EXAMINED per camera in this pass — the pacing bound. */
35073
- maxRowsPerDevice: require_dist.number().int().min(1).max(5e5).optional(),
35074
- /** Rows DELETED per camera in this pass — the blast-radius bound. */
35075
- maxReclaimPerDevice: require_dist.number().int().min(1).max(5e5).optional(),
35076
- /** Bytes released across the whole run — the disk-contention bound. */
35077
- maxBytesPerRun: require_dist.number().int().min(1).optional(),
35078
- /** Wall-clock ceiling for the run. */
35079
- budgetMinutes: require_dist.number().int().min(1).max(720).optional(),
35080
- /**
35081
- * Bytes per second the pass may release. The recordings volume sustains
35082
- * ~4 MB/s and the archive needs it more than this does; the default is half
35083
- * of that.
35084
- */
35085
- throttleBytesPerSec: require_dist.number().int().min(64 * 1024).optional(),
35086
- /**
35087
- * Minutes of immunity for a recent row. Protective ONLY — raising it can
35088
- * exclusively save rows, never select them.
35089
- */
35090
- graceMinutes: require_dist.number().int().min(1).max(10080).optional()
35091
- });
35092
- var MediaReclaimDeviceReportSchema = require_dist.object({
35093
- deviceId: require_dist.number(),
35094
- scope: MediaReclaimScopeSchema,
35095
- examined: require_dist.number(),
35096
- eligible: require_dist.number(),
35097
- reclaimed: require_dist.number(),
35098
- bytesReclaimed: require_dist.number(),
35099
- /** Rows kept because the owning track is pinned (`staging`/`favourited`). */
35100
- held: require_dist.number(),
35101
- /** Rows kept because the owner could not be shown to be a motion event. */
35102
- unresolved: require_dist.number(),
35103
- withinGrace: require_dist.number(),
35104
- /** Deletes that REFUSED — the blob survived or the volume could not answer.
35105
- * The row was kept and the cursor did not pass it. */
35106
- refused: require_dist.number(),
35107
- stoppedBy: require_dist.string(),
35108
- /** Where the NEXT pass resumes. `0` = this device+scope was walked to the end. */
35109
- nextCursor: require_dist.number()
35110
- });
35111
- var MediaReclaimActionResultSchema = require_dist.object({
35112
- /** `false` when a pass was already in flight — nothing was done. */
35113
- ran: require_dist.boolean(),
35114
- mode: require_dist._enum(["report", "reclaim"]),
35115
- devices: require_dist.array(MediaReclaimDeviceReportSchema),
35116
- totalExamined: require_dist.number(),
35117
- totalEligible: require_dist.number(),
35118
- totalReclaimed: require_dist.number(),
35119
- totalBytesReclaimed: require_dist.number(),
35120
- totalRefused: require_dist.number(),
35121
- durationMs: require_dist.number(),
35122
- /** Every device+scope reported `exhausted` — there is nothing left to do. */
35123
- complete: require_dist.boolean()
35124
- });
35125
- var mediaReclaimActions = require_dist.defineCustomActions({ "retention.mediaReclaim": require_dist.customAction(MediaReclaimActionInputSchema, MediaReclaimActionResultSchema, { auth: "admin" }) });
35126
- //#endregion
35127
35136
  //#region src/pipeline-analytics/retention/media-reclaim.ts
35128
35137
  var MEDIA_RECLAIM_SCOPES = ["motion-still", "track-filmstrip"];
35129
35138
  var DEFAULT_MEDIA_RECLAIM_LIMITS = {
@@ -35335,6 +35344,93 @@ async function runMediaReclaim(input) {
35335
35344
  };
35336
35345
  }
35337
35346
  //#endregion
35347
+ //#region src/pipeline-analytics/retention/media-reclaim-actions.ts
35348
+ /**
35349
+ * The operator button for the debug-media reclaim.
35350
+ *
35351
+ * The mechanism ships armed with nothing: **arming it is the operator's
35352
+ * call**. `mode` omitted means `report` — it counts, logs one line per camera
35353
+ * and returns rows/bytes per device, and deletes nothing. `reclaim` is a word
35354
+ * that has to be typed.
35355
+ *
35356
+ * Everything about the shape is borrowed from `orphan-audit-actions.ts`, which
35357
+ * earned it: dry-run default, explicit pacing, explicit blast radius, and a
35358
+ * report you can read instead of a log line you have to find.
35359
+ */
35360
+ /**
35361
+ * `report` counts; `reclaim` deletes.
35362
+ *
35363
+ * `.default('report')` is load-bearing: the settings form sends `{}` for a
35364
+ * button with no fields, and the safe reading of an unstated intent is the one
35365
+ * that destroys nothing.
35366
+ */
35367
+ var MediaReclaimActionModeSchema = require_dist._enum(["report", "reclaim"]).default("report");
35368
+ var MediaReclaimScopeSchema = require_dist._enum(["motion-still", "track-filmstrip"]);
35369
+ var MediaReclaimActionInputSchema = require_dist.object({
35370
+ mode: MediaReclaimActionModeSchema,
35371
+ /** Which populations to walk. Omitted = both. */
35372
+ scopes: require_dist.array(MediaReclaimScopeSchema).min(1).optional(),
35373
+ /** Restrict to these cameras. Omitted = every camera in retention scope. */
35374
+ deviceIds: require_dist.array(require_dist.number().int()).min(1).optional(),
35375
+ /** Start every device from the top instead of resuming its cursor. */
35376
+ restart: require_dist.boolean().optional(),
35377
+ /** Rows read per statement. Smaller = more yields = a gentler pass. */
35378
+ pageSize: require_dist.number().int().min(50).max(5e3).optional(),
35379
+ /** Rows EXAMINED per camera in this pass — the pacing bound. */
35380
+ maxRowsPerDevice: require_dist.number().int().min(1).max(5e5).optional(),
35381
+ /** Rows DELETED per camera in this pass — the blast-radius bound. */
35382
+ maxReclaimPerDevice: require_dist.number().int().min(1).max(5e5).optional(),
35383
+ /** Bytes released across the whole run — the disk-contention bound. */
35384
+ maxBytesPerRun: require_dist.number().int().min(1).optional(),
35385
+ /** Wall-clock ceiling for the run. */
35386
+ budgetMinutes: require_dist.number().int().min(1).max(720).optional(),
35387
+ /**
35388
+ * Bytes per second the pass may release. The recordings volume sustains
35389
+ * ~4 MB/s and the archive needs it more than this does; the default is half
35390
+ * of that.
35391
+ */
35392
+ throttleBytesPerSec: require_dist.number().int().min(64 * 1024).optional(),
35393
+ /**
35394
+ * Minutes of immunity for a recent row. Protective ONLY — raising it can
35395
+ * exclusively save rows, never select them.
35396
+ */
35397
+ graceMinutes: require_dist.number().int().min(1).max(10080).optional()
35398
+ });
35399
+ var MediaReclaimDeviceReportSchema = require_dist.object({
35400
+ deviceId: require_dist.number(),
35401
+ scope: MediaReclaimScopeSchema,
35402
+ examined: require_dist.number(),
35403
+ eligible: require_dist.number(),
35404
+ reclaimed: require_dist.number(),
35405
+ bytesReclaimed: require_dist.number(),
35406
+ /** Rows kept because the owning track is pinned (`staging`/`favourited`). */
35407
+ held: require_dist.number(),
35408
+ /** Rows kept because the owner could not be shown to be a motion event. */
35409
+ unresolved: require_dist.number(),
35410
+ withinGrace: require_dist.number(),
35411
+ /** Deletes that REFUSED — the blob survived or the volume could not answer.
35412
+ * The row was kept and the cursor did not pass it. */
35413
+ refused: require_dist.number(),
35414
+ stoppedBy: require_dist.string(),
35415
+ /** Where the NEXT pass resumes. `0` = this device+scope was walked to the end. */
35416
+ nextCursor: require_dist.number()
35417
+ });
35418
+ var MediaReclaimActionResultSchema = require_dist.object({
35419
+ /** `false` when a pass was already in flight — nothing was done. */
35420
+ ran: require_dist.boolean(),
35421
+ mode: require_dist._enum(["report", "reclaim"]),
35422
+ devices: require_dist.array(MediaReclaimDeviceReportSchema),
35423
+ totalExamined: require_dist.number(),
35424
+ totalEligible: require_dist.number(),
35425
+ totalReclaimed: require_dist.number(),
35426
+ totalBytesReclaimed: require_dist.number(),
35427
+ totalRefused: require_dist.number(),
35428
+ durationMs: require_dist.number(),
35429
+ /** Every device+scope reported `exhausted` — there is nothing left to do. */
35430
+ complete: require_dist.boolean()
35431
+ });
35432
+ var mediaReclaimActions = require_dist.defineCustomActions({ "retention.mediaReclaim": require_dist.customAction(MediaReclaimActionInputSchema, MediaReclaimActionResultSchema, { auth: "admin" }) });
35433
+ //#endregion
35338
35434
  //#region src/pipeline-analytics/retention/orphan-audit-actions.ts
35339
35435
  /**
35340
35436
  * The operator button for the orphan audit.
@@ -36691,6 +36787,28 @@ function trackMediaScopesForKinds(trackId, kinds) {
36691
36787
  return kinds.some((kind) => DETAIL_CROP_KINDS.includes(kind)) ? scopes : scopes.filter((scope) => scope.publishCropAs === null);
36692
36788
  }
36693
36789
  /**
36790
+ * The STORED kinds worth reading from one scope for a `kinds` request, or
36791
+ * `null` when the scope cannot contribute to it at all.
36792
+ *
36793
+ * This is the same vocabulary translation {@link collectTrackMedia} performs on
36794
+ * the way out, moved to the way IN so it can narrow the query instead of the
36795
+ * answer. A caller naming `faceCrop` must ask the face owner for its stored
36796
+ * `crop` — asking it for `faceCrop` matches nothing, and asking it for
36797
+ * everything opens blobs the answer will discard.
36798
+ *
36799
+ * `undefined` means "every kind": the honest translation of an unfiltered
36800
+ * request, and the only safe answer for the open-ended track scope.
36801
+ *
36802
+ * `null` (skip) is only ever returned for a DETAIL owner whose one publishable
36803
+ * kind was not asked for. The track scope is never skipped — its kinds are
36804
+ * open-ended, and dropping it on a guess is how a listing starts lying.
36805
+ */
36806
+ function storedKindsForScope(scope, kinds) {
36807
+ if (kinds === void 0 || kinds.length === 0) return void 0;
36808
+ if (scope.publishCropAs === null) return kinds;
36809
+ return kinds.includes(scope.publishCropAs) ? [STORED_DETAIL_CROP_KIND] : null;
36810
+ }
36811
+ /**
36694
36812
  * Fold every scope's rows into ONE track media listing: detail crops
36695
36813
  * relabelled, everything filtered to `kinds`, ordered oldest first.
36696
36814
  *
@@ -36827,8 +36945,10 @@ var AnalyticsQueryFacade = class {
36827
36945
  return this.withMediaUrl(events);
36828
36946
  }
36829
36947
  async getEventMedia(input) {
36830
- const all = await (this.deps.mediaStore()?.listByOwner("event", input.eventId, input.deviceId) ?? Promise.resolve([]));
36831
- return input.kind ? all.filter((m) => m.kind === input.kind) : all;
36948
+ const store = this.deps.mediaStore();
36949
+ if (store === null) return [];
36950
+ const prefix = this.deps.eventMediaPathPrefix();
36951
+ return (await store.listByOwner("event", input.eventId, input.deviceId, input.kind === void 0 ? void 0 : [input.kind])).map((row) => toWireMediaFile(row, prefix));
36832
36952
  }
36833
36953
  /**
36834
36954
  * A track's media WITH the bytes, restricted to `kinds` when given.
@@ -36841,14 +36961,36 @@ var AnalyticsQueryFacade = class {
36841
36961
  async getTrackMedia(input) {
36842
36962
  const store = this.deps.mediaStore();
36843
36963
  if (store === null) return [];
36964
+ const prefix = this.deps.eventMediaPathPrefix();
36844
36965
  const scopes = trackMediaScopesForKinds(input.trackId, input.kinds);
36845
- return collectTrackMedia(await Promise.all(scopes.map(async (scope) => ({
36846
- scope,
36847
- rows: await store.listByOwner(scope.ownerKind, scope.ownerId, input.deviceId)
36848
- }))), (row, kind) => ({
36966
+ return collectTrackMedia(await Promise.all(scopes.map(async (scope) => {
36967
+ const stored = storedKindsForScope(scope, input.kinds);
36968
+ if (stored === null) return {
36969
+ scope,
36970
+ rows: []
36971
+ };
36972
+ return {
36973
+ scope,
36974
+ rows: await store.listByOwner(scope.ownerKind, scope.ownerId, input.deviceId, stored)
36975
+ };
36976
+ })), (row, kind) => ({
36849
36977
  ...row,
36850
36978
  kind
36851
- }), input.kinds);
36979
+ }), input.kinds).map((row) => toWireMediaFile(row, prefix));
36980
+ }
36981
+ /**
36982
+ * An event's media WITHOUT the bytes.
36983
+ *
36984
+ * Index rows only — it never opens a blob. The track detail modal unions
36985
+ * this with {@link listTrackMedia} to build its filmstrip and then fetches
36986
+ * each tile from the `event-media` plane by key, so the bytes this used to
36987
+ * carry were read, base64'd, shipped and discarded.
36988
+ */
36989
+ async listEventMedia(input) {
36990
+ const store = this.deps.mediaStore();
36991
+ if (store === null) return [];
36992
+ const prefix = this.deps.eventMediaPathPrefix();
36993
+ return (await store.listInfoByOwner("event", input.eventId, input.deviceId)).map((row) => toWireMediaFileInfo(row, prefix));
36852
36994
  }
36853
36995
  /**
36854
36996
  * A track's media WITHOUT the bytes.
@@ -36866,13 +37008,14 @@ var AnalyticsQueryFacade = class {
36866
37008
  async listTrackMedia(input) {
36867
37009
  const store = this.deps.mediaStore();
36868
37010
  if (store === null) return [];
37011
+ const prefix = this.deps.eventMediaPathPrefix();
36869
37012
  return collectTrackMedia(await Promise.all(trackMediaOwnerScopes(input.trackId).map(async (scope) => ({
36870
37013
  scope,
36871
37014
  rows: await store.listInfoByOwner(scope.ownerKind, scope.ownerId, input.deviceId)
36872
37015
  }))), (row, kind) => ({
36873
37016
  ...row,
36874
37017
  kind
36875
- }), void 0);
37018
+ }), void 0).map((row) => toWireMediaFileInfo(row, prefix));
36876
37019
  }
36877
37020
  async pruneEventsBefore(input) {
36878
37021
  const eventStore = this.deps.eventStore();
@@ -39317,6 +39460,31 @@ function createEventMediaHandler(deps) {
39317
39460
  }
39318
39461
  //#endregion
39319
39462
  //#region src/pipeline-analytics/event-thumbnail-resolver.ts
39463
+ /**
39464
+ * Rank `candidates` with `pick`, then open ONE blob — the winner's.
39465
+ *
39466
+ * The ladders decide on `kind` alone, so the bytes are dead weight until the
39467
+ * choice is made. Handing them pre-read rows meant a tile that serves the
39468
+ * 31 KB `thumbnailSmall` first read the whole 3 597 219-byte set, including a
39469
+ * 2.8 MB `keyFrame` no list view has ever rendered.
39470
+ *
39471
+ * A winner whose blob is GONE is dropped and the ladder re-run on the rest.
39472
+ * That fall-through is not an extra: `listByOwner` used to provide it for free
39473
+ * by skipping unreadable rows before the ladder ever saw them, and without it
39474
+ * an index row that outlived its file would 404 a tile that used to render.
39475
+ * Bounded by the candidate count — each attempt removes one row.
39476
+ */
39477
+ async function readWinningBlob(candidates, pick, readBlob) {
39478
+ let remaining = candidates;
39479
+ while (remaining.length > 0) {
39480
+ const winner = pick(remaining);
39481
+ if (winner === void 0) return null;
39482
+ const bytes = await readBlob(winner.key);
39483
+ if (bytes !== null) return bytes;
39484
+ remaining = remaining.filter((row) => row.key !== winner.key);
39485
+ }
39486
+ return null;
39487
+ }
39320
39488
  /** Pick an event-OWNED media blob: the tight `crop`, else the native-res boxed
39321
39489
  * `fullFrameBoxed`, else any available file. Both preferred kinds are RETIRED
39322
39490
  * for new tracks (served only for old stored rows). Undefined when the event
@@ -39367,15 +39535,24 @@ async function collectCropForcedMedia(source, id) {
39367
39535
  };
39368
39536
  }
39369
39537
  /**
39370
- * Resolve the default-path media for an event id: the event's own media when it
39371
- * has any, else the owning track's fallback media. Returns `null` when neither
39372
- * yields a blob (caller 404s → the surface shows an icon).
39538
+ * Which rows the default path should serve from, and how to rank them: the
39539
+ * event's OWN media when it has any, else the owning track's.
39540
+ *
39541
+ * It yields the candidate SET rather than a winner so the caller can open one
39542
+ * blob and, if that blob has gone, re-rank the rest ({@link readWinningBlob}).
39543
+ * Returning a single row would force the caller to read it blind and 404 a tile
39544
+ * whose index row outlived its file.
39373
39545
  */
39374
- async function resolveDefaultEventMedia(deps) {
39375
- const owned = pickEventOwnedMedia(deps.eventFiles);
39376
- if (owned) return owned;
39546
+ async function collectDefaultEventMedia(deps) {
39547
+ if (deps.eventFiles.length > 0) return {
39548
+ candidates: deps.eventFiles,
39549
+ pick: pickEventOwnedMedia
39550
+ };
39377
39551
  const trackId = await deps.getTrackIdForEvent(deps.eventId) ?? deps.eventId;
39378
- return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
39552
+ return {
39553
+ candidates: await deps.listTrackMedia(trackId),
39554
+ pick: pickTrackFallbackMedia
39555
+ };
39379
39556
  }
39380
39557
  //#endregion
39381
39558
  //#region src/pipeline-analytics/face-settings.ts
@@ -47483,6 +47660,70 @@ var PackageDropDetector = class {
47483
47660
  }
47484
47661
  };
47485
47662
  //#endregion
47663
+ //#region src/pipeline-analytics/pipeline/person-over-vehicle.ts
47664
+ /**
47665
+ * How much of the PERSON box must sit inside a vehicle box to be counted.
47666
+ *
47667
+ * 0.8 and not 1.0: a phantom raised on a car's window pillar or wing mirror
47668
+ * routinely pokes a little outside the vehicle box, and requiring total
47669
+ * containment would count almost none of them. 0.8 and not 0.5: half a person
47670
+ * box overlapping a car is what a pedestrian walking in front of one looks
47671
+ * like, and counting those would make the number mean "somebody was near a
47672
+ * car", which is not a defect.
47673
+ */
47674
+ var CONTAINMENT_THRESHOLD = .8;
47675
+ /**
47676
+ * The classes that count as a vehicle for this test.
47677
+ *
47678
+ * A local list rather than a shared taxonomy on purpose: it exists to describe
47679
+ * ONE measurement, and a shared enum would make a change here silently change
47680
+ * detection behaviour somewhere else.
47681
+ */
47682
+ var VEHICLE_CLASSES$1 = [
47683
+ "car",
47684
+ "truck",
47685
+ "bus",
47686
+ "motorcycle",
47687
+ "vehicle"
47688
+ ];
47689
+ /** Area of the intersection of two boxes. `0` when they do not overlap. */
47690
+ function intersectionArea(a, b) {
47691
+ const left = Math.max(a.x, b.x);
47692
+ const right = Math.min(a.x + a.w, b.x + b.w);
47693
+ const top = Math.max(a.y, b.y);
47694
+ const bottom = Math.min(a.y + a.h, b.y + b.h);
47695
+ if (right <= left || bottom <= top) return 0;
47696
+ return (right - left) * (bottom - top);
47697
+ }
47698
+ /**
47699
+ * Fraction of `person` that lies inside `vehicle`, in `[0,1]`.
47700
+ *
47701
+ * `0` for a degenerate person box: a zero-area box is inside everything and
47702
+ * inside nothing, and answering `1` would count every one of them.
47703
+ */
47704
+ function containedFraction(person, vehicle) {
47705
+ const area = person.w * person.h;
47706
+ if (area <= 0) return 0;
47707
+ return intersectionArea(person, vehicle) / area;
47708
+ }
47709
+ /**
47710
+ * Is this person box contained in any vehicle box in the same frame?
47711
+ *
47712
+ * Returns the fraction for the BEST-containing vehicle when it clears
47713
+ * {@link CONTAINMENT_THRESHOLD}, else `null`. The fraction rides along because
47714
+ * a counter that only ever says "yes" cannot later be re-thresholded against
47715
+ * what was actually measured.
47716
+ */
47717
+ function personOverVehicle(person, neighbours, threshold = CONTAINMENT_THRESHOLD) {
47718
+ let best = 0;
47719
+ for (const neighbour of neighbours) {
47720
+ if (!VEHICLE_CLASSES$1.includes(neighbour.className)) continue;
47721
+ const fraction = containedFraction(person, neighbour.bbox);
47722
+ if (fraction > best) best = fraction;
47723
+ }
47724
+ return best >= threshold ? best : null;
47725
+ }
47726
+ //#endregion
47486
47727
  //#region src/pipeline-analytics/pipeline/stationary/stationary-evidence.ts
47487
47728
  /**
47488
47729
  * stationary-evidence — how a parked object EARNS a registry entry.
@@ -50014,83 +50255,6 @@ function buildRecordedStillPorts(input) {
50014
50255
  })
50015
50256
  };
50016
50257
  }
50017
- function errorMessage(err) {
50018
- return err instanceof Error ? err.message : String(err);
50019
- }
50020
- async function reconcileGallerySamples(deps, options) {
50021
- let checked = 0;
50022
- let reclaimed = 0;
50023
- let keyless = 0;
50024
- let skippedGrace = 0;
50025
- let stoppedByCeiling = false;
50026
- const identities = await deps.listIdentities();
50027
- for (const identity of identities) {
50028
- const samples = await deps.listSamples(identity.id);
50029
- for (const sample of samples) {
50030
- if (sample.mediaKey === void 0) {
50031
- keyless += 1;
50032
- continue;
50033
- }
50034
- if (options.now - sample.addedAt < options.graceMs) {
50035
- skippedGrace += 1;
50036
- continue;
50037
- }
50038
- if (reclaimed >= options.maxReclaimPerRun) {
50039
- stoppedByCeiling = true;
50040
- deps.logger.warn("gallery reconcile: reclaim ceiling reached — remaining dead references keep until the next run", { meta: { ceiling: options.maxReclaimPerRun } });
50041
- return {
50042
- checked,
50043
- reclaimed,
50044
- keyless,
50045
- skippedGrace,
50046
- stoppedByCeiling
50047
- };
50048
- }
50049
- let state;
50050
- try {
50051
- state = await deps.blobState(sample.mediaKey);
50052
- } catch (err) {
50053
- const reason = errorMessage(err);
50054
- deps.logger.warn("gallery reconcile: a blob probe FAILED — aborting the pass, keeping every row", { meta: {
50055
- sampleId: sample.id,
50056
- identity: identity.name,
50057
- error: reason
50058
- } });
50059
- return {
50060
- checked,
50061
- reclaimed,
50062
- keyless,
50063
- skippedGrace,
50064
- stoppedByCeiling,
50065
- aborted: reason
50066
- };
50067
- }
50068
- checked += 1;
50069
- if (state === "exists") continue;
50070
- await deps.removeSample(identity.id, sample.id);
50071
- reclaimed += 1;
50072
- deps.logger.warn("gallery reconcile: sample blob is gone from the filesystem — reference removed", { meta: {
50073
- sampleId: sample.id,
50074
- identity: identity.name,
50075
- mediaKey: sample.mediaKey,
50076
- state
50077
- } });
50078
- }
50079
- }
50080
- if (reclaimed > 0) deps.logger.info("gallery reconcile: dead references removed", { meta: {
50081
- checked,
50082
- reclaimed,
50083
- keyless,
50084
- skippedGrace
50085
- } });
50086
- return {
50087
- checked,
50088
- reclaimed,
50089
- keyless,
50090
- skippedGrace,
50091
- stoppedByCeiling
50092
- };
50093
- }
50094
50258
  //#endregion
50095
50259
  //#region src/pipeline-analytics/retention/embedding-orphan-scan.ts
50096
50260
  /** The scope id this leg reports and persists its cursor under. */
@@ -50207,6 +50371,83 @@ async function scanEmbeddingOrphans(deps, limits, cursor) {
50207
50371
  nextCursor: Math.max(0, offset)
50208
50372
  };
50209
50373
  }
50374
+ function errorMessage(err) {
50375
+ return err instanceof Error ? err.message : String(err);
50376
+ }
50377
+ async function reconcileGallerySamples(deps, options) {
50378
+ let checked = 0;
50379
+ let reclaimed = 0;
50380
+ let keyless = 0;
50381
+ let skippedGrace = 0;
50382
+ let stoppedByCeiling = false;
50383
+ const identities = await deps.listIdentities();
50384
+ for (const identity of identities) {
50385
+ const samples = await deps.listSamples(identity.id);
50386
+ for (const sample of samples) {
50387
+ if (sample.mediaKey === void 0) {
50388
+ keyless += 1;
50389
+ continue;
50390
+ }
50391
+ if (options.now - sample.addedAt < options.graceMs) {
50392
+ skippedGrace += 1;
50393
+ continue;
50394
+ }
50395
+ if (reclaimed >= options.maxReclaimPerRun) {
50396
+ stoppedByCeiling = true;
50397
+ deps.logger.warn("gallery reconcile: reclaim ceiling reached — remaining dead references keep until the next run", { meta: { ceiling: options.maxReclaimPerRun } });
50398
+ return {
50399
+ checked,
50400
+ reclaimed,
50401
+ keyless,
50402
+ skippedGrace,
50403
+ stoppedByCeiling
50404
+ };
50405
+ }
50406
+ let state;
50407
+ try {
50408
+ state = await deps.blobState(sample.mediaKey);
50409
+ } catch (err) {
50410
+ const reason = errorMessage(err);
50411
+ deps.logger.warn("gallery reconcile: a blob probe FAILED — aborting the pass, keeping every row", { meta: {
50412
+ sampleId: sample.id,
50413
+ identity: identity.name,
50414
+ error: reason
50415
+ } });
50416
+ return {
50417
+ checked,
50418
+ reclaimed,
50419
+ keyless,
50420
+ skippedGrace,
50421
+ stoppedByCeiling,
50422
+ aborted: reason
50423
+ };
50424
+ }
50425
+ checked += 1;
50426
+ if (state === "exists") continue;
50427
+ await deps.removeSample(identity.id, sample.id);
50428
+ reclaimed += 1;
50429
+ deps.logger.warn("gallery reconcile: sample blob is gone from the filesystem — reference removed", { meta: {
50430
+ sampleId: sample.id,
50431
+ identity: identity.name,
50432
+ mediaKey: sample.mediaKey,
50433
+ state
50434
+ } });
50435
+ }
50436
+ }
50437
+ if (reclaimed > 0) deps.logger.info("gallery reconcile: dead references removed", { meta: {
50438
+ checked,
50439
+ reclaimed,
50440
+ keyless,
50441
+ skippedGrace
50442
+ } });
50443
+ return {
50444
+ checked,
50445
+ reclaimed,
50446
+ keyless,
50447
+ skippedGrace,
50448
+ stoppedByCeiling
50449
+ };
50450
+ }
50210
50451
  /**
50211
50452
  * Where the completion marker lives.
50212
50453
  *
@@ -53269,14 +53510,14 @@ function isAfterCursor(row, cursor) {
53269
53510
  if (row.lastSeen < cursor.lastSeen) return true;
53270
53511
  return row.lastSeen === cursor.lastSeen && row.id < cursor.trackId;
53271
53512
  }
53272
- var VEHICLE_CLASSES$1 = new Set([
53513
+ var VEHICLE_CLASSES = new Set([
53273
53514
  "vehicle",
53274
53515
  "car",
53275
53516
  "truck",
53276
53517
  "bus"
53277
53518
  ]);
53278
53519
  function motionlessMaxPxForClass(className) {
53279
- return VEHICLE_CLASSES$1.has(className.toLowerCase()) ? 8 : 40;
53520
+ return VEHICLE_CLASSES.has(className.toLowerCase()) ? 8 : 40;
53280
53521
  }
53281
53522
  function netDisplacementPx(positions) {
53282
53523
  const first = positions[0];
@@ -57036,70 +57277,6 @@ function hasWrappedDetection(entries) {
57036
57277
  return entries.some((e) => e.capName === "detection-pipeline" && e.kind === "wrapped");
57037
57278
  }
57038
57279
  //#endregion
57039
- //#region src/pipeline-analytics/pipeline/person-over-vehicle.ts
57040
- /**
57041
- * How much of the PERSON box must sit inside a vehicle box to be counted.
57042
- *
57043
- * 0.8 and not 1.0: a phantom raised on a car's window pillar or wing mirror
57044
- * routinely pokes a little outside the vehicle box, and requiring total
57045
- * containment would count almost none of them. 0.8 and not 0.5: half a person
57046
- * box overlapping a car is what a pedestrian walking in front of one looks
57047
- * like, and counting those would make the number mean "somebody was near a
57048
- * car", which is not a defect.
57049
- */
57050
- var CONTAINMENT_THRESHOLD = .8;
57051
- /**
57052
- * The classes that count as a vehicle for this test.
57053
- *
57054
- * A local list rather than a shared taxonomy on purpose: it exists to describe
57055
- * ONE measurement, and a shared enum would make a change here silently change
57056
- * detection behaviour somewhere else.
57057
- */
57058
- var VEHICLE_CLASSES = [
57059
- "car",
57060
- "truck",
57061
- "bus",
57062
- "motorcycle",
57063
- "vehicle"
57064
- ];
57065
- /** Area of the intersection of two boxes. `0` when they do not overlap. */
57066
- function intersectionArea(a, b) {
57067
- const left = Math.max(a.x, b.x);
57068
- const right = Math.min(a.x + a.w, b.x + b.w);
57069
- const top = Math.max(a.y, b.y);
57070
- const bottom = Math.min(a.y + a.h, b.y + b.h);
57071
- if (right <= left || bottom <= top) return 0;
57072
- return (right - left) * (bottom - top);
57073
- }
57074
- /**
57075
- * Fraction of `person` that lies inside `vehicle`, in `[0,1]`.
57076
- *
57077
- * `0` for a degenerate person box: a zero-area box is inside everything and
57078
- * inside nothing, and answering `1` would count every one of them.
57079
- */
57080
- function containedFraction(person, vehicle) {
57081
- const area = person.w * person.h;
57082
- if (area <= 0) return 0;
57083
- return intersectionArea(person, vehicle) / area;
57084
- }
57085
- /**
57086
- * Is this person box contained in any vehicle box in the same frame?
57087
- *
57088
- * Returns the fraction for the BEST-containing vehicle when it clears
57089
- * {@link CONTAINMENT_THRESHOLD}, else `null`. The fraction rides along because
57090
- * a counter that only ever says "yes" cannot later be re-thresholded against
57091
- * what was actually measured.
57092
- */
57093
- function personOverVehicle(person, neighbours, threshold = CONTAINMENT_THRESHOLD) {
57094
- let best = 0;
57095
- for (const neighbour of neighbours) {
57096
- if (!VEHICLE_CLASSES.includes(neighbour.className)) continue;
57097
- const fraction = containedFraction(person, neighbour.bbox);
57098
- if (fraction > best) best = fraction;
57099
- }
57100
- return best >= threshold ? best : null;
57101
- }
57102
- //#endregion
57103
57280
  //#region src/pipeline-analytics/pipeline/capture-crop.ts
57104
57281
  function createCaptureCrop(deps) {
57105
57282
  return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth, deviceId) => {
@@ -63541,6 +63718,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
63541
63718
  * Set once the data-plane is registered; null until then (e.g. no
63542
63719
  * dataPlane facility in the current environment). */
63543
63720
  eventMediaBaseUrl = null;
63721
+ /**
63722
+ * The SAME path, derived rather than observed — a pure function of the addon
63723
+ * id, so it exists before `serve()` is attempted and survives its failure.
63724
+ *
63725
+ * `eventMediaBaseUrl` above answers "is the plane up?", which is the right
63726
+ * question for the OPTIONAL `mediaUrl` stamp on a slim event projection.
63727
+ * It is the wrong question for a media row's `url`, which is that row's only
63728
+ * address: a row that dropped its address on a boot race would be
63729
+ * indistinguishable from a row with no bytes, and this repo has already paid
63730
+ * for one read that quietly returned less than it was asked for.
63731
+ */
63732
+ eventMediaPathPrefix() {
63733
+ return `/addon/${this.ctx.id}/event-media`;
63734
+ }
63544
63735
  lastActiveTrackIds = /* @__PURE__ */ new Map();
63545
63736
  /** See `pipeline/suppressed-births.ts` — rejected births must not be
63546
63737
  * re-upserted, and must be forgotten when the tracker drops the id. */
@@ -63791,6 +63982,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
63791
63982
  sensorEventStore: () => this.sensorEventStore,
63792
63983
  opsLog: () => this.eventsOpsLog,
63793
63984
  eventMediaBaseUrl: () => this.eventMediaBaseUrl,
63985
+ eventMediaPathPrefix: () => this.eventMediaPathPrefix(),
63794
63986
  logger: () => this.ctx.logger,
63795
63987
  zoneCatalogue: () => this.zoneCatalogue
63796
63988
  });
@@ -66333,6 +66525,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
66333
66525
  logger: this.ctx.logger,
66334
66526
  refreshGallery: () => this.faceRecognizer?.refreshGallery(),
66335
66527
  eventMediaBaseUrl: () => this.eventMediaBaseUrl,
66528
+ eventMediaPathPrefix: () => this.eventMediaPathPrefix(),
66336
66529
  emitFaceGalleryChanged: (payload) => this.emitFaceGalleryChanged(payload)
66337
66530
  });
66338
66531
  this.faceGalleryProvider = faceGallery;
@@ -66347,6 +66540,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
66347
66540
  this.plateRecognizer?.refreshGallery();
66348
66541
  },
66349
66542
  eventMediaBaseUrl: () => this.eventMediaBaseUrl,
66543
+ eventMediaPathPrefix: () => this.eventMediaPathPrefix(),
66350
66544
  emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload)
66351
66545
  });
66352
66546
  return [
@@ -70032,6 +70226,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70032
70226
  async listTrackMedia(input) {
70033
70227
  return this.queryFacade.listTrackMedia(input);
70034
70228
  }
70229
+ /** What media an EVENT has, without any of it — the same manifest, for the
70230
+ * half of the detail modal's filmstrip that is event-owned. */
70231
+ async listEventMedia(input) {
70232
+ return this.queryFacade.listEventMedia(input);
70233
+ }
70035
70234
  /**
70036
70235
  * Search object events by text using CLIP cosine similarity.
70037
70236
  *
@@ -71752,31 +71951,44 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71752
71951
  return media;
71753
71952
  }
71754
71953
  }
71954
+ /**
71955
+ * Resolve an event/track id to ONE tile's bytes.
71956
+ *
71957
+ * Every ladder below ranks rows by `kind` alone, so this walks the INDEX
71958
+ * (`listInfoByOwner`, no blob opened) and opens only the winner. It used to
71959
+ * walk `listByOwner`, which reads and base64-encodes every blob an owner
71960
+ * holds before the first comparison is made: measured on the live hub
71961
+ * 2026-08-30, a track's six kinds are 3 597 219 bytes and the tile serves
71962
+ * `thumbnailSmall` — 31 155 of them. An Events page of forty tiles read
71963
+ * ~144 MB and allocated ~190 MB of base64 to deliver ~1.2 MB, all of it on
71964
+ * the single-threaded post-analysis runner.
71965
+ */
71755
71966
  async readEventThumbnail(id, preferKind) {
71756
- const eventFiles = await (this.mediaStore?.listByOwner("event", id) ?? Promise.resolve([]));
71967
+ const listInfo = (ownerKind, ownerId) => this.mediaStore?.listInfoByOwner(ownerKind, ownerId) ?? Promise.resolve([]);
71968
+ const getTrackIdForEvent = (eventId) => this.eventStore?.getTrackIdForEvent(eventId) ?? Promise.resolve(null);
71969
+ const readBlob = async (key) => {
71970
+ const file = await (this.mediaStore?.getByKey(key) ?? Promise.resolve(null));
71971
+ return file === null ? null : {
71972
+ bytes: Buffer.from(file.base64, "base64"),
71973
+ key: file.key
71974
+ };
71975
+ };
71976
+ const eventFiles = await listInfo("event", id);
71757
71977
  if (preferKind !== void 0 && preferKind.length > 0) {
71758
71978
  const layers = await collectCropForcedMedia({
71759
- listByOwner: (ownerKind, ownerId) => this.mediaStore?.listByOwner(ownerKind, ownerId) ?? Promise.resolve([]),
71760
- getTrackIdForEvent: (eventId) => this.eventStore?.getTrackIdForEvent(eventId) ?? Promise.resolve(null)
71979
+ listByOwner: listInfo,
71980
+ getTrackIdForEvent
71761
71981
  }, id);
71762
- const chosen = resolveCropForcedMedia(layers.bodyFiles, (files) => pickCleanMedia(files, preferKind), layers.detailFiles);
71763
- if (!chosen) return null;
71764
- return {
71765
- bytes: Buffer.from(chosen.base64, "base64"),
71766
- key: chosen.key
71767
- };
71982
+ const bodyKeys = new Set(layers.bodyFiles.map((f) => f.key));
71983
+ return await readWinningBlob([...layers.bodyFiles, ...layers.detailFiles], (rows) => resolveCropForcedMedia(rows.filter((r) => bodyKeys.has(r.key)), (files) => pickCleanMedia(files, preferKind), rows.filter((r) => !bodyKeys.has(r.key))), readBlob);
71768
71984
  }
71769
- const chosen = await resolveDefaultEventMedia({
71985
+ const { candidates, pick } = await collectDefaultEventMedia({
71770
71986
  eventId: id,
71771
71987
  eventFiles,
71772
- listTrackMedia: (trackId) => this.mediaStore?.listByOwner("track", trackId) ?? Promise.resolve([]),
71773
- getTrackIdForEvent: (eventId) => this.eventStore?.getTrackIdForEvent(eventId) ?? Promise.resolve(null)
71988
+ listTrackMedia: (trackId) => listInfo("track", trackId),
71989
+ getTrackIdForEvent
71774
71990
  });
71775
- if (!chosen) return null;
71776
- return {
71777
- bytes: Buffer.from(chosen.base64, "base64"),
71778
- key: chosen.key
71779
- };
71991
+ return await readWinningBlob(candidates, pick, readBlob);
71780
71992
  }
71781
71993
  globalSettingsSchema(cap) {
71782
71994
  return this.schema(buildGlobalSettingsSchema(cap));