@camstack/addon-post-analysis 1.2.53 → 1.2.55

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-DGdY1GSf.js");
5
+ const require_dist = require("../dist-B1CMW3_t.js");
6
6
  let node_fs = require("node:fs");
7
7
  let node_fs$1 = require_dist.__toESM(node_fs, 1);
8
8
  node_fs = require_dist.__toESM(node_fs);
@@ -10,8 +10,11 @@ let node_path = require("node:path");
10
10
  let node_path$1 = require_dist.__toESM(node_path, 1);
11
11
  node_path = require_dist.__toESM(node_path);
12
12
  require("node:crypto");
13
+ let node_util = require("node:util");
14
+ let node_zlib = require("node:zlib");
13
15
  let node_child_process = require("node:child_process");
14
- //#region ../system/dist/model-download-service-D8B-4ktF.mjs
16
+ (0, node_util.promisify)(node_zlib.brotliCompress);
17
+ (0, node_util.promisify)(node_zlib.gzip);
15
18
  function isNonEmptyFile(filePath) {
16
19
  return node_fs$1.existsSync(filePath) && node_fs$1.statSync(filePath).size > 0;
17
20
  }
@@ -1,15 +1,17 @@
1
- import { A as embeddingEncoderCapability, G as BaseAddon, N as hfModelUrl } from "../dist-Da97q0x6.mjs";
1
+ import { A as embeddingEncoderCapability, G as BaseAddon, N as hfModelUrl } from "../dist-D9Bm8RyD.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import * as fs from "node:fs";
4
4
  import "node:fs";
5
5
  import * as path$1 from "node:path";
6
6
  import "node:path";
7
7
  import "node:crypto";
8
+ import { promisify } from "node:util";
9
+ import { brotliCompress, gzip } from "node:zlib";
8
10
  import { spawn } from "node:child_process";
9
11
  //#region \0rolldown/runtime.js
10
12
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
11
- //#endregion
12
- //#region ../system/dist/model-download-service-D8B-4ktF.mjs
13
+ promisify(brotliCompress);
14
+ promisify(gzip);
13
15
  function isNonEmptyFile(filePath) {
14
16
  return fs.existsSync(filePath) && fs.statSync(filePath).size > 0;
15
17
  }
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-DGdY1GSf.js");
1
+ const require_dist = require("./dist-B1CMW3_t.js");
2
2
  let node_fs = require("node:fs");
3
3
  node_fs = require_dist.__toESM(node_fs, 1);
4
4
  let node_path = require("node:path");
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.47",
21
+ version: "1.2.51",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_pipeline_analytics_widgets",
@@ -33,7 +33,7 @@ var e = {
33
33
  },
34
34
  "@camstack/ui-library": {
35
35
  name: "@camstack/ui-library",
36
- version: "1.2.35",
36
+ version: "1.2.36",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_pipeline_analytics_widgets",
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.47",
39
+ version: "1.2.51",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.35",
84
+ version: "1.2.36",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -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-DGdY1GSf.js");
5
+ const require_dist = require("../dist-B1CMW3_t.js");
6
6
  let node_fs = require("node:fs");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_dist.__toESM(node_path);
@@ -6771,6 +6771,40 @@ function makeNcActionHandlers(deps) {
6771
6771
  }
6772
6772
  };
6773
6773
  }
6774
+ //#endregion
6775
+ //#region src/pipeline-analytics/data-plane-access.ts
6776
+ /**
6777
+ * Prefix literals for the export planes, mirrored here so the policy reads as
6778
+ * a table without importing three handler modules into the addon's boot path.
6779
+ * `data-plane-access.spec.ts` asserts they still match the constants their
6780
+ * handlers register with — a mirror nothing checks is a lie waiting to happen.
6781
+ */
6782
+ var TRAINING_EXPORT_PREFIX_REF = "training-export";
6783
+ var RETRAIN_EXPORT_PREFIX_REF = "retrain-export";
6784
+ var DEBUG_EXPORT_PREFIX_REF = "debug-export";
6785
+ /**
6786
+ * Every data plane this addon serves, and the level the HUB enforces before it
6787
+ * proxies. Exhaustive by test: a new plane that forgets to appear here fails
6788
+ * `data-plane-access.spec.ts`.
6789
+ */
6790
+ var PIPELINE_ANALYTICS_PLANE_ACCESS = {
6791
+ /** One stored crop/thumbnail by an unguessable id the caller already holds. */
6792
+ "event-media": "authenticated",
6793
+ /** Self-authenticating single-use action token (D47). */
6794
+ "nc-action": "public",
6795
+ /** Self-authenticating single-use artifact token (D47). */
6796
+ "nc-artifact": "public",
6797
+ /** Every frame behind a marked selection — a corpus, not an item. */
6798
+ [TRAINING_EXPORT_PREFIX_REF]: "admin",
6799
+ [RETRAIN_EXPORT_PREFIX_REF]: "admin",
6800
+ /** Diagnostic bundle: logs, config and frames. */
6801
+ [DEBUG_EXPORT_PREFIX_REF]: "admin"
6802
+ };
6803
+ /** The declared level for a plane prefix. Typed, so a prefix with no declared
6804
+ * policy is a compile error rather than a silent default. */
6805
+ function planeAccess(prefix) {
6806
+ return PIPELINE_ANALYTICS_PLANE_ACCESS[prefix];
6807
+ }
6774
6808
  var MAX_EXPORT_BYTES = 256 * 1024 * 1024;
6775
6809
  var DebugFindingSchema = require_dist.object({
6776
6810
  code: require_dist.string(),
@@ -7129,40 +7163,6 @@ function parseDebugDeviceIds(url) {
7129
7163
  return out;
7130
7164
  }
7131
7165
  //#endregion
7132
- //#region src/pipeline-analytics/data-plane-access.ts
7133
- /**
7134
- * Prefix literals for the export planes, mirrored here so the policy reads as
7135
- * a table without importing three handler modules into the addon's boot path.
7136
- * `data-plane-access.spec.ts` asserts they still match the constants their
7137
- * handlers register with — a mirror nothing checks is a lie waiting to happen.
7138
- */
7139
- var TRAINING_EXPORT_PREFIX_REF = "training-export";
7140
- var RETRAIN_EXPORT_PREFIX_REF = "retrain-export";
7141
- var DEBUG_EXPORT_PREFIX_REF = "debug-export";
7142
- /**
7143
- * Every data plane this addon serves, and the level the HUB enforces before it
7144
- * proxies. Exhaustive by test: a new plane that forgets to appear here fails
7145
- * `data-plane-access.spec.ts`.
7146
- */
7147
- var PIPELINE_ANALYTICS_PLANE_ACCESS = {
7148
- /** One stored crop/thumbnail by an unguessable id the caller already holds. */
7149
- "event-media": "authenticated",
7150
- /** Self-authenticating single-use action token (D47). */
7151
- "nc-action": "public",
7152
- /** Self-authenticating single-use artifact token (D47). */
7153
- "nc-artifact": "public",
7154
- /** Every frame behind a marked selection — a corpus, not an item. */
7155
- [TRAINING_EXPORT_PREFIX_REF]: "admin",
7156
- [RETRAIN_EXPORT_PREFIX_REF]: "admin",
7157
- /** Diagnostic bundle: logs, config and frames. */
7158
- [DEBUG_EXPORT_PREFIX_REF]: "admin"
7159
- };
7160
- /** The declared level for a plane prefix. Typed, so a prefix with no declared
7161
- * policy is a compile error rather than a silent default. */
7162
- function planeAccess(prefix) {
7163
- return PIPELINE_ANALYTICS_PLANE_ACCESS[prefix];
7164
- }
7165
- //#endregion
7166
7166
  //#region src/pipeline-analytics/debug/debug-export-handler.ts
7167
7167
  /**
7168
7168
  * Pacing. The export is a BACKGROUND job sharing a thread with live decode, so
@@ -7689,6 +7689,25 @@ var embeddingActions = require_dist.defineCustomActions({
7689
7689
  "embedding.wipe": require_dist.customAction(NoInputSchema, WipeResultSchema, { auth: "admin" })
7690
7690
  });
7691
7691
  //#endregion
7692
+ //#region src/pipeline-analytics/map-with-concurrency.ts
7693
+ /**
7694
+ * Apply `mapper` to every item with at most `limit` in flight at once.
7695
+ *
7696
+ * A rejecting mapper rejects the whole call (same contract as `Promise.all`);
7697
+ * callers that must not fail a page for one bad row catch inside the mapper.
7698
+ */
7699
+ async function mapWithConcurrency(items, limit, mapper) {
7700
+ const results = new Array(items.length);
7701
+ if (items.length === 0) return results;
7702
+ const effectiveLimit = Math.max(1, Math.min(Math.trunc(limit), items.length));
7703
+ const entries = items.entries();
7704
+ const worker = async () => {
7705
+ for (const [index, item] of entries) results[index] = await mapper(item, index);
7706
+ };
7707
+ await Promise.all(Array.from({ length: effectiveLimit }, () => worker()));
7708
+ return results;
7709
+ }
7710
+ //#endregion
7692
7711
  //#region src/pipeline-analytics/pipeline/key-event-scoring.ts
7693
7712
  /**
7694
7713
  * key-event-scoring — the SINGLE deterministic "importance" scorer for a
@@ -7960,6 +7979,16 @@ function clusterByEmbedding(items, threshold = .5) {
7960
7979
  var OPERATOR_FACE_STEP_ID = "operator:face-gallery";
7961
7980
  /** Embedding model id used when enrolling face crops as identity samples. */
7962
7981
  var MODEL_ID = "arcface-r100";
7982
+ /**
7983
+ * How many face crops `listRecentFaces` resolves at once.
7984
+ *
7985
+ * Each crop costs a store round-trip plus a disk read, so the page is I/O-bound
7986
+ * and the serial form dominated the operator's assign latency. 16 recovers
7987
+ * nearly all of the win while leaving the UDS channel — shared with the live
7988
+ * detection path — far from saturated; unbounded would trade a slow page for a
7989
+ * stalled pipeline.
7990
+ */
7991
+ var FACE_CROP_FETCH_CONCURRENCY = 16;
7963
7992
  var FaceGalleryProvider = class {
7964
7993
  identityStore;
7965
7994
  faceStore;
@@ -7968,6 +7997,7 @@ var FaceGalleryProvider = class {
7968
7997
  eventStore;
7969
7998
  logger;
7970
7999
  refreshGallery;
8000
+ eventMediaBaseUrl;
7971
8001
  emitFaceGalleryChanged;
7972
8002
  constructor(deps) {
7973
8003
  this.identityStore = deps.identityStore;
@@ -7977,6 +8007,7 @@ var FaceGalleryProvider = class {
7977
8007
  this.eventStore = deps.eventStore;
7978
8008
  this.logger = deps.logger;
7979
8009
  this.refreshGallery = deps.refreshGallery;
8010
+ this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
7980
8011
  this.emitFaceGalleryChanged = deps.emitFaceGalleryChanged;
7981
8012
  }
7982
8013
  /** Best-effort emit — never allowed to fail the mutation it follows. */
@@ -8068,6 +8099,21 @@ var FaceGalleryProvider = class {
8068
8099
  const media = await this.mediaStore.listByOwner("face", face.id);
8069
8100
  return media.length > 0 ? media[0].base64 : void 0;
8070
8101
  }
8102
+ /**
8103
+ * The crop as a data-plane URL, or undefined when there is no stored key or
8104
+ * no plane to serve it.
8105
+ *
8106
+ * The `event-media` plane resolves a raw MediaStore key (any id containing
8107
+ * `:` goes to `getByKey`), so a face crop needs NO new plane and no new
8108
+ * access decision — the same `authenticated` level already serves the track
8109
+ * thumbnails the same operator is looking at.
8110
+ */
8111
+ cropUrlFor(face) {
8112
+ if (face.mediaKey === void 0 || face.mediaKey === "") return void 0;
8113
+ const base = this.eventMediaBaseUrl();
8114
+ if (base === null) return void 0;
8115
+ return `${base}/${encodeURIComponent(face.mediaKey)}`;
8116
+ }
8071
8117
  async listRecentFaces(input) {
8072
8118
  const faces = await this.faceStore.listAllRecentFaces({
8073
8119
  limit: input?.limit,
@@ -8075,11 +8121,12 @@ var FaceGalleryProvider = class {
8075
8121
  });
8076
8122
  const identities = await this.identityStore.listIdentities();
8077
8123
  const nameMap = new Map(identities.map((i) => [i.id, i.name]));
8078
- const result = [];
8079
- for (const face of faces) {
8080
- const base64 = await this.faceCropBase64(face);
8124
+ const includeCrops = input?.includeCrops ?? true;
8125
+ return mapWithConcurrency(faces, FACE_CROP_FETCH_CONCURRENCY, async (face) => {
8126
+ const base64 = includeCrops ? await this.faceCropBase64(face) : void 0;
8127
+ const cropUrl = this.cropUrlFor(face);
8081
8128
  const identityName = face.recognizedIdentityId ? nameMap.get(face.recognizedIdentityId) : void 0;
8082
- result.push({
8129
+ return {
8083
8130
  faceId: face.id,
8084
8131
  deviceId: face.deviceId,
8085
8132
  trackId: face.trackId,
@@ -8088,15 +8135,15 @@ var FaceGalleryProvider = class {
8088
8135
  ...identityName !== void 0 ? { identityName } : {},
8089
8136
  assigned: face.assigned,
8090
8137
  ...base64 !== void 0 ? { base64 } : {},
8138
+ ...cropUrl !== void 0 ? { cropUrl } : {},
8091
8139
  ...face.faceBbox !== void 0 ? { faceBbox: face.faceBbox } : {},
8092
8140
  ...face.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: face.keyFrameMediaKey } : {},
8093
8141
  ...face.bestMatchScore != null ? { bestMatchScore: face.bestMatchScore } : {},
8094
8142
  ...face.nativeFaceShortSidePx != null ? { nativeFaceShortSidePx: face.nativeFaceShortSidePx } : {},
8095
8143
  ...face.suggestedIdentityId != null ? { suggestedIdentityId: face.suggestedIdentityId } : {},
8096
8144
  ...face.suggestedMatchScore != null ? { suggestedMatchScore: face.suggestedMatchScore } : {}
8097
- });
8098
- }
8099
- return result;
8145
+ };
8146
+ });
8100
8147
  }
8101
8148
  /**
8102
8149
  * Get a face by deviceId + trackId. Returns null if not found or if
@@ -12405,23 +12452,73 @@ async function makeSquareThumb(bytes, size) {
12405
12452
  }).jpeg({ quality: THUMB_QUALITY }).toBuffer();
12406
12453
  }
12407
12454
  //#endregion
12455
+ //#region src/pipeline-analytics/zone-name-stamp.ts
12456
+ /**
12457
+ * Copy each track with `zoneNames` stamped on.
12458
+ *
12459
+ * Three silences, all deliberate, all of them "say nothing" rather than "say
12460
+ * something wrong":
12461
+ *
12462
+ * - a track that visited no zone gets NO field (an empty array on every row of
12463
+ * a 1000-row page is pure wire cost for no information);
12464
+ * - a zone id with no entry in the catalogue — a zone deleted since the track
12465
+ * was written — is DROPPED. `zoneNames` is therefore not positionally
12466
+ * aligned with `zonesVisited` and can be shorter;
12467
+ * - a device whose catalogue could not be read at all leaves the track exactly
12468
+ * as it arrived. A zones cap that is momentarily down must not look like a
12469
+ * track that was never in a zone.
12470
+ */
12471
+ function stampZoneNames(tracks, lookup) {
12472
+ return tracks.map((track) => {
12473
+ if (track.zonesVisited.length === 0) return track;
12474
+ const catalogue = lookup(track.deviceId);
12475
+ if (catalogue === null) return track;
12476
+ const names = [];
12477
+ for (const id of track.zonesVisited) {
12478
+ const name = catalogue.get(id);
12479
+ if (name !== void 0 && name.length > 0 && !names.includes(name)) names.push(name);
12480
+ }
12481
+ if (names.length === 0) return track;
12482
+ return {
12483
+ ...track,
12484
+ zoneNames: names
12485
+ };
12486
+ });
12487
+ }
12488
+ //#endregion
12408
12489
  //#region src/pipeline-analytics/analytics-query-facade.ts
12409
12490
  var AnalyticsQueryFacade = class {
12410
12491
  deps;
12411
12492
  constructor(deps) {
12412
12493
  this.deps = deps;
12413
12494
  }
12495
+ /**
12496
+ * Stamp the human `zoneNames` onto a batch of tracks.
12497
+ *
12498
+ * ONE place, on the read path, for every track surface — `zonesVisited` is
12499
+ * ids and a client cannot resolve them without a per-camera fan-out. A hub
12500
+ * with no catalogue reader configured returns the tracks untouched.
12501
+ */
12502
+ async withZoneNames(tracks) {
12503
+ const cache = this.deps.zoneCatalogue?.() ?? null;
12504
+ if (cache === null || tracks.length === 0) return tracks;
12505
+ const zoned = tracks.filter((t) => t.zonesVisited.length > 0);
12506
+ if (zoned.length === 0) return tracks;
12507
+ await cache.ensure(zoned.map((t) => t.deviceId));
12508
+ return stampZoneNames(tracks, (deviceId) => cache.lookup(deviceId));
12509
+ }
12414
12510
  async getActiveTracks(input) {
12415
- return this.deps.trackStore()?.getActive(input.deviceId) ?? [];
12511
+ return this.withZoneNames(this.deps.trackStore()?.getActive(input.deviceId) ?? []);
12416
12512
  }
12417
12513
  async getTrack(input) {
12418
12514
  const store = this.deps.trackStore();
12419
- const active = store?.getActiveByTrack(input.trackId);
12420
- if (active) return active;
12421
- return await store?.getPersistedByTrackId(input.trackId) ?? null;
12515
+ const track = store?.getActiveByTrack(input.trackId) ?? await store?.getPersistedByTrackId(input.trackId) ?? null;
12516
+ if (track === null) return null;
12517
+ const [named] = await this.withZoneNames([track]);
12518
+ return named ?? track;
12422
12519
  }
12423
12520
  async listTracks(input) {
12424
- return this.deps.trackStore()?.queryHistorical(input) ?? [];
12521
+ return this.withZoneNames(await this.deps.trackStore()?.queryHistorical(input) ?? []);
12425
12522
  }
12426
12523
  /**
12427
12524
  * Batched cluster-wide track listing — one merged (`lastSeen` DESC,
@@ -12431,10 +12528,14 @@ var AnalyticsQueryFacade = class {
12431
12528
  * indexed page + k-way merge and the cursor encoding.
12432
12529
  */
12433
12530
  async listRecentTracks(input) {
12434
- return this.deps.trackStore()?.queryRecent(input) ?? {
12531
+ const page = await this.deps.trackStore()?.queryRecent(input) ?? {
12435
12532
  tracks: [],
12436
12533
  nextCursor: null
12437
12534
  };
12535
+ return {
12536
+ ...page,
12537
+ tracks: await this.withZoneNames(page.tracks)
12538
+ };
12438
12539
  }
12439
12540
  /** Per-camera sensor-event history (state changes of linked devices). */
12440
12541
  async getSensorEvents(input) {
@@ -17106,6 +17207,72 @@ var RetrainService = class {
17106
17207
  }
17107
17208
  };
17108
17209
  //#endregion
17210
+ //#region src/pipeline-analytics/zone-catalogue-cache.ts
17211
+ /**
17212
+ * zone-catalogue-cache — the per-device zone id→name dictionary the track read
17213
+ * path stamps `zoneNames` from.
17214
+ *
17215
+ * The `zones` capability is per-device, so resolving names on a merged
17216
+ * `listRecentTracks` page would otherwise be one cap call per camera per page —
17217
+ * the exact fan-out that surface exists to remove. One read per camera per TTL
17218
+ * is the whole cost, against a catalogue that changes only when an operator
17219
+ * draws a polygon.
17220
+ *
17221
+ * The failure rule is the important part: a refresh that throws KEEPS the last
17222
+ * known catalogue. Dropping it would make every zoned track look unzoned for
17223
+ * the next minute, which reads as "zones stopped working" rather than "the
17224
+ * zones cap blipped".
17225
+ */
17226
+ /** How long a device's zone catalogue is trusted before a re-read. A zone
17227
+ * rename should reach the feed without a restart; a minute is invisible to an
17228
+ * operator and reduces the read to ~16 cheap calls/min on this fleet. */
17229
+ var ZONE_CATALOGUE_TTL_MS = 6e4;
17230
+ var ZoneCatalogueCache = class {
17231
+ deps;
17232
+ entries = /* @__PURE__ */ new Map();
17233
+ /** In-flight reads, so a burst of pages for the same camera makes one call. */
17234
+ inFlight = /* @__PURE__ */ new Map();
17235
+ constructor(deps) {
17236
+ this.deps = deps;
17237
+ }
17238
+ /** Make sure every device has a catalogue no older than the TTL. */
17239
+ async ensure(deviceIds) {
17240
+ const stale = [...new Set(deviceIds)].filter((id) => this.isStale(id));
17241
+ await Promise.all(stale.map((id) => this.refresh(id)));
17242
+ }
17243
+ /** The device's zone id→name map, or null when it was never readable. */
17244
+ lookup(deviceId) {
17245
+ return this.entries.get(deviceId)?.names ?? null;
17246
+ }
17247
+ isStale(deviceId) {
17248
+ const held = this.entries.get(deviceId);
17249
+ if (held === void 0) return true;
17250
+ return this.deps.now() - held.readAtMs >= ZONE_CATALOGUE_TTL_MS;
17251
+ }
17252
+ async refresh(deviceId) {
17253
+ const running = this.inFlight.get(deviceId);
17254
+ if (running !== void 0) return running;
17255
+ const task = this.read(deviceId).finally(() => {
17256
+ this.inFlight.delete(deviceId);
17257
+ });
17258
+ this.inFlight.set(deviceId, task);
17259
+ return task;
17260
+ }
17261
+ async read(deviceId) {
17262
+ try {
17263
+ const zones = await this.deps.fetchZones(deviceId);
17264
+ const names = /* @__PURE__ */ new Map();
17265
+ for (const zone of zones) if (zone.id.length > 0 && zone.name.length > 0) names.set(zone.id, zone.name);
17266
+ this.entries.set(deviceId, {
17267
+ names,
17268
+ readAtMs: this.deps.now()
17269
+ });
17270
+ } catch (error) {
17271
+ this.deps.onError(deviceId, error);
17272
+ }
17273
+ }
17274
+ };
17275
+ //#endregion
17109
17276
  //#region src/pipeline-analytics/media-capture-log.ts
17110
17277
  var DEFAULT_FLUSH_INTERVAL_MS$1 = 6e4;
17111
17278
  var MediaCaptureLogAggregator = class {
@@ -20124,6 +20291,21 @@ var PlateRecognizer = class {
20124
20291
  }
20125
20292
  };
20126
20293
  //#endregion
20294
+ //#region src/pipeline-analytics/read-slice.ts
20295
+ /**
20296
+ * The loaded slice, or `undefined` when it has never loaded and the one
20297
+ * permitted refresh also failed to produce it.
20298
+ *
20299
+ * A failing refresh is swallowed deliberately: every caller degrades to "no
20300
+ * zones / no rules", and a zone read must never take down the frame that asked.
20301
+ */
20302
+ async function readSliceOnce(handle) {
20303
+ const loaded = handle.value;
20304
+ if (loaded !== void 0) return loaded;
20305
+ await handle.refresh().catch(() => void 0);
20306
+ return handle.value;
20307
+ }
20308
+ //#endregion
20127
20309
  //#region src/pipeline-analytics/rebuild-detail-request.ts
20128
20310
  /** The one step a rebuild runs. */
20129
20311
  var REBUILD_DETAIL_STEP_ID = "clip-embedding";
@@ -30944,6 +31126,27 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
30944
31126
  * bodies live in `analytics-query-facade.ts`; the class keeps one-line
30945
31127
  * delegates because the pipeline-analytics cap registers `this` as its
30946
31128
  * provider. Deps are lazy getters — the stores are late-bound in boot. */
31129
+ /**
31130
+ * Zone id→name dictionary for the track read path.
31131
+ *
31132
+ * `zonesVisited` persists ids; every operator-facing surface needs the name.
31133
+ * Cached per device (see `ZoneCatalogueCache`) so a merged 1000-row page
31134
+ * costs at most one `listZones` per camera per minute rather than one per
31135
+ * page.
31136
+ */
31137
+ zoneCatalogue = new ZoneCatalogueCache({
31138
+ fetchZones: (deviceId) => this.ctx.api.zones.listZones.query({ deviceId }),
31139
+ now: () => Date.now(),
31140
+ onError: (deviceId, error) => {
31141
+ this.ctx.logger.warn("zone catalogue read failed — zone names unresolved for this device", {
31142
+ tags: { deviceId },
31143
+ meta: {
31144
+ deviceId,
31145
+ error: require_dist.errMsg(error)
31146
+ }
31147
+ });
31148
+ }
31149
+ });
30947
31150
  queryFacade = new AnalyticsQueryFacade({
30948
31151
  trackStore: () => this.trackStore,
30949
31152
  eventStore: () => this.eventStore,
@@ -30951,7 +31154,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
30951
31154
  sensorEventStore: () => this.sensorEventStore,
30952
31155
  opsLog: () => this.eventsOpsLog,
30953
31156
  eventMediaBaseUrl: () => this.eventMediaBaseUrl,
30954
- logger: () => this.ctx.logger
31157
+ logger: () => this.ctx.logger,
31158
+ zoneCatalogue: () => this.zoneCatalogue
30955
31159
  });
30956
31160
  /** The shared crop extractor (native-res first, detection-frame fallback),
30957
31161
  * captured in the constructor so `processFrame` can crop object thumbnails in
@@ -31305,7 +31509,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
31305
31509
  let storage = this.ctx.kernel.storage;
31306
31510
  const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
31307
31511
  if (mediaRoot) {
31308
- const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-CpXZkQ6V.js"));
31512
+ const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-cjH3qw1E.js"));
31309
31513
  storage = new FilesystemStorageProvider(mediaRoot, { eventMedia: mediaRoot });
31310
31514
  logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
31311
31515
  }
@@ -32316,6 +32520,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
32316
32520
  eventStore: stores.eventStore,
32317
32521
  logger: this.ctx.logger,
32318
32522
  refreshGallery: () => this.faceRecognizer?.refreshGallery(),
32523
+ eventMediaBaseUrl: () => this.eventMediaBaseUrl,
32319
32524
  emitFaceGalleryChanged: (payload) => this.emitFaceGalleryChanged(payload)
32320
32525
  });
32321
32526
  const plateGallery = new PlateGalleryProvider({
@@ -33327,10 +33532,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
33327
33532
  async resolveDevicePackageRules(deviceId) {
33328
33533
  const proxy = await this.ensureProxy(deviceId);
33329
33534
  if (!proxy) return [];
33330
- const cached = proxy.state.zoneRules.value?.package;
33331
- if (cached && cached.length > 0) return cached;
33332
- await proxy.state.zoneRules.refresh().catch(() => void 0);
33333
- return proxy.state.zoneRules.value?.package ?? [];
33535
+ return (await readSliceOnce(proxy.state.zoneRules))?.package ?? [];
33334
33536
  }
33335
33537
  /**
33336
33538
  * Route one track's `runDetailSubtree` results (two-plane detail dispatch)
@@ -34638,10 +34840,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
34638
34840
  async resolveDeviceZones(deviceId) {
34639
34841
  const proxy = await this.ensureProxy(deviceId);
34640
34842
  if (!proxy) return [];
34641
- const cached = proxy.state.zones.value?.zones;
34642
- if (cached && cached.length > 0) return cached;
34643
- await proxy.state.zones.refresh().catch(() => void 0);
34644
- return proxy.state.zones.value?.zones ?? [];
34843
+ return (await readSliceOnce(proxy.state.zones))?.zones ?? [];
34645
34844
  }
34646
34845
  releaseProxy(deviceId) {
34647
34846
  const unsubs = this.proxyUnsubs.get(deviceId);