@camstack/addon-post-analysis 1.2.116 → 1.2.118

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-C0uPGtBv.js");
5
+ const require_dist = require("../dist-CO6pe7bi.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");
@@ -15149,26 +15149,28 @@ function remainingSummaryCandidates(kept, taken, rank) {
15149
15149
  /**
15150
15150
  * The media a tile is drawn from, best first.
15151
15151
  *
15152
- * For a group: `groupCrop` (union crop, the group's own blob) then
15153
- * `nativeFrame` (pointer at the anchor's clean keyFrame). A digest of a
15154
- * cohort may show the native still when the crop was not written; a
15155
- * member's `keyFrame` listed as itself stays off this ladder so tracker
15156
- * ID churn cannot refill a mosaic with eleven driveways. Boxed frames stay
15157
- * absent: a drawn rectangle is a permanent lie in a digest tile.
15152
+ * First-level group crop only (`groupCrop`), then the group's native-frame
15153
+ * pointer, then a member's clean full frame. Member `thumbnail` / plate /
15154
+ * face crops stay off this ladder: a successful plate read overwrites the
15155
+ * vehicle first-level crop under the same `thumbnail` kind, and a digest
15156
+ * tile of a targa is the wrong picture. Boxed frames stay absent: a drawn
15157
+ * rectangle is a permanent lie in a digest tile. Duplicate driveways from
15158
+ * tracker ID churn are collapsed by `frameDedupeKey`, not by hiding the
15159
+ * full-frame fallback.
15158
15160
  */
15159
15161
  var SUMMARY_MEDIA_LADDER = [
15160
15162
  "groupCrop",
15161
15163
  "nativeFrame",
15162
- "thumbnail",
15163
- "thumbnailSmall",
15164
- "snapshot"
15164
+ "keyFrame",
15165
+ "keyFrameSmall",
15166
+ "fullFrame"
15165
15167
  ];
15166
15168
  /**
15167
15169
  * Pick ONE media row for a track — the cheap question first (index rows only),
15168
15170
  * so exactly one blob is ever read.
15169
15171
  *
15170
- * Within a rung the row CLOSEST to the track's peak is chosen; `snapshot` is a
15171
- * filmstrip and its rows accumulate, so "the first one" would systematically
15172
+ * Within a rung the row CLOSEST to the track's peak is chosen; a member may
15173
+ * carry more than one full-frame still, so "the first one" would systematically
15172
15174
  * pick the moment the subject entered rather than the moment it was best seen.
15173
15175
  */
15174
15176
  function pickSummaryMedia(rows, aroundMs) {
@@ -25922,7 +25924,7 @@ var CLASS_RANK_VEHICLE = .8;
25922
25924
  var CLASS_RANK_ANIMAL = .5;
25923
25925
  var CLASS_RANK_DEFAULT = .25;
25924
25926
  var PERSON_CLASSES = new Set(["person", "face"]);
25925
- var VEHICLE_CLASSES = new Set([
25927
+ var VEHICLE_CLASSES$1 = new Set([
25926
25928
  "vehicle",
25927
25929
  "car",
25928
25930
  "truck",
@@ -25953,7 +25955,7 @@ function clamp01$1(x) {
25953
25955
  function classRank(className) {
25954
25956
  const c = className.toLowerCase();
25955
25957
  if (PERSON_CLASSES.has(c)) return 1;
25956
- if (VEHICLE_CLASSES.has(c)) return CLASS_RANK_VEHICLE;
25958
+ if (VEHICLE_CLASSES$1.has(c)) return CLASS_RANK_VEHICLE;
25957
25959
  if (ANIMAL_CLASSES.has(c)) return CLASS_RANK_ANIMAL;
25958
25960
  return CLASS_RANK_DEFAULT;
25959
25961
  }
@@ -26556,7 +26558,7 @@ var FaceGalleryProvider = class {
26556
26558
  const identities = await this.identityStore.listIdentities();
26557
26559
  const nameMap = new Map(identities.map((i) => [i.id, i.name]));
26558
26560
  const identityName = face.recognizedIdentityId ? nameMap.get(face.recognizedIdentityId) : void 0;
26559
- const base64 = await this.faceCropBase64(face);
26561
+ const cropUrl = this.cropUrlFor(face);
26560
26562
  return {
26561
26563
  faceId: face.id,
26562
26564
  deviceId: face.deviceId,
@@ -26565,7 +26567,7 @@ var FaceGalleryProvider = class {
26565
26567
  ...face.recognizedIdentityId != null ? { recognizedIdentityId: face.recognizedIdentityId } : {},
26566
26568
  ...identityName !== void 0 ? { identityName } : {},
26567
26569
  assigned: face.assigned,
26568
- ...base64 !== void 0 ? { base64 } : {},
26570
+ ...cropUrl !== void 0 ? { cropUrl } : {},
26569
26571
  ...face.faceBbox !== void 0 ? { faceBbox: face.faceBbox } : {},
26570
26572
  ...face.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: face.keyFrameMediaKey } : {},
26571
26573
  ...face.bestMatchScore != null ? { bestMatchScore: face.bestMatchScore } : {},
@@ -29685,9 +29687,15 @@ var TrackCloser = class {
29685
29687
  await this.suppressZeroMedia(t.deviceId, t.trackId);
29686
29688
  return;
29687
29689
  }
29688
- if (outcome === "raster-fallback" && closure?.rasterFallback) await this.commitRasterFallback(t, closure.rasterFallback.jpeg, closure.rasterFallback.timestamp);
29690
+ let thumbnailLanded = ownedMedia.some((m) => m.kind === "thumbnail");
29691
+ if (outcome === "raster-fallback" && closure?.rasterFallback) {
29692
+ await this.commitRasterFallback(t, closure.rasterFallback.jpeg, closure.rasterFallback.timestamp);
29693
+ thumbnailLanded = true;
29694
+ }
29689
29695
  await this.maybePromoteLastFrame(t, ownedMedia);
29690
- await this.maybeDeriveThumbnailFromKeyFrame(t, ownedMedia);
29696
+ const hasRasterFallback = closure?.rasterFallback !== void 0;
29697
+ if (!thumbnailLanded) thumbnailLanded = await this.maybeDeriveThumbnailFromKeyFrame(t, ownedMedia, hasRasterFallback);
29698
+ if (!thumbnailLanded && closure?.rasterFallback) await this.commitRasterFallback(t, closure.rasterFallback.jpeg, closure.rasterFallback.timestamp);
29691
29699
  this.deps.logger.info("track ended", {
29692
29700
  tags: { deviceId: t.deviceId },
29693
29701
  meta: {
@@ -29805,14 +29813,23 @@ var TrackCloser = class {
29805
29813
  * and the rate could only ever be counted from the database, after the fact.
29806
29814
  * A branch that drops work silently reads as "never happened".
29807
29815
  */
29808
- async maybeDeriveThumbnailFromKeyFrame(t, ownedMedia) {
29809
- if (ownedMedia.some((m) => m.kind === "thumbnail")) return;
29816
+ async maybeDeriveThumbnailFromKeyFrame(t, ownedMedia, hasRasterFallback) {
29817
+ if (ownedMedia.some((m) => m.kind === "thumbnail")) return true;
29810
29818
  const derive = this.deps.deriveThumbnailFromKeyFrame;
29811
- if (!derive) return this.logNoThumbnailAtClose(t, "derive-not-wired");
29819
+ if (!derive) {
29820
+ if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "derive-not-wired");
29821
+ return false;
29822
+ }
29812
29823
  const keyFrame = ownedMedia.find((m) => m.kind === "keyFrame");
29813
- if (!keyFrame) return this.logNoThumbnailAtClose(t, "no-key-frame");
29824
+ if (!keyFrame) {
29825
+ if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "no-key-frame");
29826
+ return false;
29827
+ }
29814
29828
  const bestSeen = this.deps.residents.bestSeenBbox(t.trackId);
29815
- if (!bestSeen) return this.logNoThumbnailAtClose(t, "no-best-seen-bbox");
29829
+ if (!bestSeen) {
29830
+ if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "no-best-seen-bbox");
29831
+ return false;
29832
+ }
29816
29833
  try {
29817
29834
  const derived = await derive({
29818
29835
  mediaKey: keyFrame.key,
@@ -29824,17 +29841,23 @@ var TrackCloser = class {
29824
29841
  },
29825
29842
  frameWidth: bestSeen.frameWidth,
29826
29843
  frameHeight: bestSeen.frameHeight,
29827
- timestamp: bestSeen.timestamp
29844
+ timestamp: keyFrame.timestamp
29828
29845
  });
29829
- if (!derived) return this.logNoThumbnailAtClose(t, "derive-returned-null", { keyFrameKey: keyFrame.key });
29846
+ if (!derived) {
29847
+ if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "derive-returned-null", { keyFrameKey: keyFrame.key });
29848
+ return false;
29849
+ }
29830
29850
  const mediaStore = this.deps.mediaStore();
29831
- if (!mediaStore) return this.logNoThumbnailAtClose(t, "no-media-store");
29851
+ if (!mediaStore) {
29852
+ if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "no-media-store");
29853
+ return false;
29854
+ }
29832
29855
  await mediaStore.put({
29833
29856
  deviceId: t.deviceId,
29834
29857
  ownerKind: "track",
29835
29858
  ownerId: t.trackId,
29836
29859
  kind: "thumbnail",
29837
- timestamp: bestSeen.timestamp,
29860
+ timestamp: keyFrame.timestamp,
29838
29861
  data: derived.thumbnail
29839
29862
  });
29840
29863
  await mediaStore.put({
@@ -29842,7 +29865,7 @@ var TrackCloser = class {
29842
29865
  ownerKind: "track",
29843
29866
  ownerId: t.trackId,
29844
29867
  kind: "thumbnailSmall",
29845
- timestamp: bestSeen.timestamp,
29868
+ timestamp: keyFrame.timestamp,
29846
29869
  data: derived.thumbnailSmall
29847
29870
  });
29848
29871
  this.deps.logger.info("thumbnail derived from keyFrame at close (no live best-shot landed)", {
@@ -29853,6 +29876,7 @@ var TrackCloser = class {
29853
29876
  skewMs: derived.skewMs
29854
29877
  }
29855
29878
  });
29879
+ return true;
29856
29880
  } catch (err) {
29857
29881
  this.deps.logger.debug("close-time keyFrame thumbnail derive failed", {
29858
29882
  tags: { deviceId: t.deviceId },
@@ -29861,10 +29885,11 @@ var TrackCloser = class {
29861
29885
  error: String(err)
29862
29886
  }
29863
29887
  });
29864
- this.logNoThumbnailAtClose(t, "derive-threw", {
29888
+ if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "derive-threw", {
29865
29889
  keyFrameKey: keyFrame.key,
29866
29890
  error: String(err)
29867
29891
  });
29892
+ return false;
29868
29893
  }
29869
29894
  }
29870
29895
  /**
@@ -30734,6 +30759,7 @@ var PlateGalleryProvider = class {
30734
30759
  logger;
30735
30760
  refreshGallery;
30736
30761
  emitPlateGalleryChanged;
30762
+ eventMediaBaseUrl;
30737
30763
  constructor(deps) {
30738
30764
  this.plateStore = deps.plateStore;
30739
30765
  this.vehicleStore = deps.vehicleStore;
@@ -30743,6 +30769,7 @@ var PlateGalleryProvider = class {
30743
30769
  this.logger = deps.logger;
30744
30770
  this.refreshGallery = deps.refreshGallery;
30745
30771
  this.emitPlateGalleryChanged = deps.emitPlateGalleryChanged;
30772
+ this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
30746
30773
  }
30747
30774
  /** Best-effort emit — never allowed to fail the mutation it follows. */
30748
30775
  safeEmitPlateGalleryChanged(payload) {
@@ -30750,6 +30777,12 @@ var PlateGalleryProvider = class {
30750
30777
  this.emitPlateGalleryChanged(payload);
30751
30778
  } catch {}
30752
30779
  }
30780
+ cropUrlFor(plate) {
30781
+ if (plate.mediaKey === void 0 || plate.mediaKey === "") return void 0;
30782
+ const base = this.eventMediaBaseUrl();
30783
+ if (base === null) return void 0;
30784
+ return `${base}/${encodeURIComponent(plate.mediaKey)}`;
30785
+ }
30753
30786
  async plateCropBase64(plate) {
30754
30787
  if (plate.mediaKey) {
30755
30788
  const m = await this.mediaStore.getByKey(plate.mediaKey);
@@ -30758,8 +30791,9 @@ var PlateGalleryProvider = class {
30758
30791
  const media = await this.mediaStore.listByOwner("plate", plate.id);
30759
30792
  return media.length > 0 ? media[0].base64 : void 0;
30760
30793
  }
30761
- async toPlateInfo(plate, nameMap) {
30762
- const base64 = await this.plateCropBase64(plate);
30794
+ async toPlateInfo(plate, nameMap, opts) {
30795
+ const base64 = opts?.includeCrops ?? true ? await this.plateCropBase64(plate) : void 0;
30796
+ const cropUrl = this.cropUrlFor(plate);
30763
30797
  const vehicleName = plate.recognizedVehicleId != null ? nameMap?.get(plate.recognizedVehicleId) : void 0;
30764
30798
  return {
30765
30799
  plateId: plate.id,
@@ -30774,7 +30808,8 @@ var PlateGalleryProvider = class {
30774
30808
  ...vehicleName !== void 0 ? { vehicleName } : {},
30775
30809
  ...plate.plateBbox !== void 0 ? { plateBbox: plate.plateBbox } : {},
30776
30810
  ...plate.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: plate.keyFrameMediaKey } : {},
30777
- ...base64 !== void 0 ? { base64 } : {}
30811
+ ...base64 !== void 0 ? { base64 } : {},
30812
+ ...cropUrl !== void 0 ? { cropUrl } : {}
30778
30813
  };
30779
30814
  }
30780
30815
  async vehicleNameMap() {
@@ -30793,7 +30828,7 @@ var PlateGalleryProvider = class {
30793
30828
  const plate = await this.plateStore.get(`plate-${input.trackId}`);
30794
30829
  if (!plate || plate.deviceId !== input.deviceId) return null;
30795
30830
  const nameMap = await this.vehicleNameMap();
30796
- return this.toPlateInfo(plate, nameMap);
30831
+ return this.toPlateInfo(plate, nameMap, { includeCrops: false });
30797
30832
  }
30798
30833
  async getPlateMedia(input) {
30799
30834
  return (await this.mediaStore.listByOwner("plate", input.plateId)).map((m) => ({
@@ -35934,6 +35969,19 @@ function createEventMediaHandler(deps) {
35934
35969
  };
35935
35970
  }
35936
35971
  //#endregion
35972
+ //#region src/pipeline-analytics/copy-face-crop-preview.ts
35973
+ var TRACK_PREVIEW_KINDS = new Set(["thumbnail", "thumbnailSmall"]);
35974
+ async function copyFaceCropIfTrackHasNoThumbnail(store, input) {
35975
+ if ((await store.listTrackMedia(input.trackId)).some((m) => TRACK_PREVIEW_KINDS.has(m.kind))) return false;
35976
+ await store.putTrackThumbnail({
35977
+ deviceId: input.deviceId,
35978
+ trackId: input.trackId,
35979
+ timestamp: input.timestamp,
35980
+ data: input.crop
35981
+ });
35982
+ return true;
35983
+ }
35984
+ //#endregion
35937
35985
  //#region src/pipeline-analytics/event-thumbnail-resolver.ts
35938
35986
  /** Pick an event-OWNED media blob: the tight `crop`, else the native-res boxed
35939
35987
  * `fullFrameBoxed`, else any available file. Both preferred kinds are RETIRED
@@ -35942,29 +35990,47 @@ function createEventMediaHandler(deps) {
35942
35990
  function pickEventOwnedMedia(files) {
35943
35991
  return files.find((f) => f.kind === "crop") ?? files.find((f) => f.kind === "fullFrameBoxed") ?? files[0];
35944
35992
  }
35945
- /** Pick a track's fallback media in the shared cadence-preference order the
35946
- * KeyEvent path uses: `thumbnailSmall` (480 fast-load) → best `thumbnail` →
35947
- * rolling `lastFrame` → `firstFrame` → newest `snapshot` → any track blob.
35948
- * Undefined when the track owns no media. */
35993
+ /** Pick a track's fallback media when no subject crop exists: `thumbnailSmall`
35994
+ * → `thumbnail` → native `keyFrameSmall`/`keyFrame`/`fullFrame` → rolling
35995
+ * `lastFrame` → `firstFrame` → newest `snapshot` → any blob. A full frame
35996
+ * beats a class icon. Undefined when the owner has no media. */
35949
35997
  function pickTrackFallbackMedia(files) {
35950
- return files.find((f) => f.kind === "thumbnailSmall") ?? files.find((f) => f.kind === "thumbnail") ?? files.find((f) => f.kind === "lastFrame") ?? files.find((f) => f.kind === "firstFrame") ?? [...files].reverse().find((f) => f.kind === "snapshot") ?? files[files.length - 1];
35998
+ return files.find((f) => f.kind === "thumbnailSmall") ?? files.find((f) => f.kind === "thumbnail") ?? files.find((f) => f.kind === "keyFrameSmall") ?? files.find((f) => f.kind === "keyFrame") ?? files.find((f) => f.kind === "fullFrame") ?? files.find((f) => f.kind === "lastFrame") ?? files.find((f) => f.kind === "firstFrame") ?? [...files].reverse().find((f) => f.kind === "snapshot") ?? files[files.length - 1];
35951
35999
  }
35952
36000
  /**
35953
- * Crop-forced resolution (the `?kind=crop` reel/timeline path). Prefer a CLEAN
35954
- * (never-boxed) frame via the injected `pickClean`; when the track owns NO clean
35955
- * frame at all — the regression class created by retiring `crop`/`fullFrame`
35956
- * production (2026-07-21), leaving degenerate tracks with only boxed
35957
- * `lastFrame`/`snapshot`/`firstFrame` — degrade to ANY track media rather than
35958
- * returning undefined. A boxed frame (with the target's box drawn) is a real
35959
- * preview; an empty tile is not. Clean is always tried FIRST so the common case
35960
- * still serves an unboxed tile. Undefined only when the owner has NO media.
36001
+ * Crop-forced resolution (the `?kind=crop` / `?kind=thumbnailSmall` reel path).
36002
+ *
36003
+ * Ladder: body crop (track/event thumbnail) → tight face/plate crop → any
36004
+ * remaining image (keyFrame / lastFrame / …). The class icon is only honest
36005
+ * when the owner has no stored bytes at all.
35961
36006
  *
35962
36007
  * `pickClean` is injected because the clean-kind picker (`pickCleanMedia`) lives
35963
36008
  * in the addon `index.ts` over the concrete `MediaFileKind` set; this keeps the
35964
36009
  * composition pure + unit-testable here.
35965
36010
  */
35966
- function resolveCropForcedMedia(files, pickClean) {
35967
- return pickClean(files) ?? pickTrackFallbackMedia(files);
36011
+ function resolveCropForcedMedia(files, pickClean, detailFiles = []) {
36012
+ return pickClean(files) ?? pickClean(detailFiles) ?? pickTrackFallbackMedia([...files, ...detailFiles]);
36013
+ }
36014
+ /**
36015
+ * Event + track media (the body/scene) and face/plate media (the detail
36016
+ * crops). Face rows live under `face-${trackId}`; a track can close with
36017
+ * `hasEmbeddedFace` and ZERO track files (0e343f86).
36018
+ */
36019
+ async function collectCropForcedMedia(source, id) {
36020
+ const eventFiles = await source.listByOwner("event", id);
36021
+ const trackFiles = await source.listByOwner("track", id);
36022
+ const ownerTrackId = trackFiles.length === 0 ? await source.getTrackIdForEvent(id) : null;
36023
+ const owningTrackFiles = ownerTrackId === null ? [] : await source.listByOwner("track", ownerTrackId);
36024
+ const trackId = ownerTrackId ?? id;
36025
+ const [faceFiles, plateFiles] = await Promise.all([source.listByOwner("face", `face-${trackId}`), source.listByOwner("plate", `plate-${trackId}`)]);
36026
+ return {
36027
+ bodyFiles: [
36028
+ ...eventFiles,
36029
+ ...trackFiles,
36030
+ ...owningTrackFiles
36031
+ ],
36032
+ detailFiles: [...faceFiles, ...plateFiles]
36033
+ };
35968
36034
  }
35969
36035
  /**
35970
36036
  * Resolve the default-path media for an event id: the event's own media when it
@@ -35978,6 +36044,14 @@ async function resolveDefaultEventMedia(deps) {
35978
36044
  return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
35979
36045
  }
35980
36046
  //#endregion
36047
+ //#region src/pipeline-analytics/ensure-track-hierarchy.ts
36048
+ async function ensureTrackHierarchyMedia(deps, input) {
36049
+ const tasks = [];
36050
+ if (!deps.isThumbnailLanded(input.trackId)) tasks.push(deps.captureBodyThumbnail(input));
36051
+ if (!deps.isKeyFrameLanded(input.trackId)) tasks.push(deps.captureKeyFrame(input));
36052
+ await Promise.all(tasks);
36053
+ }
36054
+ //#endregion
35981
36055
  //#region src/pipeline-analytics/face-settings.ts
35982
36056
  /**
35983
36057
  * Per-device face-recognition settings. Cascade: a per-device override on top
@@ -36496,6 +36570,21 @@ var FaceRecognizer = class {
36496
36570
  ...held,
36497
36571
  crop
36498
36572
  });
36573
+ if (crop !== void 0 && input.frameHandle !== void 0) this.deps.onLiveHierarchy?.({
36574
+ deviceId: input.deviceId,
36575
+ trackId: c.trackId,
36576
+ timestamp: input.timestamp,
36577
+ bodyBbox: {
36578
+ x: c.bbox.x,
36579
+ y: c.bbox.y,
36580
+ w: c.bbox.w,
36581
+ h: c.bbox.h
36582
+ },
36583
+ frameWidth: input.frameWidth,
36584
+ frameHeight: input.frameHeight,
36585
+ cropPadding: input.cropPadding,
36586
+ frameHandle: input.frameHandle
36587
+ });
36499
36588
  this.deps.logger.debug("face: best-face held", {
36500
36589
  tags: {
36501
36590
  deviceId: input.deviceId,
@@ -36645,6 +36734,22 @@ var FaceRecognizer = class {
36645
36734
  });
36646
36735
  return;
36647
36736
  }
36737
+ try {
36738
+ await this.deps.copyCropToTrackPreview?.({
36739
+ deviceId,
36740
+ trackId,
36741
+ crop: held.crop,
36742
+ timestamp: held.timestamp
36743
+ });
36744
+ } catch (err) {
36745
+ this.deps.logger.debug("face: track preview copy failed", {
36746
+ tags: { deviceId },
36747
+ meta: {
36748
+ trackId,
36749
+ error: String(err)
36750
+ }
36751
+ });
36752
+ }
36648
36753
  const keyFrameMediaKey = this.deps.getKeyFrameMediaKey?.(trackId);
36649
36754
  try {
36650
36755
  await this.deps.faceStore.insert({
@@ -39935,8 +40040,9 @@ var OverlayDetailStateStore = class {
39935
40040
  *
39936
40041
  * See docs/superpowers/specs/2026-07-17-package-zones-design.md §3.3 +
39937
40042
  * docs/superpowers/specs/2026-07-17-detection-config-exposure-design.md
39938
- * (surface C). Dwell default is 15s per operator decision (the design's
39939
- * §3.3 draft said 45s).
40043
+ * (surface C). Confirmation is N frames (default 3); dwell default is 0
40044
+ * (an extra bar). The 15 s dwell plus a 5 s round-gap missed a real
40045
+ * parcel on device 615 (2026-08-24 13:40).
39940
40046
  */
39941
40047
  var PackageDropSettingsSchema = require_dist.object({
39942
40048
  /**
@@ -39957,13 +40063,20 @@ var PackageDropSettingsSchema = require_dist.object({
39957
40063
  */
39958
40064
  packageDropEnabled: require_dist.boolean().default(true),
39959
40065
  /**
39960
- * Minimum OBSERVED dwell (seconds, since first-seen) before a newly
39961
- * promoted stationary package counts as a delivery. Kills a bag briefly
39962
- * set down and snatched back. Operator default 15s. Values beyond the
39963
- * stationary promotion window (30s) are satisfied only once the entry
39964
- * has actually been observed that long.
40066
+ * Frames the box must be re-seen on before it is a parked package.
40067
+ * Same bar as tracker `minHits`: one frame is a flicker, two can still
40068
+ * be a passer-by's bag, three is "it stayed". A settle burst of three
40069
+ * frames in ~1 s is enough; no second governor round after the courier
40070
+ * left. Device 615 2026-08-24 13:40 expired at 1 round / 1.5 s dwell.
40071
+ */
40072
+ packageDropMinFrames: require_dist.number().int().min(1).max(20).default(3),
40073
+ /**
40074
+ * Extra OBSERVED dwell (seconds, since first-seen) on top of N frames.
40075
+ * 0 (the default) means the frame count is enough — a courier who drops
40076
+ * and leaves in 2 s still delivers. Raise this to reject a bag briefly
40077
+ * set down and snatched back.
39965
40078
  */
39966
- packageDropDwellSec: require_dist.number().int().min(0).max(3600).default(15),
40079
+ packageDropDwellSec: require_dist.number().int().min(0).max(3600).default(0),
39967
40080
  /** Reject tiny far-field blobs: min bbox area as a fraction of the frame. */
39968
40081
  packageDropMinBboxAreaFrac: require_dist.number().min(0).max(1).default(.004),
39969
40082
  /**
@@ -39981,13 +40094,15 @@ var PackageDropSettingsSchema = require_dist.object({
39981
40094
  packageDropPickupEnabled: require_dist.boolean().default(true),
39982
40095
  /**
39983
40096
  * A parcel needs a deliverer: a stillness candidate only ever promotes to
39984
- * `delivered` if a non-package track (person/vehicle/animal) was active on
39985
- * the device within this many seconds BEFORE the box first appeared — and a
39986
- * departure only counts as `picked-up` with somebody around the
39987
- * disappearance. 0 disables both gates. Device 615's streetlight wedge
39988
- * cycled delivered→picked-up ~23×/night with nobody in frame, ever.
39989
- * Default 300 s: wide enough to absorb the motion-settle governor's cadence
39990
- * (the box is often first SEEN minutes after the courier left).
40097
+ * `delivered` if a non-package track (person/vehicle/animal) was active IN
40098
+ * A CONFIGURED PACKAGE DROP-OFF ZONE within this many seconds BEFORE the
40099
+ * box first appeared — and a departure only counts as `picked-up` with
40100
+ * somebody in that zone around the disappearance. Whole-camera motion (a
40101
+ * walker at the far gate) does not count. 0 disables both gates. Device
40102
+ * 615's streetlight wedge cycled delivered→picked-up ~23×/night with
40103
+ * nobody in the drop-off zone. Default 300 s: wide enough to absorb the
40104
+ * motion-settle governor's cadence (the box is often first SEEN minutes
40105
+ * after the courier left).
39991
40106
  */
39992
40107
  packageDropActivityWindowSec: require_dist.number().int().min(0).max(3600).default(300),
39993
40108
  /**
@@ -40011,6 +40126,7 @@ function resolvePackageDropSettings(raw) {
40011
40126
  };
40012
40127
  return {
40013
40128
  packageDropEnabled: pick("packageDropEnabled"),
40129
+ packageDropMinFrames: pick("packageDropMinFrames"),
40014
40130
  packageDropDwellSec: pick("packageDropDwellSec"),
40015
40131
  packageDropMinBboxAreaFrac: pick("packageDropMinBboxAreaFrac"),
40016
40132
  packageDropMaxBboxAreaFrac: pick("packageDropMaxBboxAreaFrac"),
@@ -40268,29 +40384,6 @@ var DeferredBirthRegistry = class {
40268
40384
  this.byDevice.delete(deviceKey);
40269
40385
  }
40270
40386
  };
40271
- var AnalyticsGroupRecordSchema = require_dist.object({
40272
- id: require_dist.string(),
40273
- deviceId: require_dist.number().int(),
40274
- openedAt: require_dist.number().int(),
40275
- closedAt: require_dist.number().int(),
40276
- timestamp: require_dist.number().int(),
40277
- memberCount: require_dist.number().int(),
40278
- memberTrackIds: require_dist.array(require_dist.string()).readonly(),
40279
- className: require_dist.string(),
40280
- classes: require_dist.array(require_dist.string()).readonly(),
40281
- /** Relative event-media path, or null when the group has no picture yet. */
40282
- mediaUrl: require_dist.string().nullable(),
40283
- singleton: require_dist.boolean()
40284
- });
40285
- var AnalyticsGroupMemberSchema = require_dist.object({
40286
- trackId: require_dist.string(),
40287
- deviceId: require_dist.number().int(),
40288
- className: require_dist.string(),
40289
- firstSeen: require_dist.number().int(),
40290
- lastSeen: require_dist.number().int(),
40291
- mediaUrl: require_dist.string().nullable()
40292
- });
40293
- var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: require_dist.array(AnalyticsGroupMemberSchema).readonly() });
40294
40387
  var ListGroupsInputSchema = require_dist.object({
40295
40388
  deviceId: require_dist.number().int().optional(),
40296
40389
  sinceMs: require_dist.number().int().optional(),
@@ -40300,66 +40393,11 @@ var ListGroupsInputSchema = require_dist.object({
40300
40393
  });
40301
40394
  var groupActions = require_dist.defineCustomActions({
40302
40395
  "analytics.listGroups": require_dist.customAction(ListGroupsInputSchema, require_dist.object({
40303
- groups: require_dist.array(AnalyticsGroupRecordSchema).readonly(),
40396
+ groups: require_dist.array(require_dist.AnalyticsGroupRecordSchema).readonly(),
40304
40397
  hasMore: require_dist.boolean()
40305
40398
  })),
40306
- "analytics.getGroup": require_dist.customAction(require_dist.object({ groupId: require_dist.string().min(1) }), require_dist.object({ group: AnalyticsGroupDetailSchema.nullable() }))
40399
+ "analytics.getGroup": require_dist.customAction(require_dist.object({ groupId: require_dist.string().min(1) }), require_dist.object({ group: require_dist.AnalyticsGroupDetailSchema.nullable() }))
40307
40400
  });
40308
- //#endregion
40309
- //#region src/pipeline-analytics/group/group-query.ts
40310
- /**
40311
- * Stamp a relative event-media URL.
40312
- *
40313
- * Media keys contain `:` and the data-plane serves them by key. Track ids are
40314
- * bare and fall through to the existing event/track thumbnail resolver. Never
40315
- * percent-encode: the handler does not URL-decode the path.
40316
- */
40317
- function stampEventMediaUrl(baseUrl, id) {
40318
- if (baseUrl === null || baseUrl.length === 0) return null;
40319
- if (id === null || id === void 0 || id.length === 0) return null;
40320
- return `${baseUrl}/${id}`;
40321
- }
40322
- /** Union of every class on the members; `className` is the first, stable order. */
40323
- function aggregateGroupClasses(tracks) {
40324
- const seen = /* @__PURE__ */ new Set();
40325
- const classes = [];
40326
- for (const track of tracks) for (const name of [track.className, ...track.classes ?? []]) {
40327
- if (name.length === 0 || seen.has(name)) continue;
40328
- seen.add(name);
40329
- classes.push(name);
40330
- }
40331
- return {
40332
- className: classes[0] ?? "unknown",
40333
- classes
40334
- };
40335
- }
40336
- function toGroupRecord(input) {
40337
- const { className, classes } = aggregateGroupClasses(input.tracks);
40338
- const mediaId = input.group.mediaKey ?? input.group.nativeKey ?? (input.group.memberCount === 1 ? input.memberTrackIds[0] : null);
40339
- return {
40340
- id: input.group.id,
40341
- deviceId: input.group.deviceId,
40342
- openedAt: input.group.openedAt,
40343
- closedAt: input.group.closedAt,
40344
- timestamp: input.group.timestamp,
40345
- memberCount: input.memberTrackIds.length > 0 ? input.memberTrackIds.length : input.group.memberCount,
40346
- memberTrackIds: [...input.memberTrackIds],
40347
- className,
40348
- classes: [...classes],
40349
- mediaUrl: stampEventMediaUrl(input.eventMediaBaseUrl, mediaId),
40350
- singleton: input.memberTrackIds.length <= 1
40351
- };
40352
- }
40353
- function toGroupMember(member, track, eventMediaBaseUrl) {
40354
- return {
40355
- trackId: member.trackId,
40356
- deviceId: member.deviceId,
40357
- className: track?.className ?? "unknown",
40358
- firstSeen: track?.firstSeen ?? member.timestamp,
40359
- lastSeen: track?.lastSeen ?? member.timestamp,
40360
- mediaUrl: stampEventMediaUrl(eventMediaBaseUrl, member.trackId)
40361
- };
40362
- }
40363
40401
  function trackToLite(track) {
40364
40402
  return {
40365
40403
  trackId: track.trackId,
@@ -40369,56 +40407,18 @@ function trackToLite(track) {
40369
40407
  lastSeen: track.lastSeen
40370
40408
  };
40371
40409
  }
40372
- async function listGroupRecords(ports, input) {
40373
- const page = await ports.groupStore.listRecent(input);
40374
- const groups = [];
40375
- for (const group of page.groups) groups.push(await buildGroupRecord(ports, group));
40376
- return {
40377
- groups,
40378
- hasMore: page.hasMore
40379
- };
40380
- }
40381
- async function getGroupDetail(ports, groupId) {
40382
- const group = await ports.groupStore.get(groupId);
40383
- if (group === null) return null;
40384
- const members = await ports.groupStore.listMembers(groupId);
40385
- const lites = [];
40386
- const rows = [];
40387
- for (const member of members) {
40388
- const track = await ports.loadTrack(member.trackId);
40389
- const lite = track === null ? null : trackToLite(track);
40390
- if (lite !== null) lites.push(lite);
40391
- rows.push(toGroupMember(member, lite, ports.eventMediaBaseUrl()));
40392
- }
40393
- return {
40394
- ...toGroupRecord({
40395
- group,
40396
- memberTrackIds: members.map((m) => m.trackId),
40397
- tracks: lites,
40398
- eventMediaBaseUrl: ports.eventMediaBaseUrl()
40399
- }),
40400
- members: rows
40401
- };
40402
- }
40403
- async function buildGroupRecord(ports, group) {
40404
- const members = await ports.groupStore.listMembers(group.id);
40405
- const lites = [];
40406
- for (const member of members) {
40407
- const track = await ports.loadTrack(member.trackId);
40408
- if (track !== null) lites.push(trackToLite(track));
40409
- }
40410
- return toGroupRecord({
40411
- group,
40412
- memberTrackIds: members.map((m) => m.trackId),
40413
- tracks: lites,
40414
- eventMediaBaseUrl: ports.eventMediaBaseUrl()
40415
- });
40416
- }
40417
40410
  /**
40418
40411
  * Gallery and digest read the same ladder: groupCrop, then the native-frame
40419
- * pointer, then member subject crops. A multi with no crop still has a
40420
- * picture if the sealer stored `nativeKey`.
40412
+ * pointer, then a member's clean full frame. Plate / face / member
40413
+ * thumbnails never reach the picker — a plate read overwrites the vehicle
40414
+ * first-level crop under the same `thumbnail` kind. A multi with no crop
40415
+ * still has a picture if the sealer stored `nativeKey`.
40421
40416
  */
40417
+ var DIGEST_MEMBER_FALLBACK_KINDS = new Set([
40418
+ "keyFrame",
40419
+ "keyFrameSmall",
40420
+ "fullFrame"
40421
+ ]);
40422
40422
  function digestMediaRows(input) {
40423
40423
  const owned = [];
40424
40424
  if (input.mediaKey !== null && input.mediaKey.length > 0) owned.push({
@@ -40432,7 +40432,7 @@ function digestMediaRows(input) {
40432
40432
  timestamp: input.timestamp
40433
40433
  });
40434
40434
  if (owned.length > 0) return owned;
40435
- return input.memberRows;
40435
+ return input.memberRows.filter((row) => DIGEST_MEMBER_FALLBACK_KINDS.has(row.kind));
40436
40436
  }
40437
40437
  var MATCH_WINDOW_MS = 1e3;
40438
40438
  var MOVE_EPS = .35;
@@ -40986,6 +40986,34 @@ function toInstant(t) {
40986
40986
  confidence: t.importance
40987
40987
  };
40988
40988
  }
40989
+ function memberBoxesAt(members, at, frame) {
40990
+ const w = Math.max(frame.width, 1);
40991
+ const h = Math.max(frame.height, 1);
40992
+ const boxes = [];
40993
+ for (const member of members) {
40994
+ let best = member.positions[0];
40995
+ let bestDt = Number.POSITIVE_INFINITY;
40996
+ for (const p of member.positions) {
40997
+ const dt = Math.abs(p.timestamp - at);
40998
+ if (dt <= 1e3 && dt < bestDt) {
40999
+ best = p;
41000
+ bestDt = dt;
41001
+ }
41002
+ }
41003
+ if (best === void 0 || bestDt === Number.POSITIVE_INFINITY) continue;
41004
+ boxes.push({
41005
+ bbox: {
41006
+ x: best.bbox.x / w,
41007
+ y: best.bbox.y / h,
41008
+ w: best.bbox.w / w,
41009
+ h: best.bbox.h / h
41010
+ },
41011
+ label: member.className,
41012
+ observedAt: best.timestamp
41013
+ });
41014
+ }
41015
+ return boxes;
41016
+ }
40989
41017
  function toSealer(track) {
40990
41018
  return {
40991
41019
  trackId: track.trackId,
@@ -41002,7 +41030,11 @@ var GroupSealer = class {
41002
41030
  constructor(deps) {
41003
41031
  this.deps = deps;
41004
41032
  }
41033
+ get processEnabled() {
41034
+ return this.deps.enabled ?? false;
41035
+ }
41005
41036
  async flushIdle(buffer, now = this.deps.now()) {
41037
+ if (!this.processEnabled) return 0;
41006
41038
  let written = 0;
41007
41039
  for (const group of buffer.idleGroups(now, GROUP_SESSION_IDLE_MS)) if (await this.persistLiveGroup(group, buffer)) {
41008
41040
  buffer.drop([group.id]);
@@ -41015,6 +41047,7 @@ var GroupSealer = class {
41015
41047
  * into the live buffer, persist whoever is already idle, keep the rest in RAM.
41016
41048
  */
41017
41049
  async recoverCamera(deviceId, buffer) {
41050
+ if (!this.processEnabled) return 0;
41018
41051
  if (buffer.isPrimed(deviceId)) return 0;
41019
41052
  const now = this.deps.now();
41020
41053
  try {
@@ -41043,6 +41076,7 @@ var GroupSealer = class {
41043
41076
  * is what wedged hub-main.
41044
41077
  */
41045
41078
  async sealWindow(deviceId, since, until) {
41079
+ if (!this.processEnabled) return 0;
41046
41080
  try {
41047
41081
  const closed = await this.deps.trackStore.queryHistorical({
41048
41082
  deviceId,
@@ -41157,26 +41191,54 @@ var GroupSealer = class {
41157
41191
  return;
41158
41192
  }
41159
41193
  try {
41160
- const raw = await (0, sharp.default)(source.jpeg).removeAlpha().raw().toBuffer({ resolveWithObject: true });
41161
- if (raw.info.channels !== 3) return;
41162
- const union = {
41163
- x: instant.ux * raw.info.width,
41164
- y: instant.uy * raw.info.height,
41165
- w: instant.uw * raw.info.width,
41166
- h: instant.uh * raw.info.height
41167
- };
41168
- const crop = await cropGroupFromRgb(raw.data, raw.info.width, raw.info.height, union);
41169
- const mediaKey = await this.deps.mediaStore.put({
41194
+ const raw = await (0, sharp.default)(source.jpeg).removeAlpha().toColorspace("srgb").raw().toBuffer({ resolveWithObject: true });
41195
+ if (raw.info.channels === 3) {
41196
+ const union = {
41197
+ x: instant.ux * raw.info.width,
41198
+ y: instant.uy * raw.info.height,
41199
+ w: instant.uw * raw.info.width,
41200
+ h: instant.uh * raw.info.height
41201
+ };
41202
+ const crop = await cropGroupFromRgb(raw.data, raw.info.width, raw.info.height, union);
41203
+ const mediaKey = await this.deps.mediaStore.put({
41204
+ deviceId,
41205
+ ownerKind: "group",
41206
+ ownerId: draft.id,
41207
+ kind: "thumbnail",
41208
+ timestamp,
41209
+ data: crop
41210
+ });
41211
+ await this.deps.groupStore.setMediaKey(draft.id, mediaKey);
41212
+ }
41213
+ } catch (err) {
41214
+ this.deps.logger.warn("group sealer: crop failed", {
41215
+ tags: { deviceId },
41216
+ meta: {
41217
+ groupId: draft.id,
41218
+ error: String(err)
41219
+ }
41220
+ });
41221
+ }
41222
+ try {
41223
+ const composed = await composeGroupFrame({
41224
+ jpeg: source.jpeg,
41225
+ boxes: memberBoxesAt(instantMembers, timestamp, frame),
41226
+ at: timestamp,
41227
+ logger: this.deps.logger,
41228
+ deviceId,
41229
+ groupKey: draft.id
41230
+ });
41231
+ if (composed === null) return;
41232
+ await this.deps.mediaStore.put({
41170
41233
  deviceId,
41171
41234
  ownerKind: "group",
41172
41235
  ownerId: draft.id,
41173
- kind: "thumbnail",
41236
+ kind: "fullFrameBoxed",
41174
41237
  timestamp,
41175
- data: crop
41238
+ data: composed.bytes
41176
41239
  });
41177
- await this.deps.groupStore.setMediaKey(draft.id, mediaKey);
41178
41240
  } catch (err) {
41179
- this.deps.logger.warn("group sealer: crop failed", {
41241
+ this.deps.logger.warn("group sealer: boxed frame failed", {
41180
41242
  tags: { deviceId },
41181
41243
  meta: {
41182
41244
  groupId: draft.id,
@@ -41201,6 +41263,37 @@ var GroupSealer = class {
41201
41263
  }
41202
41264
  };
41203
41265
  //#endregion
41266
+ //#region src/pipeline-analytics/group/group-cursor.ts
41267
+ function encodeGroupCursor(cursor) {
41268
+ return Buffer.from(JSON.stringify({
41269
+ c: cursor.closedAt,
41270
+ i: cursor.groupId
41271
+ }), "utf8").toString("base64url");
41272
+ }
41273
+ function decodeGroupCursor(raw) {
41274
+ let parsed;
41275
+ try {
41276
+ parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
41277
+ } catch {
41278
+ throw new Error("listGroups: malformed cursor");
41279
+ }
41280
+ if (parsed === null || typeof parsed !== "object" || !("c" in parsed) || !("i" in parsed) || typeof parsed.c !== "number" || !Number.isFinite(parsed.c) || typeof parsed.i !== "string" || parsed.i.length === 0) throw new Error("listGroups: malformed cursor");
41281
+ return {
41282
+ closedAt: parsed.c,
41283
+ groupId: parsed.i
41284
+ };
41285
+ }
41286
+ function compareGroupDesc(a, b) {
41287
+ if (a.closedAt !== b.closedAt) return b.closedAt - a.closedAt;
41288
+ if (a.id === b.id) return 0;
41289
+ return a.id < b.id ? 1 : -1;
41290
+ }
41291
+ /** True when `row` sits strictly AFTER the cursor in DESC order. */
41292
+ function isAfterGroupCursor(row, cursor) {
41293
+ if (row.closedAt < cursor.closedAt) return true;
41294
+ return row.closedAt === cursor.closedAt && row.id < cursor.groupId;
41295
+ }
41296
+ //#endregion
41204
41297
  //#region src/pipeline-analytics/group/group-store.ts
41205
41298
  /**
41206
41299
  * The group ENTITY store — a co-moving partition of one session, with one
@@ -41462,6 +41555,24 @@ var TrackGroupStore = class {
41462
41555
  data: r.data
41463
41556
  }));
41464
41557
  }
41558
+ /**
41559
+ * Membership for a page of groups in ONE query. `listGroupRecords` used to
41560
+ * call {@link listMembers} per row — 60 groups is 60 settings-store RPCs,
41561
+ * ~20 s, and the viewer feed times out into a retry loop.
41562
+ */
41563
+ async listMembersForGroups(groupIds) {
41564
+ if (groupIds.length === 0) return [];
41565
+ return (await this.store.query.query({
41566
+ collection: TRACK_GROUP_MEMBERS_COLLECTION,
41567
+ filter: {
41568
+ whereIn: { groupId: [...groupIds] },
41569
+ limit: 1e4
41570
+ }
41571
+ })).map((r) => this.normalizeMember({
41572
+ id: r.id,
41573
+ data: r.data
41574
+ }));
41575
+ }
41465
41576
  async membersOfTracks(trackIds) {
41466
41577
  const out = [];
41467
41578
  for (const trackId of trackIds) {
@@ -41477,20 +41588,29 @@ var TrackGroupStore = class {
41477
41588
  return out;
41478
41589
  }
41479
41590
  /**
41480
- * Newest-closed page for the Library Groups tab.
41591
+ * Newest-closed page for the Groups feed.
41481
41592
  *
41482
41593
  * Time filter is overlap (`closedAt >= since` and `openedAt <= until`),
41483
41594
  * same as {@link listOverlapping}. Pagination is applied AFTER the window
41484
41595
  * so a day cursor is honest rather than a raw-row offset that skips live
41485
- * groups on a busy camera.
41596
+ * groups on a busy camera. `deviceIds: []` is an empty page (same as
41597
+ * `listRecentTracks`); omit the field for every camera. `deviceId` stays
41598
+ * as the single-camera alias the custom-action bridge still sends.
41486
41599
  */
41487
41600
  async listRecent(input) {
41488
41601
  const limit = Math.min(Math.max(input.limit ?? 40, 1), 100);
41489
41602
  const offset = Math.max(input.offset ?? 0, 0);
41490
- const slice = (await this.store.query.query({
41603
+ const deviceIds = input.deviceIds !== void 0 ? input.deviceIds : input.deviceId !== void 0 ? [input.deviceId] : void 0;
41604
+ if (deviceIds !== void 0 && deviceIds.length === 0) return {
41605
+ groups: [],
41606
+ hasMore: false,
41607
+ nextCursor: null
41608
+ };
41609
+ const cursor = input.cursor !== void 0 && input.cursor.length > 0 ? decodeGroupCursor(input.cursor) : null;
41610
+ const filtered = (await this.store.query.query({
41491
41611
  collection: TRACK_GROUPS_COLLECTION,
41492
41612
  filter: {
41493
- ...input.deviceId !== void 0 ? { where: { deviceId: input.deviceId } } : {},
41613
+ ...deviceIds !== void 0 && deviceIds.length === 1 ? { where: { deviceId: deviceIds[0] } } : deviceIds !== void 0 ? { whereIn: { deviceId: [...deviceIds] } } : {},
41494
41614
  orderBy: {
41495
41615
  field: "closedAt",
41496
41616
  direction: "desc"
@@ -41503,11 +41623,20 @@ var TrackGroupStore = class {
41503
41623
  })).filter((g) => {
41504
41624
  if (input.sinceMs !== void 0 && g.closedAt < input.sinceMs) return false;
41505
41625
  if (input.untilMs !== void 0 && g.openedAt > input.untilMs) return false;
41626
+ if (cursor !== null && !isAfterGroupCursor(g, cursor)) return false;
41506
41627
  return true;
41507
- }).slice(offset, offset + limit + 1);
41628
+ }).toSorted(compareGroupDesc);
41629
+ const slice = (cursor !== null ? filtered : filtered.slice(offset)).slice(0, limit + 1);
41630
+ const groups = slice.slice(0, limit);
41631
+ const hasMore = slice.length > limit;
41632
+ const last = groups[groups.length - 1];
41508
41633
  return {
41509
- groups: slice.slice(0, limit),
41510
- hasMore: slice.length > limit
41634
+ groups,
41635
+ hasMore,
41636
+ nextCursor: hasMore && last !== void 0 ? encodeGroupCursor({
41637
+ closedAt: last.closedAt,
41638
+ groupId: last.id
41639
+ }) : null
41511
41640
  };
41512
41641
  }
41513
41642
  /** Groups whose life overlaps `[since, until]` on one camera. */
@@ -42839,7 +42968,7 @@ function evaluateStationaryPromotion(input) {
42839
42968
  };
42840
42969
  }
42841
42970
  var DEFAULT_PACKAGE_STILLNESS_CONFIG = {
42842
- minRounds: 2,
42971
+ minHits: 3,
42843
42972
  roundGapMs: 5e3,
42844
42973
  ttlMs: 5 * 6e4,
42845
42974
  assocIou: .3
@@ -42872,8 +43001,9 @@ var PackageStillnessTracker = class {
42872
43001
  const prev = next[index];
42873
43002
  if (prev === void 0) continue;
42874
43003
  consumed.add(prev.id);
42875
- const updated = confirm(prev, input, s, this.config.roundGapMs);
42876
- if (!(updated.promotedAt === null && updated.activityBackedAtOpen && updated.rounds >= this.config.minRounds && input.timestamp - updated.firstSeenAt >= input.minDwellMs)) {
43004
+ const updated = confirm(prev, input, s);
43005
+ const minHits = input.minHits ?? this.config.minHits;
43006
+ if (!(updated.promotedAt === null && updated.activityBackedAtOpen && updated.hits >= minHits && input.timestamp - updated.firstSeenAt >= input.minDwellMs)) {
42877
43007
  next[index] = updated;
42878
43008
  continue;
42879
43009
  }
@@ -42897,7 +43027,7 @@ var PackageStillnessTracker = class {
42897
43027
  id: c.id,
42898
43028
  className: c.className,
42899
43029
  bbox: { ...c.bbox },
42900
- rounds: c.rounds,
43030
+ hits: c.hits,
42901
43031
  firstSeenAt: c.firstSeenAt,
42902
43032
  lastSeenAt: c.lastSeenAt,
42903
43033
  activityBackedAtOpen: c.activityBackedAtOpen
@@ -42955,7 +43085,7 @@ var PackageStillnessTracker = class {
42955
43085
  frameHeight: input.frameHeight,
42956
43086
  firstSeenAt: input.timestamp,
42957
43087
  lastSeenAt: input.timestamp,
42958
- rounds: 1,
43088
+ hits: 1,
42959
43089
  promotedAt: null,
42960
43090
  activityBackedAtOpen,
42961
43091
  ...s.trackId !== void 0 ? { sourceTrackId: s.trackId } : {},
@@ -42965,18 +43095,16 @@ var PackageStillnessTracker = class {
42965
43095
  };
42966
43096
  }
42967
43097
  };
42968
- /** Re-confirm a candidate with this frame's box. A new ROUND is only counted
42969
- * when the gap since the last sighting clears `roundGapMs`, so a burst of
42970
- * frames inside one governor round counts once. */
42971
- function confirm(prev, input, s, roundGapMs) {
42972
- const newRound = input.timestamp - prev.lastSeenAt >= roundGapMs;
43098
+ /** Re-confirm a candidate with this frame's box. Every sighting is a hit —
43099
+ * a settle burst of three frames in one second is three confirmations. */
43100
+ function confirm(prev, input, s) {
42973
43101
  return {
42974
43102
  ...prev,
42975
43103
  bbox: { ...s.bbox },
42976
43104
  frameWidth: input.frameWidth,
42977
43105
  frameHeight: input.frameHeight,
42978
43106
  lastSeenAt: input.timestamp,
42979
- rounds: newRound ? prev.rounds + 1 : prev.rounds,
43107
+ hits: prev.hits + 1,
42980
43108
  ...s.trackId !== void 0 ? { sourceTrackId: s.trackId } : {},
42981
43109
  ...s.label !== void 0 ? { label: s.label } : {},
42982
43110
  ...s.confidence !== void 0 ? { confidence: s.confidence } : {},
@@ -43001,6 +43129,31 @@ function toEntry(c) {
43001
43129
  };
43002
43130
  }
43003
43131
  //#endregion
43132
+ //#region src/pipeline-analytics/pipeline/package/package-zone-activity.ts
43133
+ /**
43134
+ * Whether a live track is motion inside a configured package drop-off zone.
43135
+ *
43136
+ * The stillness activity gate used to stamp ANY non-package track on the
43137
+ * camera. A walker at the far gate then "delivered" the streetlight wedge
43138
+ * on the doormat. The operator's prerequisite is motion in the drop-off
43139
+ * zone the package rule points at — the same zones `PackageDropDetector`
43140
+ * uses for the centroid gate.
43141
+ */
43142
+ function isDelivererInPackageZone(trackZones, packageZoneIds) {
43143
+ if (packageZoneIds.size === 0 || trackZones.length === 0) return false;
43144
+ for (const z of trackZones) if (packageZoneIds.has(z)) return true;
43145
+ return false;
43146
+ }
43147
+ /** Zone ids referenced by enabled `package`-stage rules. Disabled rules do not count. */
43148
+ function collectPackageZoneIds(rules) {
43149
+ const ids = /* @__PURE__ */ new Set();
43150
+ for (const r of rules) {
43151
+ if (r.enabled === false) continue;
43152
+ for (const z of r.zoneIds) ids.add(z);
43153
+ }
43154
+ return ids;
43155
+ }
43156
+ //#endregion
43004
43157
  //#region src/pipeline-analytics/pipeline/stationary/stationary-zones.ts
43005
43158
  /**
43006
43159
  * Zone ids whose 0–1 polygon contains the entry's normalised bbox centroid.
@@ -44177,6 +44330,12 @@ var PlateRecognizer = class {
44177
44330
  text: c.plateText,
44178
44331
  score: c.plateScore,
44179
44332
  bbox: c.plateBbox,
44333
+ bodyBbox: {
44334
+ x: c.bbox.x,
44335
+ y: c.bbox.y,
44336
+ w: c.bbox.w,
44337
+ h: c.bbox.h
44338
+ },
44180
44339
  timestamp: input.timestamp,
44181
44340
  frameWidth: input.frameWidth,
44182
44341
  frameHeight: input.frameHeight,
@@ -44250,6 +44409,16 @@ var PlateRecognizer = class {
44250
44409
  }
44251
44410
  });
44252
44411
  }
44412
+ if (crop !== void 0 && input.frameHandle !== void 0 && input.bodyBbox !== void 0) this.deps.onLiveHierarchy?.({
44413
+ deviceId: input.deviceId,
44414
+ trackId: input.trackId,
44415
+ timestamp: input.timestamp,
44416
+ bodyBbox: input.bodyBbox,
44417
+ frameWidth: input.frameWidth,
44418
+ frameHeight: input.frameHeight,
44419
+ cropPadding: input.cropPadding,
44420
+ frameHandle: input.frameHandle
44421
+ });
44253
44422
  this.bestPlate.set(input.trackId, {
44254
44423
  text: input.text,
44255
44424
  score: input.score,
@@ -48284,14 +48453,23 @@ function isAfterCursor(row, cursor) {
48284
48453
  if (row.lastSeen < cursor.lastSeen) return true;
48285
48454
  return row.lastSeen === cursor.lastSeen && row.id < cursor.trackId;
48286
48455
  }
48456
+ var VEHICLE_CLASSES = new Set([
48457
+ "vehicle",
48458
+ "car",
48459
+ "truck",
48460
+ "bus"
48461
+ ]);
48462
+ function motionlessMaxPxForClass(className) {
48463
+ return VEHICLE_CLASSES.has(className.toLowerCase()) ? 8 : 40;
48464
+ }
48287
48465
  function netDisplacementPx(positions) {
48288
48466
  const first = positions[0];
48289
48467
  const last = positions[positions.length - 1];
48290
48468
  if (!first || !last) return 0;
48291
48469
  return Math.hypot(last.x - first.x, last.y - first.y);
48292
48470
  }
48293
- function isShortMotionlessTrack(lifeMs, movedPx) {
48294
- return lifeMs <= 25e3 && movedPx <= 8;
48471
+ function isShortMotionlessTrack(lifeMs, movedPx, className) {
48472
+ return lifeMs <= 25e3 && movedPx <= motionlessMaxPxForClass(className);
48295
48473
  }
48296
48474
  //#endregion
48297
48475
  //#region src/pipeline-analytics/store/track-store.ts
@@ -48875,7 +49053,7 @@ var TrackStore = class {
48875
49053
  for (const [trackId, t] of this.active) {
48876
49054
  if (nowMs - t.lastSeen < this.config.ttlMs) continue;
48877
49055
  t.active = false;
48878
- if (t.positions.length > 0 && isShortMotionlessTrack(t.lastSeen - t.firstSeen, netDisplacementPx(t.positions))) t.closedReason = "stationary-promotion";
49056
+ if (t.positions.length > 0 && isShortMotionlessTrack(t.lastSeen - t.firstSeen, netDisplacementPx(t.positions), t.className)) t.closedReason = "stationary-promotion";
48879
49057
  const record = cloneTrack(t);
48880
49058
  try {
48881
49059
  await this.persistCompleted(record);
@@ -49904,6 +50082,28 @@ var TrackStore = class {
49904
50082
  return this.rowToTrack(row.id, row.data);
49905
50083
  }
49906
50084
  /**
50085
+ * Batch read of persisted tracks. `listGroupRecords` cannot call
50086
+ * {@link getPersistedByTrackId} per member — that is one settings-store RPC
50087
+ * per row and a 60-group page takes ~20 s, which the viewer feed times out.
50088
+ */
50089
+ async getPersistedByTrackIds(trackIds) {
50090
+ if (trackIds.length === 0) return [];
50091
+ const PAGE = 500;
50092
+ const out = [];
50093
+ for (let i = 0; i < trackIds.length; i += PAGE) {
50094
+ const chunk = trackIds.slice(i, i + PAGE);
50095
+ const rows = await this.store.query.query({
50096
+ collection: TRACKS_COLLECTION,
50097
+ filter: {
50098
+ whereIn: { id: [...chunk] },
50099
+ limit: chunk.length
50100
+ }
50101
+ });
50102
+ for (const row of rows) out.push(this.rowToTrack(row.id, row.data));
50103
+ }
50104
+ return out;
50105
+ }
50106
+ /**
49907
50107
  * Persist a SYNTHETIC track (a sensor/control event projected into the
49908
50108
  * unified track store). Unlike `persistCompleted` there is no live in-RAM
49909
50109
  * state and no trajectory envelope (synthetic tracks carry no positions) —
@@ -54976,11 +55176,22 @@ function buildGlobalSettingsSchema(cap) {
54976
55176
  description: "Also emit a package-picked-up event when the delivered package leaves.",
54977
55177
  default: PACKAGE_DROP_DEFAULTS.packageDropPickupEnabled
54978
55178
  },
55179
+ {
55180
+ type: "slider",
55181
+ key: "packageDropMinFrames",
55182
+ label: "Confirmation frames",
55183
+ description: "How many frames the box must be seen on before it counts as a delivery. One is a flicker; two can still be a passer-by; three is the default bar against false positives. Independent of dwell.",
55184
+ min: 1,
55185
+ max: 10,
55186
+ step: 1,
55187
+ default: PACKAGE_DROP_DEFAULTS.packageDropMinFrames,
55188
+ showValue: true
55189
+ },
54979
55190
  {
54980
55191
  type: "slider",
54981
55192
  key: "packageDropDwellSec",
54982
55193
  label: "Minimum dwell",
54983
- description: "How long a package must stay parked before it counts as a delivery. Higher = fewer false positives from bags briefly set down.",
55194
+ description: "Extra time the package must stay parked after the confirmation frames. 0 (default) means N frames is enough — a courier who drops and leaves in a couple of seconds still delivers. Raise to reject a bag briefly set down.",
54984
55195
  min: 0,
54985
55196
  max: 300,
54986
55197
  step: 5,
@@ -55014,7 +55225,7 @@ function buildGlobalSettingsSchema(cap) {
55014
55225
  type: "slider",
55015
55226
  key: "packageDropActivityWindowSec",
55016
55227
  label: "Deliverer window",
55017
- description: "A delivery only counts if a person or vehicle was active on this camera within this many seconds before the package appeared — and a pick-up only with somebody around when it vanished. 0 disables the requirement.",
55228
+ description: "A delivery only counts if a person or vehicle was in the configured package drop-off zone within this many seconds before the package appeared — and a pick-up only with somebody in that zone when it vanished. Motion elsewhere on the camera does not count. 0 disables the requirement.",
55018
55229
  min: 0,
55019
55230
  max: 1800,
55020
55231
  step: 30,
@@ -56848,10 +57059,12 @@ var CONFIRMATION_CROP_MAX_WIDTH = 320;
56848
57059
  * a late confirmation is still worth running; see `DEFAULT_CAPTURE_PRIORITY`). */
56849
57060
  var ENRICHMENT_CAPTURE_STALE_MS = 3e3;
56850
57061
  /** Reconcile cadence for the per-device `zones` / `zoneRules` slice handles.
56851
- * The `device.state-changed` push doesn't reliably reach a forked child, so
56852
- * each cached proxy re-pulls both slices on this timer (see ensureProxy) —
56853
- * a zone drawn in the editor shows up in per-zone stats within one tick. */
56854
- var ZONE_SLICE_RECONCILE_MS = 3e4;
57062
+ * The `device.state-changed` push reaches a forked child through
57063
+ * `createEventBusSliceSource` (the DeviceProxy no longer waits on
57064
+ * `live.onEvent`). Events are still lossy telemetry (D8); this timer
57065
+ * is the RPC reconcile, not the mechanism. Five minutes matches the
57066
+ * viewer's `RECONCILE_MS`. */
57067
+ var ZONE_SLICE_RECONCILE_MS = 3e5;
56855
57068
  /** How often the evaluating node re-reads the central scene set. */
56856
57069
  var SCENE_RELOAD_INTERVAL_MS = 3e4;
56857
57070
  /** How long the install coordinates are trusted between settings reads. */
@@ -56915,38 +57128,31 @@ var MOTION_EVENT_HEARTBEAT_MS = 5e3;
56915
57128
  *
56916
57129
  * `thumbnail` joined the clean set on 2026-07-17: the track best-thumbnail is
56917
57130
  * now a subject-centered crop (event-media-dispatcher), and for DEGENERATE
56918
- * tracks (no object event → no `crop`) it is the ONLY subject-centered media —
56919
- * without it a crop-forced request degraded to the `keyFrame` FULL FRAME,
56920
- * which is how 65% of the timeline tiles rendered as whole scenes. It sits
56921
- * right after a real `crop`, before the full-scene `fullFrame`/`keyFrame`.
56922
- *
56923
- * `keyFrameSmall` (the 960px no-box downscale of the best-shot `keyFrame`)
56924
- * joined the clean set on 2026-07-21 so a crop-forced request can degrade to it
56925
- * for tracks whose only clean media is the small key frame; it sits last
56926
- * (fullest-scene, but subject-centered enough to serve as a reasonable fallback
56927
- * over 404). The retired-but-still-stored kinds (`crop`) stay listed so old rows
56928
- * keep resolving.
57131
+ * tracks (no object event → no `crop`) it is the ONLY subject-centered media.
57132
+ *
57133
+ * Full-scene kinds (`fullFrame`, `keyFrame`, `keyFrameSmall`) stay out of
57134
+ * this list so a crop is preferred when one exists. `resolveCropForcedMedia`
57135
+ * then falls through to those full-scene kinds — a keyFrame beats an icon.
57136
+ * An *explicit* `?kind=keyFrame` / `?kind=snapshot` still matches exactly, so
57137
+ * videoclips and other kind-specific fetches keep working.
57138
+ *
57139
+ * The retired-but-still-stored kind (`crop`) stays listed so old rows resolve.
56929
57140
  */
56930
57141
  var CLEAN_MEDIA_KINDS = [
56931
57142
  "crop",
56932
57143
  "thumbnail",
56933
- "thumbnailSmall",
56934
- "fullFrame",
56935
- "keyFrame",
56936
- "keyFrameSmall"
57144
+ "thumbnailSmall"
56937
57145
  ];
56938
57146
  /**
56939
- * Pick a CLEAN (never boxed) media file for a crop-forced request: the exact
56940
- * `preferKind` if it is itself clean and present, else the first available
56941
- * {@link CLEAN_MEDIA_KINDS} frame. Returns undefined when only boxed / no media
56942
- * exists (caller 404s → the viewer shows an icon).
57147
+ * Pick media for a crop-forced request: the exact `preferKind` when present
57148
+ * (so `?kind=snapshot` still serves a snapshot), else the first available
57149
+ * subject crop in {@link CLEAN_MEDIA_KINDS}. Returns undefined when no crop
57150
+ * is in the list — the caller then falls through to any remaining image
57151
+ * (`resolveCropForcedMedia`); the class icon is only for a total miss.
56943
57152
  */
56944
57153
  function pickCleanMedia(files, preferKind) {
56945
- const isClean = (k) => CLEAN_MEDIA_KINDS.includes(k);
56946
- if (isClean(preferKind)) {
56947
- const exact = files.find((f) => f.kind === preferKind);
56948
- if (exact) return exact;
56949
- }
57154
+ const exact = files.find((f) => f.kind === preferKind);
57155
+ if (exact) return exact;
56950
57156
  for (const kind of CLEAN_MEDIA_KINDS) {
56951
57157
  const found = files.find((f) => f.kind === kind);
56952
57158
  if (found) return found;
@@ -57439,11 +57645,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
57439
57645
  */
57440
57646
  packageStillness = new PackageStillnessTracker({ newId: () => (0, node_crypto.randomUUID)() });
57441
57647
  /**
57442
- * Frame-clock timestamp of the last NON-package track seen per device — the
57443
- * activity mirror behind the package activity gate ("a parcel needs a
57444
- * deliverer"). Deliberately not {@link lastTrackActivityMs}: that map is
57445
- * stamped by ANY live track, including the package-class FP itself, which
57446
- * would let the streetlight wedge vouch for its own delivery.
57648
+ * Frame-clock timestamp of the last NON-package track seen IN A PACKAGE
57649
+ * DROP-OFF ZONE per device — the activity mirror behind the package
57650
+ * activity gate ("a parcel needs a deliverer"). A walker at the far gate
57651
+ * does not stamp this. Deliberately not {@link lastTrackActivityMs}: that
57652
+ * map is stamped by ANY live track, including the package-class FP itself,
57653
+ * which would let the streetlight wedge vouch for its own delivery.
57447
57654
  */
57448
57655
  lastNonPackageActivityAt = /* @__PURE__ */ new Map();
57449
57656
  /** Last time the oversize-sighting drop was logged per device — one line
@@ -58409,6 +58616,19 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
58409
58616
  }, trackId);
58410
58617
  },
58411
58618
  markEmbeddedFace: (trackId) => stores.trackStore.markEmbeddedFace(trackId),
58619
+ copyCropToTrackPreview: async (input) => {
58620
+ await copyFaceCropIfTrackHasNoThumbnail({
58621
+ listTrackMedia: (trackId) => stores.mediaStore.listByOwner("track", trackId),
58622
+ putTrackThumbnail: (put) => stores.mediaStore.put({
58623
+ deviceId: put.deviceId,
58624
+ ownerKind: "track",
58625
+ ownerId: put.trackId,
58626
+ kind: "thumbnail",
58627
+ timestamp: put.timestamp,
58628
+ data: put.data
58629
+ })
58630
+ }, input);
58631
+ },
58412
58632
  getKeyFrameMediaKey: (trackId) => this.residents.keyFrameKey(trackId),
58413
58633
  emitFaceGalleryChanged: (payload) => this.emitFaceGalleryChanged(payload),
58414
58634
  onSemanticMatch: (match) => this.promoteSemanticRecognition({
@@ -58423,6 +58643,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
58423
58643
  confidence: match.confidence,
58424
58644
  ...match.frameHandle !== void 0 ? { frameHandle: match.frameHandle } : {}
58425
58645
  }),
58646
+ onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
58426
58647
  logger: logger.child("FaceRecognizer")
58427
58648
  });
58428
58649
  this.faceRecognizer.refreshGallery();
@@ -58455,6 +58676,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
58455
58676
  deriveCropFromKeyFrame,
58456
58677
  getKeyFrameMediaKey: (trackId) => this.residents.keyFrameKey(trackId),
58457
58678
  emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload),
58679
+ onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
58458
58680
  logger: logger.child("PlateRecognizer")
58459
58681
  });
58460
58682
  this.plateRecognizer.refreshGallery();
@@ -58810,13 +59032,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
58810
59032
  buildSummaryPorts(stores) {
58811
59033
  return {
58812
59034
  listTracks: async ({ deviceId, sinceMs, untilMs, limit }) => {
58813
- await this.groupSealer?.sealWindow(deviceId, sinceMs, untilMs);
58814
- const groups = await stores.groupStore.listOverlapping({
58815
- deviceId,
58816
- sinceMs,
58817
- untilMs,
58818
- limit
58819
- });
58820
59035
  const windowTracks = await stores.trackStore.queryHistorical({
58821
59036
  deviceId,
58822
59037
  since: sinceMs,
@@ -58824,33 +59039,19 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
58824
59039
  projection: "slim",
58825
59040
  limit: Math.max(limit, 2e3)
58826
59041
  });
58827
- const trackById = new Map(windowTracks.map((t) => [t.trackId, t]));
58828
59042
  const { minHits } = await this.resolveDeviceDetectionSensitivitySettings(deviceId);
58829
- const out = [];
58830
- for (const group of groups) {
58831
- const members = await stores.groupStore.listMembers(group.id);
58832
- const tracks = members.map((m) => trackById.get(m.trackId)).filter((t) => t !== void 0);
58833
- if (tracks.length === 0) continue;
58834
- const classes = [...new Set(tracks.flatMap((t) => [t.className, ...t.classes ?? []]))];
58835
- const zones = [...new Set(tracks.flatMap((t) => [...t.zonesVisited]))];
58836
- const importance = Math.max(0, ...tracks.map((t) => t.importance ?? 0));
58837
- const primary = tracks.reduce((best, t) => (t.importance ?? 0) > (best.importance ?? 0) ? t : best);
58838
- out.push({
58839
- trackId: group.id,
58840
- groupId: group.id,
58841
- memberTrackIds: members.map((m) => m.trackId),
58842
- deviceId,
58843
- className: primary.className,
58844
- firstSeen: group.openedAt,
58845
- lastSeen: group.closedAt,
58846
- importance,
58847
- zones,
58848
- classes,
58849
- ...tracks.every((t) => t.closedReason === "stationary-promotion") ? { stationary: true } : {},
58850
- confirmed: tracks.some((t) => t.observations !== void 0 && t.observations >= minHits)
58851
- });
58852
- }
58853
- return out;
59043
+ return windowTracks.map((t) => ({
59044
+ trackId: t.trackId,
59045
+ deviceId,
59046
+ className: t.className,
59047
+ firstSeen: t.firstSeen,
59048
+ lastSeen: t.lastSeen,
59049
+ zones: [...t.zonesVisited],
59050
+ ...t.importance !== void 0 ? { importance: t.importance } : {},
59051
+ ...t.classes !== void 0 && t.classes.length > 0 ? { classes: t.classes } : {},
59052
+ ...t.closedReason === "stationary-promotion" ? { stationary: true } : {},
59053
+ confirmed: t.observations !== void 0 && t.observations >= minHits
59054
+ }));
58854
59055
  },
58855
59056
  listMediaInfo: async (id) => {
58856
59057
  const group = await stores.groupStore.get(id);
@@ -59845,6 +60046,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
59845
60046
  refreshGallery: () => {
59846
60047
  this.plateRecognizer?.refreshGallery();
59847
60048
  },
60049
+ eventMediaBaseUrl: () => this.eventMediaBaseUrl,
59848
60050
  emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload)
59849
60051
  });
59850
60052
  return [
@@ -60333,6 +60535,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
60333
60535
  deviceId,
60334
60536
  tracked: result.tracked,
60335
60537
  packageClasses,
60538
+ packageZoneIds: collectPackageZoneIds(proxy?.state.zoneRules.value?.package ?? []),
60336
60539
  settings: packageSettings,
60337
60540
  frameWidth: result.frameWidth,
60338
60541
  frameHeight: result.frameHeight,
@@ -60900,7 +61103,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
60900
61103
  if (!first) return;
60901
61104
  const lifeMs = track.lastSeen - track.firstSeen;
60902
61105
  const moved = netDisplacementPx(track.positions ?? []);
60903
- if (!isShortMotionlessTrack(lifeMs, moved)) {
61106
+ if (!isShortMotionlessTrack(lifeMs, moved, track.className)) {
60904
61107
  this.phantomCells?.noteMovingClose({
60905
61108
  deviceId: track.deviceId,
60906
61109
  className: track.className,
@@ -61018,6 +61221,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
61018
61221
  }, d.score, d.label, frame.timestamp);
61019
61222
  if (this.plateRecognizer) {
61020
61223
  const mediaSettings = await this.resolveDeviceMediaSettings(deviceId);
61224
+ const body = this.residents.bestSeenBbox(trackId);
61021
61225
  await this.plateRecognizer.observePlateRead({
61022
61226
  deviceId,
61023
61227
  trackId,
@@ -61033,7 +61237,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
61033
61237
  frameWidth: frame.frameWidth,
61034
61238
  frameHeight: frame.frameHeight,
61035
61239
  cropPadding: mediaSettings.cropPadding,
61036
- ...frame.frameHandle !== void 0 ? { frameHandle: frame.frameHandle } : {}
61240
+ ...frame.frameHandle !== void 0 ? { frameHandle: frame.frameHandle } : {},
61241
+ ...body !== void 0 ? { bodyBbox: {
61242
+ x: body.x,
61243
+ y: body.y,
61244
+ w: body.w,
61245
+ h: body.h
61246
+ } } : {}
61037
61247
  });
61038
61248
  }
61039
61249
  }
@@ -61334,6 +61544,51 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
61334
61544
  }
61335
61545
  }).catch(() => {})));
61336
61546
  }
61547
+ /**
61548
+ * A face or plate crop just landed from a still-live handle. Fill the
61549
+ * missing parent rungs — person/vehicle thumbnail (display crop, same
61550
+ * surface that produced the detail) and native keyFrame — so Events never
61551
+ * has a child crop without a body and a scene.
61552
+ */
61553
+ requestTrackHierarchy(input) {
61554
+ ensureTrackHierarchyMedia({
61555
+ isThumbnailLanded: (trackId) => this.residents.isThumbnailLanded(trackId),
61556
+ isKeyFrameLanded: (trackId) => this.residents.keyFrameCapture(trackId).landed,
61557
+ captureBodyThumbnail: (req) => this.captureHierarchyBodyThumbnail(req),
61558
+ captureKeyFrame: async (req) => {
61559
+ await this.persistKeyFrames(req.deviceId, req.timestamp, [{
61560
+ trackId: req.trackId,
61561
+ retry: false
61562
+ }], req.frameHandle);
61563
+ }
61564
+ }, input);
61565
+ }
61566
+ async captureHierarchyBodyThumbnail(input) {
61567
+ const displayCrop = this.captureDisplayCropFn;
61568
+ const mediaStore = this.mediaStore;
61569
+ if (!displayCrop || !mediaStore) return;
61570
+ await this.captureScheduler.request({
61571
+ deviceId: input.deviceId,
61572
+ trackId: input.trackId,
61573
+ kind: "thumbnail",
61574
+ coalesceKey: captureCoalesceKey(input.deviceId, "thumbnail", `${input.trackId}:hierarchy`),
61575
+ staleAfterMs: ENRICHMENT_CAPTURE_STALE_MS,
61576
+ exec: async () => {
61577
+ if (this.residents.isThumbnailLanded(input.trackId)) return;
61578
+ const jpeg = await displayCrop(input.frameHandle, input.bodyBbox, input.frameWidth, input.frameHeight, input.cropPadding, input.trackId);
61579
+ if (!jpeg) return;
61580
+ await mediaStore.put({
61581
+ deviceId: input.deviceId,
61582
+ ownerKind: "track",
61583
+ ownerId: input.trackId,
61584
+ kind: "thumbnail",
61585
+ timestamp: input.timestamp,
61586
+ data: jpeg
61587
+ });
61588
+ this.residents.markThumbnailLanded(input.deviceId, input.trackId);
61589
+ }
61590
+ }).catch(() => {});
61591
+ }
61337
61592
  /** Attribution-time SEMANTIC best-shot promotion (D-semantic-bestshot,
61338
61593
  * 2026-08-21; see `pipeline/semantic-promotion.ts`): offer a frame from
61339
61594
  * which a recognition SUCCEEDED to the best-frame ranking with the
@@ -62047,25 +62302,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
62047
62302
  }
62048
62303
  /** Hub-only: assign this sighting in RAM. First activity on a camera
62049
62304
  * replays 2 minutes of that camera (crash recover), then observe is sync. */
62050
- enqueueLiveGroup(track, now) {
62051
- if (!this.isPostProcessingNode || !this.groupLive) return;
62052
- const deviceId = track.deviceId;
62053
- const snapshot = toGroupableTrack(track);
62054
- if (this.groupLive.isPrimed(deviceId)) {
62055
- this.groupLive.observe(snapshot, now);
62056
- return;
62057
- }
62058
- const next = (this.groupObserveTail.get(deviceId) ?? Promise.resolve()).then(async () => {
62059
- if (this.groupLive && this.groupSealer && !this.groupLive.isPrimed(deviceId)) await this.groupSealer.recoverCamera(deviceId, this.groupLive);
62060
- this.groupLive?.observe(snapshot, now);
62061
- }).catch((err) => {
62062
- this.ctx.logger.warn("group live observe failed", {
62063
- tags: { deviceId },
62064
- meta: { error: String(err) }
62065
- });
62066
- });
62067
- this.groupObserveTail.set(deviceId, next);
62068
- }
62305
+ enqueueLiveGroup(track, now) {}
62069
62306
  /** Unified retention (Phase 1): earliest retained footage per device — the
62070
62307
  * follow-recordings cutoff source. Cached 10 min: availability answers in
62071
62308
  * ~70 ms but the sweep asks once per device per pass. `null` = no footage
@@ -62511,7 +62748,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
62511
62748
  runPackageStillness(input) {
62512
62749
  if (this.packageDropDetector === null) return;
62513
62750
  const tags = { deviceId: input.deviceId };
62514
- for (const t of input.tracked) if (!input.packageClasses.has(t.className)) {
62751
+ for (const t of input.tracked) {
62752
+ if (input.packageClasses.has(t.className)) continue;
62753
+ if (!isDelivererInPackageZone(t.zones, input.packageZoneIds)) continue;
62515
62754
  this.lastNonPackageActivityAt.set(input.deviceId, input.timestamp);
62516
62755
  break;
62517
62756
  }
@@ -62558,6 +62797,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
62558
62797
  frameWidth: input.frameWidth,
62559
62798
  frameHeight: input.frameHeight,
62560
62799
  timestamp: input.timestamp,
62800
+ minHits: input.settings.packageDropMinFrames,
62561
62801
  minDwellMs: input.settings.packageDropDwellSec * 1e3,
62562
62802
  activityWindowMs: input.settings.packageDropActivityWindowSec * 1e3,
62563
62803
  ...lastActivityAt !== void 0 ? { lastActivityAt } : {}
@@ -62661,9 +62901,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
62661
62901
  for (const ex of outcome.expired) {
62662
62902
  const observedDwellMs = ex.lastSeenAt - ex.firstSeenAt;
62663
62903
  const requiredDwellMs = input.settings.packageDropDwellSec * 1e3;
62904
+ const requiredHits = input.settings.packageDropMinFrames;
62664
62905
  const reasons = [];
62665
62906
  if (!ex.activityBackedAtOpen) reasons.push("no-activity-before-open");
62666
- if (ex.rounds < 2) reasons.push("rounds");
62907
+ if (ex.hits < requiredHits) reasons.push("hits");
62667
62908
  if (observedDwellMs < requiredDwellMs) reasons.push("dwell");
62668
62909
  this.ctx.logger.info("package candidate expired without promotion", {
62669
62910
  tags,
@@ -62671,8 +62912,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
62671
62912
  candidateId: ex.id,
62672
62913
  className: ex.className,
62673
62914
  bbox: ex.bbox,
62674
- rounds: ex.rounds,
62675
- minRounds: 2,
62915
+ hits: ex.hits,
62916
+ minHits: requiredHits,
62676
62917
  observedDwellMs,
62677
62918
  requiredDwellMs,
62678
62919
  activityBackedAtOpen: ex.activityBackedAtOpen,
@@ -62853,15 +63094,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
62853
63094
  * applies to the very next frame even when frames stop briefly
62854
63095
  * (e.g. during binding flips).
62855
63096
  *
62856
- * RECONCILE: the push channel behind `subscribe` (`device.state-changed`
62857
- * via `live.onEvent`) does not reliably reach a forked addon child — a
62858
- * zone created AFTER the proxy's cold read stayed invisible until the
62859
- * addon respawned (live-diagnosed on device 617, 2026-07-16: zone slice
62860
- * populated hub-side, `zones: []` in every snapshot). Events are lossy
62861
- * telemetry (D8); the durable channel is RPC + reconcile — so each
62862
- * proxy also refreshes its two slices on a slow timer. `refresh()`
62863
- * round-trips `deviceState.getCapSlice` and fans out through the SAME
62864
- * subscribe callbacks above, so a zone edit lands within one interval.
63097
+ * RECONCILE: the push channel behind `subscribe` is `device.state-changed`
63098
+ * via the addon's `ctx.eventBus` (`createEventBusSliceSource`). That
63099
+ * declares UDS interest so the parent actually fans the slice here. Events
63100
+ * remain lossy (D8); `refresh()` round-trips `deviceState.getCapSlice` on
63101
+ * this timer as the backstop.
62865
63102
  */
62866
63103
  async ensureProxy(deviceId) {
62867
63104
  const cached = this.proxies.get(deviceId);
@@ -62934,6 +63171,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
62934
63171
  async listRecentTracks(input) {
62935
63172
  return this.queryFacade.listRecentTracks(input);
62936
63173
  }
63174
+ async listGroups(input) {
63175
+ return {
63176
+ groups: [],
63177
+ nextCursor: null
63178
+ };
63179
+ }
63180
+ async getGroup(input) {
63181
+ return null;
63182
+ }
62937
63183
  /**
62938
63184
  * Every event kind the device can produce: built-ins (motion + audio),
62939
63185
  * detection classes actually observed on the device, and sensor kinds
@@ -64078,33 +64324,38 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
64078
64324
  buildFaceRescoreActionHandlers() {
64079
64325
  return { "face.rescoreTrack": (input) => this.rescoreFaceForTrack(input) };
64080
64326
  }
64081
- buildGroupActionHandlers() {
64082
- const ports = () => {
64083
- const groupStore = this.groupStore;
64084
- if (groupStore === null) return null;
64085
- return {
64086
- groupStore,
64087
- loadTrack: async (trackId) => {
64088
- const tracks = this.trackStore;
64089
- if (!tracks) return null;
64090
- return tracks.getActiveByTrack(trackId) ?? await tracks.getPersistedByTrackId(trackId);
64091
- },
64092
- eventMediaBaseUrl: () => this.eventMediaBaseUrl
64093
- };
64327
+ groupQueryPorts() {
64328
+ const groupStore = this.groupStore;
64329
+ if (groupStore === null) return null;
64330
+ return {
64331
+ groupStore,
64332
+ loadTracks: async (trackIds) => {
64333
+ const map = /* @__PURE__ */ new Map();
64334
+ const tracks = this.trackStore;
64335
+ if (!tracks || trackIds.length === 0) return map;
64336
+ const missing = [];
64337
+ for (const id of trackIds) {
64338
+ const active = tracks.getActiveByTrack(id);
64339
+ if (active !== null) map.set(id, trackToLite(active));
64340
+ else missing.push(id);
64341
+ }
64342
+ if (missing.length === 0) return map;
64343
+ for (const persisted of await tracks.getPersistedByTrackIds(missing)) map.set(persisted.trackId, trackToLite(persisted));
64344
+ return map;
64345
+ },
64346
+ eventMediaBaseUrl: () => this.eventMediaBaseUrl
64094
64347
  };
64348
+ }
64349
+ buildGroupActionHandlers() {
64095
64350
  return {
64096
64351
  "analytics.listGroups": async (input) => {
64097
- const ready = ports();
64098
- if (ready === null) return {
64352
+ return {
64099
64353
  groups: [],
64100
64354
  hasMore: false
64101
64355
  };
64102
- return listGroupRecords(ready, input);
64103
64356
  },
64104
64357
  "analytics.getGroup": async (input) => {
64105
- const ready = ports();
64106
- if (ready === null) return { group: null };
64107
- return { group: await getGroupDetail(ready, input.groupId) };
64358
+ return { group: null };
64108
64359
  }
64109
64360
  };
64110
64361
  }
@@ -64778,14 +65029,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
64778
65029
  async readEventThumbnail(id, preferKind) {
64779
65030
  const eventFiles = await (this.mediaStore?.listByOwner("event", id) ?? Promise.resolve([]));
64780
65031
  if (preferKind !== void 0 && preferKind.length > 0) {
64781
- const trackFiles = await (this.mediaStore?.listByOwner("track", id) ?? Promise.resolve([]));
64782
- const ownerTrackId = trackFiles.length === 0 ? await (this.eventStore?.getTrackIdForEvent(id) ?? Promise.resolve(null)) : null;
64783
- const owningTrackFiles = ownerTrackId === null ? [] : await (this.mediaStore?.listByOwner("track", ownerTrackId) ?? Promise.resolve([]));
64784
- const chosen = resolveCropForcedMedia([
64785
- ...eventFiles,
64786
- ...trackFiles,
64787
- ...owningTrackFiles
64788
- ], (files) => pickCleanMedia(files, preferKind));
65032
+ const layers = await collectCropForcedMedia({
65033
+ listByOwner: (ownerKind, ownerId) => this.mediaStore?.listByOwner(ownerKind, ownerId) ?? Promise.resolve([]),
65034
+ getTrackIdForEvent: (eventId) => this.eventStore?.getTrackIdForEvent(eventId) ?? Promise.resolve(null)
65035
+ }, id);
65036
+ const chosen = resolveCropForcedMedia(layers.bodyFiles, (files) => pickCleanMedia(files, preferKind), layers.detailFiles);
64789
65037
  if (!chosen) return null;
64790
65038
  return {
64791
65039
  bytes: Buffer.from(chosen.base64, "base64"),