@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.
@@ -1,4 +1,4 @@
1
- import { $ as array, B as subKindsOf, C as addonWidgetsSourceCapability, D as cosineSimilarity, E as buildEventKindDescriptor, F as kebabToCamel, G as BaseAddon, H as videoclipsCapability, I as notificationRulesCapability, J as hydrateSchema, K as DeviceType, L as pipelineAnalyticsCapability, M as faceGalleryCapability, O as customAction, P as isScheduleActive, Q as _enum, R as plateGalleryCapability, S as TrackSourceSchema, T as audioMetricsCapability, U as zoneAnalyticsCapability, V as vectorDimFromBase64, W as errMsg, X as nodePin, Y as isDeviceScopedCap, Z as sleep, _ as NcTaxonomySchema, a as EVENT_PAD_MS, at as string, b as TimelapseRuleInputSchema, c as NC_CONDITION_CATALOG, d as NcRuleInputSchema, et as boolean, f as NcRulePatchSchema, g as NcSnoozeSuppressedSchema, h as NcSnoozeSchema, i as EVENT_KIND_BY_CAP, it as record, j as encodeVectorBase64, k as defineCustomActions, l as NC_TAXONOMY, m as NcSnoozeInputSchema, n as DEFAULT_EVENT_COLOR, nt as number, o as LabelAttributionSchema, ot as unknown, p as NcRuleSchema, q as createEvent, r as DeclaredDevices, rt as object, s as MACRO_LABELS, st as EventCategory, t as BaseDevice, tt as literal, u as NcConditionDescriptorSchema, v as OpsLogEntrySchema, w as alarmPanelCapability, x as TimelapseRuleSchema, y as RetrainStatusSchema, z as readDeviceStateFrom } from "../dist-Da97q0x6.mjs";
1
+ import { $ as array, B as subKindsOf, C as addonWidgetsSourceCapability, D as cosineSimilarity, E as buildEventKindDescriptor, F as kebabToCamel, G as BaseAddon, H as videoclipsCapability, I as notificationRulesCapability, J as hydrateSchema, K as DeviceType, L as pipelineAnalyticsCapability, M as faceGalleryCapability, O as customAction, P as isScheduleActive, Q as _enum, R as plateGalleryCapability, S as TrackSourceSchema, T as audioMetricsCapability, U as zoneAnalyticsCapability, V as vectorDimFromBase64, W as errMsg, X as nodePin, Y as isDeviceScopedCap, Z as sleep, _ as NcTaxonomySchema, a as EVENT_PAD_MS, at as string, b as TimelapseRuleInputSchema, c as NC_CONDITION_CATALOG, d as NcRuleInputSchema, et as boolean, f as NcRulePatchSchema, g as NcSnoozeSuppressedSchema, h as NcSnoozeSchema, i as EVENT_KIND_BY_CAP, it as record, j as encodeVectorBase64, k as defineCustomActions, l as NC_TAXONOMY, m as NcSnoozeInputSchema, n as DEFAULT_EVENT_COLOR, nt as number, o as LabelAttributionSchema, ot as unknown, p as NcRuleSchema, q as createEvent, r as DeclaredDevices, rt as object, s as MACRO_LABELS, st as EventCategory, t as BaseDevice, tt as literal, u as NcConditionDescriptorSchema, v as OpsLogEntrySchema, w as alarmPanelCapability, x as TimelapseRuleSchema, y as RetrainStatusSchema, z as readDeviceStateFrom } from "../dist-D9Bm8RyD.mjs";
2
2
  import { promises } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
@@ -6757,6 +6757,40 @@ function makeNcActionHandlers(deps) {
6757
6757
  }
6758
6758
  };
6759
6759
  }
6760
+ //#endregion
6761
+ //#region src/pipeline-analytics/data-plane-access.ts
6762
+ /**
6763
+ * Prefix literals for the export planes, mirrored here so the policy reads as
6764
+ * a table without importing three handler modules into the addon's boot path.
6765
+ * `data-plane-access.spec.ts` asserts they still match the constants their
6766
+ * handlers register with — a mirror nothing checks is a lie waiting to happen.
6767
+ */
6768
+ var TRAINING_EXPORT_PREFIX_REF = "training-export";
6769
+ var RETRAIN_EXPORT_PREFIX_REF = "retrain-export";
6770
+ var DEBUG_EXPORT_PREFIX_REF = "debug-export";
6771
+ /**
6772
+ * Every data plane this addon serves, and the level the HUB enforces before it
6773
+ * proxies. Exhaustive by test: a new plane that forgets to appear here fails
6774
+ * `data-plane-access.spec.ts`.
6775
+ */
6776
+ var PIPELINE_ANALYTICS_PLANE_ACCESS = {
6777
+ /** One stored crop/thumbnail by an unguessable id the caller already holds. */
6778
+ "event-media": "authenticated",
6779
+ /** Self-authenticating single-use action token (D47). */
6780
+ "nc-action": "public",
6781
+ /** Self-authenticating single-use artifact token (D47). */
6782
+ "nc-artifact": "public",
6783
+ /** Every frame behind a marked selection — a corpus, not an item. */
6784
+ [TRAINING_EXPORT_PREFIX_REF]: "admin",
6785
+ [RETRAIN_EXPORT_PREFIX_REF]: "admin",
6786
+ /** Diagnostic bundle: logs, config and frames. */
6787
+ [DEBUG_EXPORT_PREFIX_REF]: "admin"
6788
+ };
6789
+ /** The declared level for a plane prefix. Typed, so a prefix with no declared
6790
+ * policy is a compile error rather than a silent default. */
6791
+ function planeAccess(prefix) {
6792
+ return PIPELINE_ANALYTICS_PLANE_ACCESS[prefix];
6793
+ }
6760
6794
  var MAX_EXPORT_BYTES = 256 * 1024 * 1024;
6761
6795
  var DebugFindingSchema = object({
6762
6796
  code: string(),
@@ -7115,40 +7149,6 @@ function parseDebugDeviceIds(url) {
7115
7149
  return out;
7116
7150
  }
7117
7151
  //#endregion
7118
- //#region src/pipeline-analytics/data-plane-access.ts
7119
- /**
7120
- * Prefix literals for the export planes, mirrored here so the policy reads as
7121
- * a table without importing three handler modules into the addon's boot path.
7122
- * `data-plane-access.spec.ts` asserts they still match the constants their
7123
- * handlers register with — a mirror nothing checks is a lie waiting to happen.
7124
- */
7125
- var TRAINING_EXPORT_PREFIX_REF = "training-export";
7126
- var RETRAIN_EXPORT_PREFIX_REF = "retrain-export";
7127
- var DEBUG_EXPORT_PREFIX_REF = "debug-export";
7128
- /**
7129
- * Every data plane this addon serves, and the level the HUB enforces before it
7130
- * proxies. Exhaustive by test: a new plane that forgets to appear here fails
7131
- * `data-plane-access.spec.ts`.
7132
- */
7133
- var PIPELINE_ANALYTICS_PLANE_ACCESS = {
7134
- /** One stored crop/thumbnail by an unguessable id the caller already holds. */
7135
- "event-media": "authenticated",
7136
- /** Self-authenticating single-use action token (D47). */
7137
- "nc-action": "public",
7138
- /** Self-authenticating single-use artifact token (D47). */
7139
- "nc-artifact": "public",
7140
- /** Every frame behind a marked selection — a corpus, not an item. */
7141
- [TRAINING_EXPORT_PREFIX_REF]: "admin",
7142
- [RETRAIN_EXPORT_PREFIX_REF]: "admin",
7143
- /** Diagnostic bundle: logs, config and frames. */
7144
- [DEBUG_EXPORT_PREFIX_REF]: "admin"
7145
- };
7146
- /** The declared level for a plane prefix. Typed, so a prefix with no declared
7147
- * policy is a compile error rather than a silent default. */
7148
- function planeAccess(prefix) {
7149
- return PIPELINE_ANALYTICS_PLANE_ACCESS[prefix];
7150
- }
7151
- //#endregion
7152
7152
  //#region src/pipeline-analytics/debug/debug-export-handler.ts
7153
7153
  /**
7154
7154
  * Pacing. The export is a BACKGROUND job sharing a thread with live decode, so
@@ -7675,6 +7675,25 @@ var embeddingActions = defineCustomActions({
7675
7675
  "embedding.wipe": customAction(NoInputSchema, WipeResultSchema, { auth: "admin" })
7676
7676
  });
7677
7677
  //#endregion
7678
+ //#region src/pipeline-analytics/map-with-concurrency.ts
7679
+ /**
7680
+ * Apply `mapper` to every item with at most `limit` in flight at once.
7681
+ *
7682
+ * A rejecting mapper rejects the whole call (same contract as `Promise.all`);
7683
+ * callers that must not fail a page for one bad row catch inside the mapper.
7684
+ */
7685
+ async function mapWithConcurrency(items, limit, mapper) {
7686
+ const results = new Array(items.length);
7687
+ if (items.length === 0) return results;
7688
+ const effectiveLimit = Math.max(1, Math.min(Math.trunc(limit), items.length));
7689
+ const entries = items.entries();
7690
+ const worker = async () => {
7691
+ for (const [index, item] of entries) results[index] = await mapper(item, index);
7692
+ };
7693
+ await Promise.all(Array.from({ length: effectiveLimit }, () => worker()));
7694
+ return results;
7695
+ }
7696
+ //#endregion
7678
7697
  //#region src/pipeline-analytics/pipeline/key-event-scoring.ts
7679
7698
  /**
7680
7699
  * key-event-scoring — the SINGLE deterministic "importance" scorer for a
@@ -7946,6 +7965,16 @@ function clusterByEmbedding(items, threshold = .5) {
7946
7965
  var OPERATOR_FACE_STEP_ID = "operator:face-gallery";
7947
7966
  /** Embedding model id used when enrolling face crops as identity samples. */
7948
7967
  var MODEL_ID = "arcface-r100";
7968
+ /**
7969
+ * How many face crops `listRecentFaces` resolves at once.
7970
+ *
7971
+ * Each crop costs a store round-trip plus a disk read, so the page is I/O-bound
7972
+ * and the serial form dominated the operator's assign latency. 16 recovers
7973
+ * nearly all of the win while leaving the UDS channel — shared with the live
7974
+ * detection path — far from saturated; unbounded would trade a slow page for a
7975
+ * stalled pipeline.
7976
+ */
7977
+ var FACE_CROP_FETCH_CONCURRENCY = 16;
7949
7978
  var FaceGalleryProvider = class {
7950
7979
  identityStore;
7951
7980
  faceStore;
@@ -7954,6 +7983,7 @@ var FaceGalleryProvider = class {
7954
7983
  eventStore;
7955
7984
  logger;
7956
7985
  refreshGallery;
7986
+ eventMediaBaseUrl;
7957
7987
  emitFaceGalleryChanged;
7958
7988
  constructor(deps) {
7959
7989
  this.identityStore = deps.identityStore;
@@ -7963,6 +7993,7 @@ var FaceGalleryProvider = class {
7963
7993
  this.eventStore = deps.eventStore;
7964
7994
  this.logger = deps.logger;
7965
7995
  this.refreshGallery = deps.refreshGallery;
7996
+ this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
7966
7997
  this.emitFaceGalleryChanged = deps.emitFaceGalleryChanged;
7967
7998
  }
7968
7999
  /** Best-effort emit — never allowed to fail the mutation it follows. */
@@ -8054,6 +8085,21 @@ var FaceGalleryProvider = class {
8054
8085
  const media = await this.mediaStore.listByOwner("face", face.id);
8055
8086
  return media.length > 0 ? media[0].base64 : void 0;
8056
8087
  }
8088
+ /**
8089
+ * The crop as a data-plane URL, or undefined when there is no stored key or
8090
+ * no plane to serve it.
8091
+ *
8092
+ * The `event-media` plane resolves a raw MediaStore key (any id containing
8093
+ * `:` goes to `getByKey`), so a face crop needs NO new plane and no new
8094
+ * access decision — the same `authenticated` level already serves the track
8095
+ * thumbnails the same operator is looking at.
8096
+ */
8097
+ cropUrlFor(face) {
8098
+ if (face.mediaKey === void 0 || face.mediaKey === "") return void 0;
8099
+ const base = this.eventMediaBaseUrl();
8100
+ if (base === null) return void 0;
8101
+ return `${base}/${encodeURIComponent(face.mediaKey)}`;
8102
+ }
8057
8103
  async listRecentFaces(input) {
8058
8104
  const faces = await this.faceStore.listAllRecentFaces({
8059
8105
  limit: input?.limit,
@@ -8061,11 +8107,12 @@ var FaceGalleryProvider = class {
8061
8107
  });
8062
8108
  const identities = await this.identityStore.listIdentities();
8063
8109
  const nameMap = new Map(identities.map((i) => [i.id, i.name]));
8064
- const result = [];
8065
- for (const face of faces) {
8066
- const base64 = await this.faceCropBase64(face);
8110
+ const includeCrops = input?.includeCrops ?? true;
8111
+ return mapWithConcurrency(faces, FACE_CROP_FETCH_CONCURRENCY, async (face) => {
8112
+ const base64 = includeCrops ? await this.faceCropBase64(face) : void 0;
8113
+ const cropUrl = this.cropUrlFor(face);
8067
8114
  const identityName = face.recognizedIdentityId ? nameMap.get(face.recognizedIdentityId) : void 0;
8068
- result.push({
8115
+ return {
8069
8116
  faceId: face.id,
8070
8117
  deviceId: face.deviceId,
8071
8118
  trackId: face.trackId,
@@ -8074,15 +8121,15 @@ var FaceGalleryProvider = class {
8074
8121
  ...identityName !== void 0 ? { identityName } : {},
8075
8122
  assigned: face.assigned,
8076
8123
  ...base64 !== void 0 ? { base64 } : {},
8124
+ ...cropUrl !== void 0 ? { cropUrl } : {},
8077
8125
  ...face.faceBbox !== void 0 ? { faceBbox: face.faceBbox } : {},
8078
8126
  ...face.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: face.keyFrameMediaKey } : {},
8079
8127
  ...face.bestMatchScore != null ? { bestMatchScore: face.bestMatchScore } : {},
8080
8128
  ...face.nativeFaceShortSidePx != null ? { nativeFaceShortSidePx: face.nativeFaceShortSidePx } : {},
8081
8129
  ...face.suggestedIdentityId != null ? { suggestedIdentityId: face.suggestedIdentityId } : {},
8082
8130
  ...face.suggestedMatchScore != null ? { suggestedMatchScore: face.suggestedMatchScore } : {}
8083
- });
8084
- }
8085
- return result;
8131
+ };
8132
+ });
8086
8133
  }
8087
8134
  /**
8088
8135
  * Get a face by deviceId + trackId. Returns null if not found or if
@@ -12391,23 +12438,73 @@ async function makeSquareThumb(bytes, size) {
12391
12438
  }).jpeg({ quality: THUMB_QUALITY }).toBuffer();
12392
12439
  }
12393
12440
  //#endregion
12441
+ //#region src/pipeline-analytics/zone-name-stamp.ts
12442
+ /**
12443
+ * Copy each track with `zoneNames` stamped on.
12444
+ *
12445
+ * Three silences, all deliberate, all of them "say nothing" rather than "say
12446
+ * something wrong":
12447
+ *
12448
+ * - a track that visited no zone gets NO field (an empty array on every row of
12449
+ * a 1000-row page is pure wire cost for no information);
12450
+ * - a zone id with no entry in the catalogue — a zone deleted since the track
12451
+ * was written — is DROPPED. `zoneNames` is therefore not positionally
12452
+ * aligned with `zonesVisited` and can be shorter;
12453
+ * - a device whose catalogue could not be read at all leaves the track exactly
12454
+ * as it arrived. A zones cap that is momentarily down must not look like a
12455
+ * track that was never in a zone.
12456
+ */
12457
+ function stampZoneNames(tracks, lookup) {
12458
+ return tracks.map((track) => {
12459
+ if (track.zonesVisited.length === 0) return track;
12460
+ const catalogue = lookup(track.deviceId);
12461
+ if (catalogue === null) return track;
12462
+ const names = [];
12463
+ for (const id of track.zonesVisited) {
12464
+ const name = catalogue.get(id);
12465
+ if (name !== void 0 && name.length > 0 && !names.includes(name)) names.push(name);
12466
+ }
12467
+ if (names.length === 0) return track;
12468
+ return {
12469
+ ...track,
12470
+ zoneNames: names
12471
+ };
12472
+ });
12473
+ }
12474
+ //#endregion
12394
12475
  //#region src/pipeline-analytics/analytics-query-facade.ts
12395
12476
  var AnalyticsQueryFacade = class {
12396
12477
  deps;
12397
12478
  constructor(deps) {
12398
12479
  this.deps = deps;
12399
12480
  }
12481
+ /**
12482
+ * Stamp the human `zoneNames` onto a batch of tracks.
12483
+ *
12484
+ * ONE place, on the read path, for every track surface — `zonesVisited` is
12485
+ * ids and a client cannot resolve them without a per-camera fan-out. A hub
12486
+ * with no catalogue reader configured returns the tracks untouched.
12487
+ */
12488
+ async withZoneNames(tracks) {
12489
+ const cache = this.deps.zoneCatalogue?.() ?? null;
12490
+ if (cache === null || tracks.length === 0) return tracks;
12491
+ const zoned = tracks.filter((t) => t.zonesVisited.length > 0);
12492
+ if (zoned.length === 0) return tracks;
12493
+ await cache.ensure(zoned.map((t) => t.deviceId));
12494
+ return stampZoneNames(tracks, (deviceId) => cache.lookup(deviceId));
12495
+ }
12400
12496
  async getActiveTracks(input) {
12401
- return this.deps.trackStore()?.getActive(input.deviceId) ?? [];
12497
+ return this.withZoneNames(this.deps.trackStore()?.getActive(input.deviceId) ?? []);
12402
12498
  }
12403
12499
  async getTrack(input) {
12404
12500
  const store = this.deps.trackStore();
12405
- const active = store?.getActiveByTrack(input.trackId);
12406
- if (active) return active;
12407
- return await store?.getPersistedByTrackId(input.trackId) ?? null;
12501
+ const track = store?.getActiveByTrack(input.trackId) ?? await store?.getPersistedByTrackId(input.trackId) ?? null;
12502
+ if (track === null) return null;
12503
+ const [named] = await this.withZoneNames([track]);
12504
+ return named ?? track;
12408
12505
  }
12409
12506
  async listTracks(input) {
12410
- return this.deps.trackStore()?.queryHistorical(input) ?? [];
12507
+ return this.withZoneNames(await this.deps.trackStore()?.queryHistorical(input) ?? []);
12411
12508
  }
12412
12509
  /**
12413
12510
  * Batched cluster-wide track listing — one merged (`lastSeen` DESC,
@@ -12417,10 +12514,14 @@ var AnalyticsQueryFacade = class {
12417
12514
  * indexed page + k-way merge and the cursor encoding.
12418
12515
  */
12419
12516
  async listRecentTracks(input) {
12420
- return this.deps.trackStore()?.queryRecent(input) ?? {
12517
+ const page = await this.deps.trackStore()?.queryRecent(input) ?? {
12421
12518
  tracks: [],
12422
12519
  nextCursor: null
12423
12520
  };
12521
+ return {
12522
+ ...page,
12523
+ tracks: await this.withZoneNames(page.tracks)
12524
+ };
12424
12525
  }
12425
12526
  /** Per-camera sensor-event history (state changes of linked devices). */
12426
12527
  async getSensorEvents(input) {
@@ -17092,6 +17193,72 @@ var RetrainService = class {
17092
17193
  }
17093
17194
  };
17094
17195
  //#endregion
17196
+ //#region src/pipeline-analytics/zone-catalogue-cache.ts
17197
+ /**
17198
+ * zone-catalogue-cache — the per-device zone id→name dictionary the track read
17199
+ * path stamps `zoneNames` from.
17200
+ *
17201
+ * The `zones` capability is per-device, so resolving names on a merged
17202
+ * `listRecentTracks` page would otherwise be one cap call per camera per page —
17203
+ * the exact fan-out that surface exists to remove. One read per camera per TTL
17204
+ * is the whole cost, against a catalogue that changes only when an operator
17205
+ * draws a polygon.
17206
+ *
17207
+ * The failure rule is the important part: a refresh that throws KEEPS the last
17208
+ * known catalogue. Dropping it would make every zoned track look unzoned for
17209
+ * the next minute, which reads as "zones stopped working" rather than "the
17210
+ * zones cap blipped".
17211
+ */
17212
+ /** How long a device's zone catalogue is trusted before a re-read. A zone
17213
+ * rename should reach the feed without a restart; a minute is invisible to an
17214
+ * operator and reduces the read to ~16 cheap calls/min on this fleet. */
17215
+ var ZONE_CATALOGUE_TTL_MS = 6e4;
17216
+ var ZoneCatalogueCache = class {
17217
+ deps;
17218
+ entries = /* @__PURE__ */ new Map();
17219
+ /** In-flight reads, so a burst of pages for the same camera makes one call. */
17220
+ inFlight = /* @__PURE__ */ new Map();
17221
+ constructor(deps) {
17222
+ this.deps = deps;
17223
+ }
17224
+ /** Make sure every device has a catalogue no older than the TTL. */
17225
+ async ensure(deviceIds) {
17226
+ const stale = [...new Set(deviceIds)].filter((id) => this.isStale(id));
17227
+ await Promise.all(stale.map((id) => this.refresh(id)));
17228
+ }
17229
+ /** The device's zone id→name map, or null when it was never readable. */
17230
+ lookup(deviceId) {
17231
+ return this.entries.get(deviceId)?.names ?? null;
17232
+ }
17233
+ isStale(deviceId) {
17234
+ const held = this.entries.get(deviceId);
17235
+ if (held === void 0) return true;
17236
+ return this.deps.now() - held.readAtMs >= ZONE_CATALOGUE_TTL_MS;
17237
+ }
17238
+ async refresh(deviceId) {
17239
+ const running = this.inFlight.get(deviceId);
17240
+ if (running !== void 0) return running;
17241
+ const task = this.read(deviceId).finally(() => {
17242
+ this.inFlight.delete(deviceId);
17243
+ });
17244
+ this.inFlight.set(deviceId, task);
17245
+ return task;
17246
+ }
17247
+ async read(deviceId) {
17248
+ try {
17249
+ const zones = await this.deps.fetchZones(deviceId);
17250
+ const names = /* @__PURE__ */ new Map();
17251
+ for (const zone of zones) if (zone.id.length > 0 && zone.name.length > 0) names.set(zone.id, zone.name);
17252
+ this.entries.set(deviceId, {
17253
+ names,
17254
+ readAtMs: this.deps.now()
17255
+ });
17256
+ } catch (error) {
17257
+ this.deps.onError(deviceId, error);
17258
+ }
17259
+ }
17260
+ };
17261
+ //#endregion
17095
17262
  //#region src/pipeline-analytics/media-capture-log.ts
17096
17263
  var DEFAULT_FLUSH_INTERVAL_MS$1 = 6e4;
17097
17264
  var MediaCaptureLogAggregator = class {
@@ -20110,6 +20277,21 @@ var PlateRecognizer = class {
20110
20277
  }
20111
20278
  };
20112
20279
  //#endregion
20280
+ //#region src/pipeline-analytics/read-slice.ts
20281
+ /**
20282
+ * The loaded slice, or `undefined` when it has never loaded and the one
20283
+ * permitted refresh also failed to produce it.
20284
+ *
20285
+ * A failing refresh is swallowed deliberately: every caller degrades to "no
20286
+ * zones / no rules", and a zone read must never take down the frame that asked.
20287
+ */
20288
+ async function readSliceOnce(handle) {
20289
+ const loaded = handle.value;
20290
+ if (loaded !== void 0) return loaded;
20291
+ await handle.refresh().catch(() => void 0);
20292
+ return handle.value;
20293
+ }
20294
+ //#endregion
20113
20295
  //#region src/pipeline-analytics/rebuild-detail-request.ts
20114
20296
  /** The one step a rebuild runs. */
20115
20297
  var REBUILD_DETAIL_STEP_ID = "clip-embedding";
@@ -30930,6 +31112,27 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
30930
31112
  * bodies live in `analytics-query-facade.ts`; the class keeps one-line
30931
31113
  * delegates because the pipeline-analytics cap registers `this` as its
30932
31114
  * provider. Deps are lazy getters — the stores are late-bound in boot. */
31115
+ /**
31116
+ * Zone id→name dictionary for the track read path.
31117
+ *
31118
+ * `zonesVisited` persists ids; every operator-facing surface needs the name.
31119
+ * Cached per device (see `ZoneCatalogueCache`) so a merged 1000-row page
31120
+ * costs at most one `listZones` per camera per minute rather than one per
31121
+ * page.
31122
+ */
31123
+ zoneCatalogue = new ZoneCatalogueCache({
31124
+ fetchZones: (deviceId) => this.ctx.api.zones.listZones.query({ deviceId }),
31125
+ now: () => Date.now(),
31126
+ onError: (deviceId, error) => {
31127
+ this.ctx.logger.warn("zone catalogue read failed — zone names unresolved for this device", {
31128
+ tags: { deviceId },
31129
+ meta: {
31130
+ deviceId,
31131
+ error: errMsg(error)
31132
+ }
31133
+ });
31134
+ }
31135
+ });
30933
31136
  queryFacade = new AnalyticsQueryFacade({
30934
31137
  trackStore: () => this.trackStore,
30935
31138
  eventStore: () => this.eventStore,
@@ -30937,7 +31140,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
30937
31140
  sensorEventStore: () => this.sensorEventStore,
30938
31141
  opsLog: () => this.eventsOpsLog,
30939
31142
  eventMediaBaseUrl: () => this.eventMediaBaseUrl,
30940
- logger: () => this.ctx.logger
31143
+ logger: () => this.ctx.logger,
31144
+ zoneCatalogue: () => this.zoneCatalogue
30941
31145
  });
30942
31146
  /** The shared crop extractor (native-res first, detection-frame fallback),
30943
31147
  * captured in the constructor so `processFrame` can crop object thumbnails in
@@ -32302,6 +32506,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
32302
32506
  eventStore: stores.eventStore,
32303
32507
  logger: this.ctx.logger,
32304
32508
  refreshGallery: () => this.faceRecognizer?.refreshGallery(),
32509
+ eventMediaBaseUrl: () => this.eventMediaBaseUrl,
32305
32510
  emitFaceGalleryChanged: (payload) => this.emitFaceGalleryChanged(payload)
32306
32511
  });
32307
32512
  const plateGallery = new PlateGalleryProvider({
@@ -33313,10 +33518,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
33313
33518
  async resolveDevicePackageRules(deviceId) {
33314
33519
  const proxy = await this.ensureProxy(deviceId);
33315
33520
  if (!proxy) return [];
33316
- const cached = proxy.state.zoneRules.value?.package;
33317
- if (cached && cached.length > 0) return cached;
33318
- await proxy.state.zoneRules.refresh().catch(() => void 0);
33319
- return proxy.state.zoneRules.value?.package ?? [];
33521
+ return (await readSliceOnce(proxy.state.zoneRules))?.package ?? [];
33320
33522
  }
33321
33523
  /**
33322
33524
  * Route one track's `runDetailSubtree` results (two-plane detail dispatch)
@@ -34624,10 +34826,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
34624
34826
  async resolveDeviceZones(deviceId) {
34625
34827
  const proxy = await this.ensureProxy(deviceId);
34626
34828
  if (!proxy) return [];
34627
- const cached = proxy.state.zones.value?.zones;
34628
- if (cached && cached.length > 0) return cached;
34629
- await proxy.state.zones.refresh().catch(() => void 0);
34630
- return proxy.state.zones.value?.zones ?? [];
34829
+ return (await readSliceOnce(proxy.state.zones))?.zones ?? [];
34631
34830
  }
34632
34831
  releaseProxy(deviceId) {
34633
34832
  const unsubs = this.proxyUnsubs.get(deviceId);
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-tCAuhv5m.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BuCI6peK.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.53",
3
+ "version": "1.2.55",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",