@camstack/addon-post-analysis 1.2.54 → 1.2.56

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,7 +1,11 @@
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-BVOgkikr.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-CAC3R7XW.mjs";
2
+ import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
+ import * as fs from "node:fs";
2
4
  import { promises } from "node:fs";
5
+ import * as path$1 from "node:path";
3
6
  import path from "node:path";
4
7
  import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
8
+ import "node:child_process";
5
9
  import sharp from "sharp";
6
10
  //#region src/notification-center/action-token.ts
7
11
  /**
@@ -721,6 +725,227 @@ var NC_ALARM_DECLARATION = {
721
725
  DeviceClass: NcAlarmPanelDevice
722
726
  };
723
727
  //#endregion
728
+ //#region ../types/dist/node.mjs
729
+ var node_exports = /* @__PURE__ */ __exportAll({
730
+ FilesystemStorageProvider: () => FilesystemStorageProvider,
731
+ PYTHON_VERSION: () => PYTHON_VERSION,
732
+ signExpiringUrl: () => signExpiringUrl,
733
+ verifyExpiringUrl: () => verifyExpiringUrl
734
+ });
735
+ /**
736
+ * Download portable Python (headless) from bjia56/portable-python.
737
+ *
738
+ * Source: https://github.com/bjia56/portable-python
739
+ *
740
+ * Advantages over python-build-standalone:
741
+ * - macOS universal2 binary (arm64+x86_64 in one)
742
+ * - Lighter (~24-28MB headless zip)
743
+ * - glibc 2.17 minimum (better compat)
744
+ * - Headless variant excludes tkinter/UI (ideal for ML inference)
745
+ *
746
+ * The zip's top-level dir is platform-specific (e.g.
747
+ * `python-headless-3.12.12-darwin-universal2/`); after extraction
748
+ * we rename it to a stable `python/` so consumers can rely on
749
+ * `<dataDir>/deps/python/bin/python3`.
750
+ */
751
+ var PYTHON_VERSION = "3.12.12";
752
+ `${PYTHON_VERSION}`;
753
+ var STORAGE_LOCATION_TYPES = [
754
+ "data",
755
+ "media",
756
+ "recordings",
757
+ "recordings-high",
758
+ "recordings-low",
759
+ "recordings-clips",
760
+ "event-images",
761
+ "models",
762
+ "addons-data",
763
+ "cache",
764
+ "logs",
765
+ "backups"
766
+ ];
767
+ var DEFAULT_LOCATION_SUBDIRS = {
768
+ data: "db",
769
+ media: "media",
770
+ recordings: "recordings",
771
+ "recordings-high": "recordings-high",
772
+ "recordings-low": "recordings-low",
773
+ "recordings-clips": "recordings-clips",
774
+ "event-images": "event-images",
775
+ models: "models",
776
+ "addons-data": "addons-data",
777
+ cache: "/tmp/camstack-cache",
778
+ logs: "logs",
779
+ backups: "backups"
780
+ };
781
+ /**
782
+ * Filesystem storage provider — serves all location types from a local directory tree.
783
+ *
784
+ * Default layout:
785
+ * {rootPath}/recordings-high/
786
+ * {rootPath}/recordings-low/
787
+ * {rootPath}/recordings-clips/
788
+ * {rootPath}/event-images/
789
+ * {rootPath}/models/
790
+ * {rootPath}/addons-data/
791
+ * {rootPath}/logs/
792
+ * /tmp/camstack-cache/ (cache is always local)
793
+ *
794
+ * Individual location paths can be overridden.
795
+ */
796
+ var FilesystemStorageProvider = class {
797
+ id = "local";
798
+ name = "Local Filesystem";
799
+ supportedLocations = [...STORAGE_LOCATION_TYPES];
800
+ rootPath;
801
+ locationPaths;
802
+ constructor(rootPath, overrides) {
803
+ this.rootPath = path$1.resolve(rootPath);
804
+ this.locationPaths = /* @__PURE__ */ new Map();
805
+ for (const loc of STORAGE_LOCATION_TYPES) {
806
+ const override = overrides?.[loc];
807
+ if (override) this.locationPaths.set(loc, path$1.resolve(override));
808
+ else {
809
+ const subdir = DEFAULT_LOCATION_SUBDIRS[loc] ?? loc;
810
+ this.locationPaths.set(loc, path$1.isAbsolute(subdir) ? subdir : path$1.join(this.rootPath, subdir));
811
+ }
812
+ }
813
+ for (const [loc, override] of Object.entries(overrides ?? {})) if (typeof override === "string" && !this.locationPaths.has(loc)) this.locationPaths.set(loc, path$1.resolve(override));
814
+ }
815
+ async resolve({ location, relativePath }) {
816
+ const base = this.locationPaths.get(location) ?? path$1.join(this.rootPath, location);
817
+ return path$1.join(base, relativePath);
818
+ }
819
+ async write({ location, relativePath, data }) {
820
+ const filePath = await this.resolve({
821
+ location,
822
+ relativePath
823
+ });
824
+ await fs.promises.mkdir(path$1.dirname(filePath), { recursive: true });
825
+ if (Buffer.isBuffer(data)) await fs.promises.writeFile(filePath, data);
826
+ else {
827
+ const writeStream = fs.createWriteStream(filePath);
828
+ await new Promise((resolve, reject) => {
829
+ data.pipe(writeStream);
830
+ writeStream.on("finish", resolve);
831
+ writeStream.on("error", reject);
832
+ });
833
+ }
834
+ }
835
+ async read({ location, relativePath }) {
836
+ return fs.promises.readFile(await this.resolve({
837
+ location,
838
+ relativePath
839
+ }));
840
+ }
841
+ async exists({ location, relativePath }) {
842
+ try {
843
+ await fs.promises.access(await this.resolve({
844
+ location,
845
+ relativePath
846
+ }));
847
+ return true;
848
+ } catch {
849
+ return false;
850
+ }
851
+ }
852
+ async list({ location, prefix }) {
853
+ const base = this.locationPaths.get(location);
854
+ if (!base) return [];
855
+ const dir = prefix ? path$1.join(base, prefix) : base;
856
+ try {
857
+ return (await fs.promises.readdir(dir, { withFileTypes: true })).map((e) => prefix ? `${prefix}/${e.name}` : e.name);
858
+ } catch {
859
+ return [];
860
+ }
861
+ }
862
+ async delete({ location, relativePath }) {
863
+ const filePath = await this.resolve({
864
+ location,
865
+ relativePath
866
+ });
867
+ await fs.promises.rm(filePath, { force: true });
868
+ }
869
+ async getAvailableSpace({ location }) {
870
+ const base = this.locationPaths.get(location);
871
+ if (!base) return null;
872
+ try {
873
+ let target = base;
874
+ while (!fs.existsSync(target)) {
875
+ const parent = path$1.dirname(target);
876
+ if (!parent || parent === target) return null;
877
+ target = parent;
878
+ }
879
+ const stats = await fs.promises.statfs(target);
880
+ return stats.bavail * stats.bsize;
881
+ } catch {
882
+ return null;
883
+ }
884
+ }
885
+ /** Get the resolved path for a location type (addon-declared ids fall back
886
+ * to `<rootPath>/<id>`). */
887
+ getLocationPath(location) {
888
+ return this.locationPaths.get(location) ?? path$1.join(this.rootPath, location);
889
+ }
890
+ /** Get the root path */
891
+ getRootPath() {
892
+ return this.rootPath;
893
+ }
894
+ };
895
+ /**
896
+ * The ONE derivation of "a signed, expiring URL".
897
+ *
898
+ * This repo mints unguessable, self-expiring links in three places now — the
899
+ * notification artifact plane, the Home Assistant media plane, and the snapshot
900
+ * link plane. The first two grew independently and are byte-identical logic
901
+ * (`hmac(secret, "<id>:<exp>")`, expiry checked before a constant-time compare),
902
+ * each restating the crypto locally because **addons never import each other**.
903
+ *
904
+ * That reason is real, and the conclusion drawn from it was wrong. Two copies of
905
+ * a signing scheme is how one of them quietly ends up with a different TTL, a
906
+ * different compare, or a missing expiry check, and nothing fails until a link
907
+ * that should have died keeps working. The fix is the same one D52 applies to
908
+ * crop geometry: one derivation, in a place every addon may depend on. A
909
+ * framework package is exactly that place — `@camstack/types/node`, off the root
910
+ * entry because `node:crypto` must never be traversed by a browser bundler.
911
+ *
912
+ * What this module deliberately does NOT decide: the TTL, the base URL, the
913
+ * shape of `id`, and the access level of the route. Those are per-plane policy
914
+ * and each caller states them where a reader can see them.
915
+ */
916
+ /**
917
+ * The signature over `(id, expMs)`.
918
+ *
919
+ * `id` is whatever the plane uses to name the thing being served — an artifact
920
+ * id, a track id, a `<deviceId>:<width>` pair. It is joined with `:` so a caller
921
+ * must not put a `:` inside a field whose boundary matters; where a plane has
922
+ * more than one field, it composes them itself and owns that ambiguity.
923
+ */
924
+ function signExpiringUrl(secret, id, expMs) {
925
+ return createHmac("sha256", secret).update(`${id}:${String(expMs)}`).digest("hex");
926
+ }
927
+ /**
928
+ * Verify a request's `(id, exp, sig)`.
929
+ *
930
+ * **Expiry is checked BEFORE the compare**, so an expired link is refused
931
+ * whether or not its signature is valid — a leaked URL stops working on its own
932
+ * and cannot be kept alive by holding a correct signature. The compare itself is
933
+ * constant-time so a public route cannot be probed for the signature byte by
934
+ * byte.
935
+ */
936
+ function verifyExpiringUrl(input) {
937
+ const { secret, id, exp, sig, nowMs } = input;
938
+ if (exp === void 0 || sig === void 0) return false;
939
+ const expMs = typeof exp === "number" ? exp : Number(exp);
940
+ if (!Number.isFinite(expMs)) return false;
941
+ if (expMs <= nowMs) return false;
942
+ const expected = signExpiringUrl(secret, id, expMs);
943
+ const a = Buffer.from(expected, "utf8");
944
+ const b = Buffer.from(sig, "utf8");
945
+ if (a.length !== b.length) return false;
946
+ return timingSafeEqual(a, b);
947
+ }
948
+ //#endregion
724
949
  //#region src/notification-center/artifact-url.ts
725
950
  /**
726
951
  * Signed, externally-reachable URLs for notification artifacts.
@@ -737,7 +962,10 @@ var NC_ALARM_DECLARATION = {
737
962
  * recipient's phone) — it holds no session and no admin token. The route is
738
963
  * therefore `access:'public'` and the authority is the signature: an
739
964
  * unguessable HMAC over `(id, exp)` with a per-install secret, expiring on
740
- * its own. Nothing else about the install is reachable through it.
965
+ * its own. Nothing else about the install is reachable through it. The HMAC
966
+ * is the shared one in `@camstack/types/node` — this module used to restate
967
+ * it, as did the Home Assistant media plane, and two copies of a signing
968
+ * scheme is how one of them quietly loses its expiry check.
741
969
  * - **The base URL is CHOSEN, never assumed.** A notification carrying
742
970
  * `http://127.0.0.1:...` is useless (the Alexa `hubUrl` bug, again). The
743
971
  * ranked endpoint list the `local-network` cap already computes — public
@@ -754,9 +982,17 @@ var NC_ALARM_DECLARATION = {
754
982
  function pickArtifactBaseUrl(endpoints) {
755
983
  return endpoints.filter((e) => e.kind !== "loopback").toSorted((a, b) => a.priority - b.priority)[0]?.baseUrl ?? null;
756
984
  }
757
- /** Sign `(id, exp)` — the exact string the verifier recomputes. */
985
+ /**
986
+ * Sign `(id, exp)` — the exact string the verifier recomputes.
987
+ *
988
+ * Delegates to the one derivation in `@camstack/types/node`. This used to
989
+ * restate the HMAC locally, and so did the Home Assistant media plane, on the
990
+ * correct reasoning that addons never import each other — but two copies of a
991
+ * signing scheme is how one of them ends up with a different expiry check that
992
+ * nothing catches.
993
+ */
758
994
  function signArtifact(secret, id, expMs) {
759
- return createHmac("sha256", secret).update(`${id}:${expMs}`).digest("hex");
995
+ return signExpiringUrl(secret, id, expMs);
760
996
  }
761
997
  /**
762
998
  * Build the fully-qualified, signed URL for an artifact.
@@ -773,12 +1009,7 @@ function buildArtifactUrl(input) {
773
1009
  * the compare so an expired link is rejected even with a valid signature.
774
1010
  */
775
1011
  function verifyArtifactSignature(input) {
776
- if (!Number.isFinite(input.exp) || input.exp <= input.nowMs) return false;
777
- const expected = signArtifact(input.secret, input.id, input.exp);
778
- const a = Buffer.from(expected, "utf8");
779
- const b = Buffer.from(input.sig, "utf8");
780
- if (a.length !== b.length) return false;
781
- return timingSafeEqual(a, b);
1012
+ return verifyExpiringUrl(input);
782
1013
  }
783
1014
  //#endregion
784
1015
  //#region src/notification-center/artifact-plane.ts
@@ -12438,23 +12669,73 @@ async function makeSquareThumb(bytes, size) {
12438
12669
  }).jpeg({ quality: THUMB_QUALITY }).toBuffer();
12439
12670
  }
12440
12671
  //#endregion
12672
+ //#region src/pipeline-analytics/zone-name-stamp.ts
12673
+ /**
12674
+ * Copy each track with `zoneNames` stamped on.
12675
+ *
12676
+ * Three silences, all deliberate, all of them "say nothing" rather than "say
12677
+ * something wrong":
12678
+ *
12679
+ * - a track that visited no zone gets NO field (an empty array on every row of
12680
+ * a 1000-row page is pure wire cost for no information);
12681
+ * - a zone id with no entry in the catalogue — a zone deleted since the track
12682
+ * was written — is DROPPED. `zoneNames` is therefore not positionally
12683
+ * aligned with `zonesVisited` and can be shorter;
12684
+ * - a device whose catalogue could not be read at all leaves the track exactly
12685
+ * as it arrived. A zones cap that is momentarily down must not look like a
12686
+ * track that was never in a zone.
12687
+ */
12688
+ function stampZoneNames(tracks, lookup) {
12689
+ return tracks.map((track) => {
12690
+ if (track.zonesVisited.length === 0) return track;
12691
+ const catalogue = lookup(track.deviceId);
12692
+ if (catalogue === null) return track;
12693
+ const names = [];
12694
+ for (const id of track.zonesVisited) {
12695
+ const name = catalogue.get(id);
12696
+ if (name !== void 0 && name.length > 0 && !names.includes(name)) names.push(name);
12697
+ }
12698
+ if (names.length === 0) return track;
12699
+ return {
12700
+ ...track,
12701
+ zoneNames: names
12702
+ };
12703
+ });
12704
+ }
12705
+ //#endregion
12441
12706
  //#region src/pipeline-analytics/analytics-query-facade.ts
12442
12707
  var AnalyticsQueryFacade = class {
12443
12708
  deps;
12444
12709
  constructor(deps) {
12445
12710
  this.deps = deps;
12446
12711
  }
12712
+ /**
12713
+ * Stamp the human `zoneNames` onto a batch of tracks.
12714
+ *
12715
+ * ONE place, on the read path, for every track surface — `zonesVisited` is
12716
+ * ids and a client cannot resolve them without a per-camera fan-out. A hub
12717
+ * with no catalogue reader configured returns the tracks untouched.
12718
+ */
12719
+ async withZoneNames(tracks) {
12720
+ const cache = this.deps.zoneCatalogue?.() ?? null;
12721
+ if (cache === null || tracks.length === 0) return tracks;
12722
+ const zoned = tracks.filter((t) => t.zonesVisited.length > 0);
12723
+ if (zoned.length === 0) return tracks;
12724
+ await cache.ensure(zoned.map((t) => t.deviceId));
12725
+ return stampZoneNames(tracks, (deviceId) => cache.lookup(deviceId));
12726
+ }
12447
12727
  async getActiveTracks(input) {
12448
- return this.deps.trackStore()?.getActive(input.deviceId) ?? [];
12728
+ return this.withZoneNames(this.deps.trackStore()?.getActive(input.deviceId) ?? []);
12449
12729
  }
12450
12730
  async getTrack(input) {
12451
12731
  const store = this.deps.trackStore();
12452
- const active = store?.getActiveByTrack(input.trackId);
12453
- if (active) return active;
12454
- return await store?.getPersistedByTrackId(input.trackId) ?? null;
12732
+ const track = store?.getActiveByTrack(input.trackId) ?? await store?.getPersistedByTrackId(input.trackId) ?? null;
12733
+ if (track === null) return null;
12734
+ const [named] = await this.withZoneNames([track]);
12735
+ return named ?? track;
12455
12736
  }
12456
12737
  async listTracks(input) {
12457
- return this.deps.trackStore()?.queryHistorical(input) ?? [];
12738
+ return this.withZoneNames(await this.deps.trackStore()?.queryHistorical(input) ?? []);
12458
12739
  }
12459
12740
  /**
12460
12741
  * Batched cluster-wide track listing — one merged (`lastSeen` DESC,
@@ -12464,10 +12745,14 @@ var AnalyticsQueryFacade = class {
12464
12745
  * indexed page + k-way merge and the cursor encoding.
12465
12746
  */
12466
12747
  async listRecentTracks(input) {
12467
- return this.deps.trackStore()?.queryRecent(input) ?? {
12748
+ const page = await this.deps.trackStore()?.queryRecent(input) ?? {
12468
12749
  tracks: [],
12469
12750
  nextCursor: null
12470
12751
  };
12752
+ return {
12753
+ ...page,
12754
+ tracks: await this.withZoneNames(page.tracks)
12755
+ };
12471
12756
  }
12472
12757
  /** Per-camera sensor-event history (state changes of linked devices). */
12473
12758
  async getSensorEvents(input) {
@@ -17139,6 +17424,72 @@ var RetrainService = class {
17139
17424
  }
17140
17425
  };
17141
17426
  //#endregion
17427
+ //#region src/pipeline-analytics/zone-catalogue-cache.ts
17428
+ /**
17429
+ * zone-catalogue-cache — the per-device zone id→name dictionary the track read
17430
+ * path stamps `zoneNames` from.
17431
+ *
17432
+ * The `zones` capability is per-device, so resolving names on a merged
17433
+ * `listRecentTracks` page would otherwise be one cap call per camera per page —
17434
+ * the exact fan-out that surface exists to remove. One read per camera per TTL
17435
+ * is the whole cost, against a catalogue that changes only when an operator
17436
+ * draws a polygon.
17437
+ *
17438
+ * The failure rule is the important part: a refresh that throws KEEPS the last
17439
+ * known catalogue. Dropping it would make every zoned track look unzoned for
17440
+ * the next minute, which reads as "zones stopped working" rather than "the
17441
+ * zones cap blipped".
17442
+ */
17443
+ /** How long a device's zone catalogue is trusted before a re-read. A zone
17444
+ * rename should reach the feed without a restart; a minute is invisible to an
17445
+ * operator and reduces the read to ~16 cheap calls/min on this fleet. */
17446
+ var ZONE_CATALOGUE_TTL_MS = 6e4;
17447
+ var ZoneCatalogueCache = class {
17448
+ deps;
17449
+ entries = /* @__PURE__ */ new Map();
17450
+ /** In-flight reads, so a burst of pages for the same camera makes one call. */
17451
+ inFlight = /* @__PURE__ */ new Map();
17452
+ constructor(deps) {
17453
+ this.deps = deps;
17454
+ }
17455
+ /** Make sure every device has a catalogue no older than the TTL. */
17456
+ async ensure(deviceIds) {
17457
+ const stale = [...new Set(deviceIds)].filter((id) => this.isStale(id));
17458
+ await Promise.all(stale.map((id) => this.refresh(id)));
17459
+ }
17460
+ /** The device's zone id→name map, or null when it was never readable. */
17461
+ lookup(deviceId) {
17462
+ return this.entries.get(deviceId)?.names ?? null;
17463
+ }
17464
+ isStale(deviceId) {
17465
+ const held = this.entries.get(deviceId);
17466
+ if (held === void 0) return true;
17467
+ return this.deps.now() - held.readAtMs >= ZONE_CATALOGUE_TTL_MS;
17468
+ }
17469
+ async refresh(deviceId) {
17470
+ const running = this.inFlight.get(deviceId);
17471
+ if (running !== void 0) return running;
17472
+ const task = this.read(deviceId).finally(() => {
17473
+ this.inFlight.delete(deviceId);
17474
+ });
17475
+ this.inFlight.set(deviceId, task);
17476
+ return task;
17477
+ }
17478
+ async read(deviceId) {
17479
+ try {
17480
+ const zones = await this.deps.fetchZones(deviceId);
17481
+ const names = /* @__PURE__ */ new Map();
17482
+ for (const zone of zones) if (zone.id.length > 0 && zone.name.length > 0) names.set(zone.id, zone.name);
17483
+ this.entries.set(deviceId, {
17484
+ names,
17485
+ readAtMs: this.deps.now()
17486
+ });
17487
+ } catch (error) {
17488
+ this.deps.onError(deviceId, error);
17489
+ }
17490
+ }
17491
+ };
17492
+ //#endregion
17142
17493
  //#region src/pipeline-analytics/media-capture-log.ts
17143
17494
  var DEFAULT_FLUSH_INTERVAL_MS$1 = 6e4;
17144
17495
  var MediaCaptureLogAggregator = class {
@@ -30992,6 +31343,27 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
30992
31343
  * bodies live in `analytics-query-facade.ts`; the class keeps one-line
30993
31344
  * delegates because the pipeline-analytics cap registers `this` as its
30994
31345
  * provider. Deps are lazy getters — the stores are late-bound in boot. */
31346
+ /**
31347
+ * Zone id→name dictionary for the track read path.
31348
+ *
31349
+ * `zonesVisited` persists ids; every operator-facing surface needs the name.
31350
+ * Cached per device (see `ZoneCatalogueCache`) so a merged 1000-row page
31351
+ * costs at most one `listZones` per camera per minute rather than one per
31352
+ * page.
31353
+ */
31354
+ zoneCatalogue = new ZoneCatalogueCache({
31355
+ fetchZones: (deviceId) => this.ctx.api.zones.listZones.query({ deviceId }),
31356
+ now: () => Date.now(),
31357
+ onError: (deviceId, error) => {
31358
+ this.ctx.logger.warn("zone catalogue read failed — zone names unresolved for this device", {
31359
+ tags: { deviceId },
31360
+ meta: {
31361
+ deviceId,
31362
+ error: errMsg(error)
31363
+ }
31364
+ });
31365
+ }
31366
+ });
30995
31367
  queryFacade = new AnalyticsQueryFacade({
30996
31368
  trackStore: () => this.trackStore,
30997
31369
  eventStore: () => this.eventStore,
@@ -30999,7 +31371,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
30999
31371
  sensorEventStore: () => this.sensorEventStore,
31000
31372
  opsLog: () => this.eventsOpsLog,
31001
31373
  eventMediaBaseUrl: () => this.eventMediaBaseUrl,
31002
- logger: () => this.ctx.logger
31374
+ logger: () => this.ctx.logger,
31375
+ zoneCatalogue: () => this.zoneCatalogue
31003
31376
  });
31004
31377
  /** The shared crop extractor (native-res first, detection-frame fallback),
31005
31378
  * captured in the constructor so `processFrame` can crop object thumbnails in
@@ -31353,7 +31726,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
31353
31726
  let storage = this.ctx.kernel.storage;
31354
31727
  const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
31355
31728
  if (mediaRoot) {
31356
- const { FilesystemStorageProvider } = await import("../node-Co0YpEFr.mjs");
31729
+ const { FilesystemStorageProvider } = await Promise.resolve().then(() => node_exports);
31357
31730
  storage = new FilesystemStorageProvider(mediaRoot, { eventMedia: mediaRoot });
31358
31731
  logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
31359
31732
  }
@@ -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-uW2bbQBi.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-ABX4r2Sy.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.54",
3
+ "version": "1.2.56",
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",
@@ -1,148 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path$1 from "node:path";
3
- import "node:crypto";
4
- import "node:child_process";
5
- var STORAGE_LOCATION_TYPES = [
6
- "data",
7
- "media",
8
- "recordings",
9
- "recordings-high",
10
- "recordings-low",
11
- "recordings-clips",
12
- "event-images",
13
- "models",
14
- "addons-data",
15
- "cache",
16
- "logs",
17
- "backups"
18
- ];
19
- var DEFAULT_LOCATION_SUBDIRS = {
20
- data: "db",
21
- media: "media",
22
- recordings: "recordings",
23
- "recordings-high": "recordings-high",
24
- "recordings-low": "recordings-low",
25
- "recordings-clips": "recordings-clips",
26
- "event-images": "event-images",
27
- models: "models",
28
- "addons-data": "addons-data",
29
- cache: "/tmp/camstack-cache",
30
- logs: "logs",
31
- backups: "backups"
32
- };
33
- /**
34
- * Filesystem storage provider — serves all location types from a local directory tree.
35
- *
36
- * Default layout:
37
- * {rootPath}/recordings-high/
38
- * {rootPath}/recordings-low/
39
- * {rootPath}/recordings-clips/
40
- * {rootPath}/event-images/
41
- * {rootPath}/models/
42
- * {rootPath}/addons-data/
43
- * {rootPath}/logs/
44
- * /tmp/camstack-cache/ (cache is always local)
45
- *
46
- * Individual location paths can be overridden.
47
- */
48
- var FilesystemStorageProvider = class {
49
- id = "local";
50
- name = "Local Filesystem";
51
- supportedLocations = [...STORAGE_LOCATION_TYPES];
52
- rootPath;
53
- locationPaths;
54
- constructor(rootPath, overrides) {
55
- this.rootPath = path$1.resolve(rootPath);
56
- this.locationPaths = /* @__PURE__ */ new Map();
57
- for (const loc of STORAGE_LOCATION_TYPES) {
58
- const override = overrides?.[loc];
59
- if (override) this.locationPaths.set(loc, path$1.resolve(override));
60
- else {
61
- const subdir = DEFAULT_LOCATION_SUBDIRS[loc] ?? loc;
62
- this.locationPaths.set(loc, path$1.isAbsolute(subdir) ? subdir : path$1.join(this.rootPath, subdir));
63
- }
64
- }
65
- for (const [loc, override] of Object.entries(overrides ?? {})) if (typeof override === "string" && !this.locationPaths.has(loc)) this.locationPaths.set(loc, path$1.resolve(override));
66
- }
67
- async resolve({ location, relativePath }) {
68
- const base = this.locationPaths.get(location) ?? path$1.join(this.rootPath, location);
69
- return path$1.join(base, relativePath);
70
- }
71
- async write({ location, relativePath, data }) {
72
- const filePath = await this.resolve({
73
- location,
74
- relativePath
75
- });
76
- await fs.promises.mkdir(path$1.dirname(filePath), { recursive: true });
77
- if (Buffer.isBuffer(data)) await fs.promises.writeFile(filePath, data);
78
- else {
79
- const writeStream = fs.createWriteStream(filePath);
80
- await new Promise((resolve, reject) => {
81
- data.pipe(writeStream);
82
- writeStream.on("finish", resolve);
83
- writeStream.on("error", reject);
84
- });
85
- }
86
- }
87
- async read({ location, relativePath }) {
88
- return fs.promises.readFile(await this.resolve({
89
- location,
90
- relativePath
91
- }));
92
- }
93
- async exists({ location, relativePath }) {
94
- try {
95
- await fs.promises.access(await this.resolve({
96
- location,
97
- relativePath
98
- }));
99
- return true;
100
- } catch {
101
- return false;
102
- }
103
- }
104
- async list({ location, prefix }) {
105
- const base = this.locationPaths.get(location);
106
- if (!base) return [];
107
- const dir = prefix ? path$1.join(base, prefix) : base;
108
- try {
109
- return (await fs.promises.readdir(dir, { withFileTypes: true })).map((e) => prefix ? `${prefix}/${e.name}` : e.name);
110
- } catch {
111
- return [];
112
- }
113
- }
114
- async delete({ location, relativePath }) {
115
- const filePath = await this.resolve({
116
- location,
117
- relativePath
118
- });
119
- await fs.promises.rm(filePath, { force: true });
120
- }
121
- async getAvailableSpace({ location }) {
122
- const base = this.locationPaths.get(location);
123
- if (!base) return null;
124
- try {
125
- let target = base;
126
- while (!fs.existsSync(target)) {
127
- const parent = path$1.dirname(target);
128
- if (!parent || parent === target) return null;
129
- target = parent;
130
- }
131
- const stats = await fs.promises.statfs(target);
132
- return stats.bavail * stats.bsize;
133
- } catch {
134
- return null;
135
- }
136
- }
137
- /** Get the resolved path for a location type (addon-declared ids fall back
138
- * to `<rootPath>/<id>`). */
139
- getLocationPath(location) {
140
- return this.locationPaths.get(location) ?? path$1.join(this.rootPath, location);
141
- }
142
- /** Get the root path */
143
- getRootPath() {
144
- return this.rootPath;
145
- }
146
- };
147
- //#endregion
148
- export { FilesystemStorageProvider };