@camstack/addon-post-analysis 1.1.33 → 1.1.34

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 { C as string, S as object, _ as createEvent, a as cosineSimilarity, b as boolean, d as plateGalleryCapability, f as videoclipsCapability, g as DeviceType, h as BaseAddon, i as audioMetricsCapability, l as nodePin, m as errMsg, n as EVENT_PAD_MS, p as zoneAnalyticsCapability, r as addonWidgetsSourceCapability, s as faceGalleryCapability, t as EVENT_KIND_BY_CAP, u as pipelineAnalyticsCapability, v as hydrateSchema, w as EventCategory, x as number, y as array } from "../dist-CA0GikiM.mjs";
1
+ import { C as object, S as number, T as EventCategory, _ as DeviceType, a as audioMetricsCapability, b as array, c as faceGalleryCapability, d as pipelineAnalyticsCapability, f as plateGalleryCapability, g as BaseAddon, h as errMsg, i as addonWidgetsSourceCapability, m as zoneAnalyticsCapability, n as EVENT_PAD_MS, o as cosineSimilarity, p as videoclipsCapability, r as OpsLogEntrySchema, t as EVENT_KIND_BY_CAP, u as nodePin, v as createEvent, w as string, x as boolean, y as hydrateSchema } from "../dist-B1EgWJrr.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import sharp from "sharp";
4
4
  //#region src/pipeline-analytics/videoclips-provider.ts
@@ -4393,6 +4393,48 @@ var MediaStore = class {
4393
4393
  }
4394
4394
  return removed;
4395
4395
  }
4396
+ /**
4397
+ * On-demand footprint aggregation for the events-management UI: sum
4398
+ * `sizeBytes` and count rows per device across the whole media collection.
4399
+ * Pages by `id` (offset-based) so a large collection never loads at once.
4400
+ * Metadata rows are small (no blob) — the image bytes live on the storage
4401
+ * provider and are counted via the persisted `sizeBytes` column.
4402
+ */
4403
+ async footprintByDevice() {
4404
+ const PAGE = 1e3;
4405
+ const out = /* @__PURE__ */ new Map();
4406
+ let offset = 0;
4407
+ for (;;) {
4408
+ const rows = await this.store.query.query({
4409
+ collection: MEDIA_COLLECTION,
4410
+ filter: {
4411
+ orderBy: {
4412
+ field: "id",
4413
+ direction: "asc"
4414
+ },
4415
+ limit: PAGE,
4416
+ offset
4417
+ }
4418
+ });
4419
+ if (rows.length === 0) break;
4420
+ for (const row of rows) {
4421
+ const data = row.data;
4422
+ const deviceId = Number(data["deviceId"]);
4423
+ const sizeBytes = Number(data["sizeBytes"]);
4424
+ if (!Number.isFinite(deviceId)) continue;
4425
+ const acc = out.get(deviceId) ?? {
4426
+ bytes: 0,
4427
+ rows: 0
4428
+ };
4429
+ acc.bytes += Number.isFinite(sizeBytes) ? sizeBytes : 0;
4430
+ acc.rows += 1;
4431
+ out.set(deviceId, acc);
4432
+ }
4433
+ if (rows.length < PAGE) break;
4434
+ offset += PAGE;
4435
+ }
4436
+ return out;
4437
+ }
4396
4438
  /** Move a single media entry from one owner to another.
4397
4439
  * The new copy is written before the old one is removed.
4398
4440
  * The old-entry deletion is best-effort: on failure a warning is logged
@@ -4754,12 +4796,12 @@ var EventStore = class {
4754
4796
  collection: MOTION_EVENTS_COLLECTION,
4755
4797
  filter: this.buildFilter(q)
4756
4798
  });
4757
- if (q.projection === "slim") return rows.map((r) => slimMotion(r.id, stripNulls(r.data)));
4799
+ if (q.projection === "slim") return rows.map((r) => slimMotion(r.id, stripNulls$1(r.data)));
4758
4800
  return rows.map((r) => {
4759
4801
  return {
4760
4802
  id: r.id,
4761
4803
  kind: "motion",
4762
- ...stripNulls(r.data)
4804
+ ...stripNulls$1(r.data)
4763
4805
  };
4764
4806
  });
4765
4807
  }
@@ -4773,12 +4815,12 @@ var EventStore = class {
4773
4815
  collection: OBJECT_EVENTS_COLLECTION,
4774
4816
  filter
4775
4817
  });
4776
- if (q.projection === "slim") return rows.map((r) => slimObject(r.id, stripNulls(r.data)));
4818
+ if (q.projection === "slim") return rows.map((r) => slimObject(r.id, stripNulls$1(r.data)));
4777
4819
  return rows.map((r) => {
4778
4820
  return {
4779
4821
  id: r.id,
4780
4822
  kind: "object",
4781
- ...stripNulls(r.data)
4823
+ ...stripNulls$1(r.data)
4782
4824
  };
4783
4825
  });
4784
4826
  }
@@ -4787,12 +4829,12 @@ var EventStore = class {
4787
4829
  collection: AUDIO_EVENTS_COLLECTION,
4788
4830
  filter: this.buildFilter(q)
4789
4831
  });
4790
- if (q.projection === "slim") return rows.map((r) => slimAudio(r.id, stripNulls(r.data)));
4832
+ if (q.projection === "slim") return rows.map((r) => slimAudio(r.id, stripNulls$1(r.data)));
4791
4833
  return rows.map((r) => {
4792
4834
  return {
4793
4835
  id: r.id,
4794
4836
  kind: "audio",
4795
- ...stripNulls(r.data)
4837
+ ...stripNulls$1(r.data)
4796
4838
  };
4797
4839
  });
4798
4840
  }
@@ -4938,6 +4980,23 @@ var EventStore = class {
4938
4980
  return updated;
4939
4981
  }
4940
4982
  /**
4983
+ * Total persisted event rows (motion + object + audio) for a device — the
4984
+ * "rows" figure in the events-management footprint. Uses the indexed `count`
4985
+ * aggregate per collection, run in parallel.
4986
+ */
4987
+ async countForDevice(deviceId) {
4988
+ const one = (collection) => this.store.count.query({
4989
+ collection,
4990
+ filter: { where: { deviceId } }
4991
+ });
4992
+ const [motion, object, audio] = await Promise.all([
4993
+ one(MOTION_EVENTS_COLLECTION),
4994
+ one(OBJECT_EVENTS_COLLECTION),
4995
+ one(AUDIO_EVENTS_COLLECTION)
4996
+ ]);
4997
+ return motion + object + audio;
4998
+ }
4999
+ /**
4941
5000
  * Return per-kind event counts in equal-width time buckets.
4942
5001
  *
4943
5002
  * Each bucket maps to a `bucketStart = since + i * bucketMs`.
@@ -5212,12 +5271,189 @@ function bboxAreaFrac(data) {
5212
5271
  if (w <= 0 || h <= 0) return 0;
5213
5272
  return w * h / (fw * fh);
5214
5273
  }
5215
- function stripNulls(data) {
5274
+ function stripNulls$1(data) {
5216
5275
  const out = {};
5217
5276
  for (const [k, v] of Object.entries(data)) if (v !== null) out[k] = v;
5218
5277
  return out;
5219
5278
  }
5220
5279
  //#endregion
5280
+ //#region src/pipeline-analytics/store/ops-log-store.ts
5281
+ /**
5282
+ * OpsLogStore — the EVENTS-domain operations audit for pipeline-analytics.
5283
+ *
5284
+ * pipeline-analytics already owns SQLite collections, so (unlike the recorder's
5285
+ * DurableState ring) the events ops-log is a DECLARED SQL-backed collection.
5286
+ * MUST be declared in `onInitialize` via `declare()` before the first insert —
5287
+ * an undeclared collection crash-loops the runner.
5288
+ *
5289
+ * Collection name: pipeline-analytics:ops-log
5290
+ *
5291
+ * Append is BEST-EFFORT (`append` catches + logs, never throws) — a failed
5292
+ * audit write must not fail the operation it records.
5293
+ */
5294
+ var OPS_LOG_COLLECTION = "pipeline-analytics:ops-log";
5295
+ var OPS_LOG_COLUMNS = [
5296
+ {
5297
+ name: "id",
5298
+ type: "TEXT",
5299
+ primaryKey: true,
5300
+ notNull: true
5301
+ },
5302
+ {
5303
+ name: "at",
5304
+ type: "INTEGER",
5305
+ notNull: true
5306
+ },
5307
+ {
5308
+ name: "domain",
5309
+ type: "TEXT",
5310
+ notNull: true
5311
+ },
5312
+ {
5313
+ name: "op",
5314
+ type: "TEXT",
5315
+ notNull: true
5316
+ },
5317
+ {
5318
+ name: "reason",
5319
+ type: "TEXT",
5320
+ notNull: true
5321
+ },
5322
+ {
5323
+ name: "deviceId",
5324
+ type: "INTEGER"
5325
+ },
5326
+ {
5327
+ name: "nodeId",
5328
+ type: "TEXT",
5329
+ notNull: true
5330
+ },
5331
+ {
5332
+ name: "itemsAffected",
5333
+ type: "INTEGER",
5334
+ notNull: true
5335
+ },
5336
+ {
5337
+ name: "bytesReclaimed",
5338
+ type: "INTEGER",
5339
+ notNull: true
5340
+ },
5341
+ {
5342
+ name: "detail",
5343
+ type: "TEXT"
5344
+ },
5345
+ {
5346
+ name: "actor",
5347
+ type: "TEXT",
5348
+ notNull: true
5349
+ }
5350
+ ];
5351
+ var OPS_LOG_INDEXES = [{
5352
+ name: "idx_opslog_at",
5353
+ columns: ["at"]
5354
+ }, {
5355
+ name: "idx_opslog_device_at",
5356
+ columns: ["deviceId", "at"]
5357
+ }];
5358
+ var OpsLogStore = class {
5359
+ store;
5360
+ logger;
5361
+ nodeId;
5362
+ now;
5363
+ newId;
5364
+ constructor(deps) {
5365
+ this.store = deps.store;
5366
+ this.logger = deps.logger;
5367
+ this.nodeId = deps.nodeId;
5368
+ this.now = deps.now ?? (() => Date.now());
5369
+ this.newId = deps.newId ?? (() => globalThis.crypto.randomUUID());
5370
+ }
5371
+ static async declare(store) {
5372
+ await store.declareCollection.mutate({
5373
+ collection: OPS_LOG_COLLECTION,
5374
+ columns: [...OPS_LOG_COLUMNS],
5375
+ indexes: [...OPS_LOG_INDEXES]
5376
+ });
5377
+ }
5378
+ /**
5379
+ * Append one events-domain ops-log row. Best-effort — stamps
5380
+ * `domain:'events'`, `nodeId`, `id`, `at`, validates, inserts, and swallows
5381
+ * (logs) any failure so it can never fail the operation being audited.
5382
+ */
5383
+ async append(input) {
5384
+ const entry = {
5385
+ id: this.newId(),
5386
+ at: this.now(),
5387
+ domain: "events",
5388
+ op: input.op,
5389
+ reason: input.reason,
5390
+ deviceId: input.deviceId,
5391
+ nodeId: this.nodeId,
5392
+ itemsAffected: input.itemsAffected,
5393
+ bytesReclaimed: input.bytesReclaimed,
5394
+ detail: input.detail ?? null,
5395
+ actor: input.actor ?? "operator"
5396
+ };
5397
+ try {
5398
+ const { id, ...rest } = OpsLogEntrySchema.parse(entry);
5399
+ await this.store.insert.mutate({
5400
+ collection: OPS_LOG_COLLECTION,
5401
+ record: {
5402
+ id,
5403
+ data: rest
5404
+ }
5405
+ });
5406
+ } catch (err) {
5407
+ this.logger.warn("OpsLogStore.append failed (best-effort)", {
5408
+ tags: { deviceId: input.deviceId ?? void 0 },
5409
+ meta: {
5410
+ op: input.op,
5411
+ error: String(err)
5412
+ }
5413
+ });
5414
+ }
5415
+ }
5416
+ /**
5417
+ * List events ops-log rows newest-first, optionally scoped to one device,
5418
+ * capped at `limit` (default {@link OPS_LOG_DEFAULT_LIMIT}).
5419
+ */
5420
+ async list(query) {
5421
+ const filter = {
5422
+ orderBy: {
5423
+ field: "at",
5424
+ direction: "desc"
5425
+ },
5426
+ limit: query.limit ?? 200
5427
+ };
5428
+ if (query.deviceId !== void 0) filter.where = { deviceId: query.deviceId };
5429
+ const rows = await this.store.query.query({
5430
+ collection: OPS_LOG_COLLECTION,
5431
+ filter
5432
+ });
5433
+ const out = [];
5434
+ for (const r of rows) {
5435
+ const parsed = OpsLogEntrySchema.safeParse({
5436
+ id: r.id,
5437
+ ...stripNulls(r.data)
5438
+ });
5439
+ if (parsed.success) out.push(parsed.data);
5440
+ else this.logger.debug("OpsLogStore.list: skipped malformed row", { meta: { id: r.id } });
5441
+ }
5442
+ return out;
5443
+ }
5444
+ };
5445
+ /** SQLite stores nullable columns as `null`; the row schema uses `.nullable()`
5446
+ * for deviceId/detail (accepts null) but drop any stray null on required
5447
+ * fields defensively before parse. */
5448
+ function stripNulls(data) {
5449
+ const out = {};
5450
+ for (const [k, v] of Object.entries(data)) {
5451
+ if (v === null && k !== "deviceId" && k !== "detail") continue;
5452
+ out[k] = v;
5453
+ }
5454
+ return out;
5455
+ }
5456
+ //#endregion
5221
5457
  //#region src/pipeline-analytics/store/sensor-event-store.ts
5222
5458
  var SENSOR_EVENTS_COLLECTION = "pipeline-analytics:sensor-events";
5223
5459
  var SENSOR_EVENT_COLUMNS = [
@@ -10959,6 +11195,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
10959
11195
  stationaryRegistry = null;
10960
11196
  mediaStore = null;
10961
11197
  eventStore = null;
11198
+ /** Events-domain ops-log (footprint/prune/manual-delete audit). Null until onInitialize. */
11199
+ eventsOpsLog = null;
10962
11200
  /** Per-camera history of LINKED-device sensor state changes (Part B). */
10963
11201
  sensorEventStore = null;
10964
11202
  /** Ingest-side reverse index (sensor device → linked camera ids), TTL-cached
@@ -11121,6 +11359,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
11121
11359
  await VehicleStore.declare(api.settingsStore);
11122
11360
  await ObjectEmbeddingStore.declare(api.settingsStore);
11123
11361
  await StationaryObjectRegistry.declare(api.settingsStore);
11362
+ await OpsLogStore.declare(api.settingsStore);
11124
11363
  const logger = this.ctx.logger;
11125
11364
  let storage = this.ctx.kernel.storage;
11126
11365
  const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
@@ -11246,6 +11485,11 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
11246
11485
  });
11247
11486
  const rawNodeId = this.ctx.kernel.localNodeId ?? this.ctx.id;
11248
11487
  const ownNodeId = rawNodeId.includes("/") ? rawNodeId.split("/")[0] : rawNodeId;
11488
+ this.eventsOpsLog = new OpsLogStore({
11489
+ store: api.settingsStore,
11490
+ logger: logger.child("ops-log"),
11491
+ nodeId: ownNodeId
11492
+ });
11249
11493
  {
11250
11494
  const designated = await this.postProcessingNodeState.get();
11251
11495
  this.isPostProcessingNode = ownNodeId === designated;
@@ -13751,6 +13995,120 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
13751
13995
  };
13752
13996
  }
13753
13997
  /**
13998
+ * Durable event-store footprint for the events-management UI: event ROWS
13999
+ * (motion + object + audio) counted per camera (indexed `count`) + total, and
14000
+ * event-owned media BYTES on disk per camera (paged `sizeBytes` sum) + total.
14001
+ * The device set is the union of cameras that have media OR persisted tracks;
14002
+ * cameras with neither rows nor bytes are omitted.
14003
+ */
14004
+ async getEventStoreFootprint() {
14005
+ const eventStore = this.eventStore;
14006
+ const mediaStore = this.mediaStore;
14007
+ if (!eventStore || !mediaStore) return {
14008
+ totalRows: 0,
14009
+ totalBytes: 0,
14010
+ devices: []
14011
+ };
14012
+ const mediaByDevice = await mediaStore.footprintByDevice();
14013
+ const deviceIds = new Set(mediaByDevice.keys());
14014
+ if (this.trackStore) for (const id of await this.trackStore.listDeviceIds()) deviceIds.add(id);
14015
+ const devices = [];
14016
+ let totalRows = 0;
14017
+ let totalBytes = 0;
14018
+ for (const deviceId of deviceIds) {
14019
+ const rows = await eventStore.countForDevice(deviceId);
14020
+ const bytes = mediaByDevice.get(deviceId)?.bytes ?? 0;
14021
+ if (rows === 0 && bytes === 0) continue;
14022
+ totalRows += rows;
14023
+ totalBytes += bytes;
14024
+ devices.push({
14025
+ deviceId,
14026
+ rows,
14027
+ bytes
14028
+ });
14029
+ }
14030
+ devices.sort((a, b) => b.bytes - a.bytes);
14031
+ return {
14032
+ totalRows,
14033
+ totalBytes,
14034
+ devices
14035
+ };
14036
+ }
14037
+ /**
14038
+ * Cluster-wide prune of events with `timestamp < olderThanMs` across every
14039
+ * camera (via the device-agnostic `evictBefore`), deleting each pruned event's
14040
+ * media in lockstep. Logs one global (`deviceId:null`) events ops-log row.
14041
+ * `bytesReclaimed` is 0 (media deletion does not surface freed bytes).
14042
+ */
14043
+ async pruneEvents(input) {
14044
+ const eventStore = this.eventStore;
14045
+ if (!eventStore) return {
14046
+ motion: 0,
14047
+ object: 0,
14048
+ audio: 0
14049
+ };
14050
+ const cutoff = input.olderThanMs;
14051
+ const evicted = await eventStore.evictBefore({
14052
+ motionCutoffMs: cutoff,
14053
+ objectCutoffMs: cutoff,
14054
+ audioCutoffMs: cutoff
14055
+ });
14056
+ const ids = [
14057
+ ...evicted.motion,
14058
+ ...evicted.object,
14059
+ ...evicted.audio
14060
+ ];
14061
+ if (ids.length > 0 && this.mediaStore) await this.mediaStore.deleteForEvents(ids);
14062
+ const counts = {
14063
+ motion: evicted.motion.length,
14064
+ object: evicted.object.length,
14065
+ audio: evicted.audio.length
14066
+ };
14067
+ const total = counts.motion + counts.object + counts.audio;
14068
+ await this.eventsOpsLog?.append({
14069
+ op: "prune",
14070
+ reason: input.reason ?? "retention",
14071
+ deviceId: null,
14072
+ itemsAffected: total,
14073
+ bytesReclaimed: 0,
14074
+ detail: `pruned events older than ${cutoff}`
14075
+ });
14076
+ if (total > 0) this.ctx.logger.info("analytics events pruned (cluster)", { meta: {
14077
+ cutoffMs: cutoff,
14078
+ ...counts
14079
+ } });
14080
+ return counts;
14081
+ }
14082
+ /**
14083
+ * Manually delete EVERY event (motion + object + audio) for one camera and its
14084
+ * event-owned media in lockstep. Logs a manual events ops-log row.
14085
+ */
14086
+ async deleteDeviceEvents(input) {
14087
+ if (!this.eventStore) return {
14088
+ motion: 0,
14089
+ object: 0,
14090
+ audio: 0
14091
+ };
14092
+ const counts = await this.pruneEventsBefore({
14093
+ deviceId: input.deviceId,
14094
+ cutoffMs: Number.MAX_SAFE_INTEGER
14095
+ });
14096
+ const total = counts.motion + counts.object + counts.audio;
14097
+ await this.eventsOpsLog?.append({
14098
+ op: "manual-delete",
14099
+ reason: "manual",
14100
+ deviceId: input.deviceId,
14101
+ itemsAffected: total,
14102
+ bytesReclaimed: 0,
14103
+ detail: "deleted all events for device"
14104
+ });
14105
+ return counts;
14106
+ }
14107
+ /** The events ops-log rows (newest-first), optionally scoped to one camera. */
14108
+ async listOpsLog(input) {
14109
+ return await this.eventsOpsLog?.list(input) ?? [];
14110
+ }
14111
+ /**
13754
14112
  * Track-centric time-based retention (design §5.1). Drains every persisted
13755
14113
  * track for the device whose `lastSeen < cutoffMs`, page by page, through the
13756
14114
  * widened cascade — enrolled faces/plates + identity media are exempt (design
@@ -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-BbAKLasA.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BuAr2V3O.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.1.33",
3
+ "version": "1.1.34",
4
4
  "description": "CamStack Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",