@camstack/addon-post-analysis 1.1.32 → 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.
@@ -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-DU_JRm-j.js");
5
+ const require_dist = require("../dist-BeDNiKi6.js");
6
6
  let node_crypto = require("node:crypto");
7
7
  let sharp = require("sharp");
8
8
  sharp = require_dist.__toESM(sharp);
@@ -4398,6 +4398,48 @@ var MediaStore = class {
4398
4398
  }
4399
4399
  return removed;
4400
4400
  }
4401
+ /**
4402
+ * On-demand footprint aggregation for the events-management UI: sum
4403
+ * `sizeBytes` and count rows per device across the whole media collection.
4404
+ * Pages by `id` (offset-based) so a large collection never loads at once.
4405
+ * Metadata rows are small (no blob) — the image bytes live on the storage
4406
+ * provider and are counted via the persisted `sizeBytes` column.
4407
+ */
4408
+ async footprintByDevice() {
4409
+ const PAGE = 1e3;
4410
+ const out = /* @__PURE__ */ new Map();
4411
+ let offset = 0;
4412
+ for (;;) {
4413
+ const rows = await this.store.query.query({
4414
+ collection: MEDIA_COLLECTION,
4415
+ filter: {
4416
+ orderBy: {
4417
+ field: "id",
4418
+ direction: "asc"
4419
+ },
4420
+ limit: PAGE,
4421
+ offset
4422
+ }
4423
+ });
4424
+ if (rows.length === 0) break;
4425
+ for (const row of rows) {
4426
+ const data = row.data;
4427
+ const deviceId = Number(data["deviceId"]);
4428
+ const sizeBytes = Number(data["sizeBytes"]);
4429
+ if (!Number.isFinite(deviceId)) continue;
4430
+ const acc = out.get(deviceId) ?? {
4431
+ bytes: 0,
4432
+ rows: 0
4433
+ };
4434
+ acc.bytes += Number.isFinite(sizeBytes) ? sizeBytes : 0;
4435
+ acc.rows += 1;
4436
+ out.set(deviceId, acc);
4437
+ }
4438
+ if (rows.length < PAGE) break;
4439
+ offset += PAGE;
4440
+ }
4441
+ return out;
4442
+ }
4401
4443
  /** Move a single media entry from one owner to another.
4402
4444
  * The new copy is written before the old one is removed.
4403
4445
  * The old-entry deletion is best-effort: on failure a warning is logged
@@ -4759,12 +4801,12 @@ var EventStore = class {
4759
4801
  collection: MOTION_EVENTS_COLLECTION,
4760
4802
  filter: this.buildFilter(q)
4761
4803
  });
4762
- if (q.projection === "slim") return rows.map((r) => slimMotion(r.id, stripNulls(r.data)));
4804
+ if (q.projection === "slim") return rows.map((r) => slimMotion(r.id, stripNulls$1(r.data)));
4763
4805
  return rows.map((r) => {
4764
4806
  return {
4765
4807
  id: r.id,
4766
4808
  kind: "motion",
4767
- ...stripNulls(r.data)
4809
+ ...stripNulls$1(r.data)
4768
4810
  };
4769
4811
  });
4770
4812
  }
@@ -4778,12 +4820,12 @@ var EventStore = class {
4778
4820
  collection: OBJECT_EVENTS_COLLECTION,
4779
4821
  filter
4780
4822
  });
4781
- if (q.projection === "slim") return rows.map((r) => slimObject(r.id, stripNulls(r.data)));
4823
+ if (q.projection === "slim") return rows.map((r) => slimObject(r.id, stripNulls$1(r.data)));
4782
4824
  return rows.map((r) => {
4783
4825
  return {
4784
4826
  id: r.id,
4785
4827
  kind: "object",
4786
- ...stripNulls(r.data)
4828
+ ...stripNulls$1(r.data)
4787
4829
  };
4788
4830
  });
4789
4831
  }
@@ -4792,12 +4834,12 @@ var EventStore = class {
4792
4834
  collection: AUDIO_EVENTS_COLLECTION,
4793
4835
  filter: this.buildFilter(q)
4794
4836
  });
4795
- if (q.projection === "slim") return rows.map((r) => slimAudio(r.id, stripNulls(r.data)));
4837
+ if (q.projection === "slim") return rows.map((r) => slimAudio(r.id, stripNulls$1(r.data)));
4796
4838
  return rows.map((r) => {
4797
4839
  return {
4798
4840
  id: r.id,
4799
4841
  kind: "audio",
4800
- ...stripNulls(r.data)
4842
+ ...stripNulls$1(r.data)
4801
4843
  };
4802
4844
  });
4803
4845
  }
@@ -4943,6 +4985,23 @@ var EventStore = class {
4943
4985
  return updated;
4944
4986
  }
4945
4987
  /**
4988
+ * Total persisted event rows (motion + object + audio) for a device — the
4989
+ * "rows" figure in the events-management footprint. Uses the indexed `count`
4990
+ * aggregate per collection, run in parallel.
4991
+ */
4992
+ async countForDevice(deviceId) {
4993
+ const one = (collection) => this.store.count.query({
4994
+ collection,
4995
+ filter: { where: { deviceId } }
4996
+ });
4997
+ const [motion, object, audio] = await Promise.all([
4998
+ one(MOTION_EVENTS_COLLECTION),
4999
+ one(OBJECT_EVENTS_COLLECTION),
5000
+ one(AUDIO_EVENTS_COLLECTION)
5001
+ ]);
5002
+ return motion + object + audio;
5003
+ }
5004
+ /**
4946
5005
  * Return per-kind event counts in equal-width time buckets.
4947
5006
  *
4948
5007
  * Each bucket maps to a `bucketStart = since + i * bucketMs`.
@@ -5217,12 +5276,189 @@ function bboxAreaFrac(data) {
5217
5276
  if (w <= 0 || h <= 0) return 0;
5218
5277
  return w * h / (fw * fh);
5219
5278
  }
5220
- function stripNulls(data) {
5279
+ function stripNulls$1(data) {
5221
5280
  const out = {};
5222
5281
  for (const [k, v] of Object.entries(data)) if (v !== null) out[k] = v;
5223
5282
  return out;
5224
5283
  }
5225
5284
  //#endregion
5285
+ //#region src/pipeline-analytics/store/ops-log-store.ts
5286
+ /**
5287
+ * OpsLogStore — the EVENTS-domain operations audit for pipeline-analytics.
5288
+ *
5289
+ * pipeline-analytics already owns SQLite collections, so (unlike the recorder's
5290
+ * DurableState ring) the events ops-log is a DECLARED SQL-backed collection.
5291
+ * MUST be declared in `onInitialize` via `declare()` before the first insert —
5292
+ * an undeclared collection crash-loops the runner.
5293
+ *
5294
+ * Collection name: pipeline-analytics:ops-log
5295
+ *
5296
+ * Append is BEST-EFFORT (`append` catches + logs, never throws) — a failed
5297
+ * audit write must not fail the operation it records.
5298
+ */
5299
+ var OPS_LOG_COLLECTION = "pipeline-analytics:ops-log";
5300
+ var OPS_LOG_COLUMNS = [
5301
+ {
5302
+ name: "id",
5303
+ type: "TEXT",
5304
+ primaryKey: true,
5305
+ notNull: true
5306
+ },
5307
+ {
5308
+ name: "at",
5309
+ type: "INTEGER",
5310
+ notNull: true
5311
+ },
5312
+ {
5313
+ name: "domain",
5314
+ type: "TEXT",
5315
+ notNull: true
5316
+ },
5317
+ {
5318
+ name: "op",
5319
+ type: "TEXT",
5320
+ notNull: true
5321
+ },
5322
+ {
5323
+ name: "reason",
5324
+ type: "TEXT",
5325
+ notNull: true
5326
+ },
5327
+ {
5328
+ name: "deviceId",
5329
+ type: "INTEGER"
5330
+ },
5331
+ {
5332
+ name: "nodeId",
5333
+ type: "TEXT",
5334
+ notNull: true
5335
+ },
5336
+ {
5337
+ name: "itemsAffected",
5338
+ type: "INTEGER",
5339
+ notNull: true
5340
+ },
5341
+ {
5342
+ name: "bytesReclaimed",
5343
+ type: "INTEGER",
5344
+ notNull: true
5345
+ },
5346
+ {
5347
+ name: "detail",
5348
+ type: "TEXT"
5349
+ },
5350
+ {
5351
+ name: "actor",
5352
+ type: "TEXT",
5353
+ notNull: true
5354
+ }
5355
+ ];
5356
+ var OPS_LOG_INDEXES = [{
5357
+ name: "idx_opslog_at",
5358
+ columns: ["at"]
5359
+ }, {
5360
+ name: "idx_opslog_device_at",
5361
+ columns: ["deviceId", "at"]
5362
+ }];
5363
+ var OpsLogStore = class {
5364
+ store;
5365
+ logger;
5366
+ nodeId;
5367
+ now;
5368
+ newId;
5369
+ constructor(deps) {
5370
+ this.store = deps.store;
5371
+ this.logger = deps.logger;
5372
+ this.nodeId = deps.nodeId;
5373
+ this.now = deps.now ?? (() => Date.now());
5374
+ this.newId = deps.newId ?? (() => globalThis.crypto.randomUUID());
5375
+ }
5376
+ static async declare(store) {
5377
+ await store.declareCollection.mutate({
5378
+ collection: OPS_LOG_COLLECTION,
5379
+ columns: [...OPS_LOG_COLUMNS],
5380
+ indexes: [...OPS_LOG_INDEXES]
5381
+ });
5382
+ }
5383
+ /**
5384
+ * Append one events-domain ops-log row. Best-effort — stamps
5385
+ * `domain:'events'`, `nodeId`, `id`, `at`, validates, inserts, and swallows
5386
+ * (logs) any failure so it can never fail the operation being audited.
5387
+ */
5388
+ async append(input) {
5389
+ const entry = {
5390
+ id: this.newId(),
5391
+ at: this.now(),
5392
+ domain: "events",
5393
+ op: input.op,
5394
+ reason: input.reason,
5395
+ deviceId: input.deviceId,
5396
+ nodeId: this.nodeId,
5397
+ itemsAffected: input.itemsAffected,
5398
+ bytesReclaimed: input.bytesReclaimed,
5399
+ detail: input.detail ?? null,
5400
+ actor: input.actor ?? "operator"
5401
+ };
5402
+ try {
5403
+ const { id, ...rest } = require_dist.OpsLogEntrySchema.parse(entry);
5404
+ await this.store.insert.mutate({
5405
+ collection: OPS_LOG_COLLECTION,
5406
+ record: {
5407
+ id,
5408
+ data: rest
5409
+ }
5410
+ });
5411
+ } catch (err) {
5412
+ this.logger.warn("OpsLogStore.append failed (best-effort)", {
5413
+ tags: { deviceId: input.deviceId ?? void 0 },
5414
+ meta: {
5415
+ op: input.op,
5416
+ error: String(err)
5417
+ }
5418
+ });
5419
+ }
5420
+ }
5421
+ /**
5422
+ * List events ops-log rows newest-first, optionally scoped to one device,
5423
+ * capped at `limit` (default {@link OPS_LOG_DEFAULT_LIMIT}).
5424
+ */
5425
+ async list(query) {
5426
+ const filter = {
5427
+ orderBy: {
5428
+ field: "at",
5429
+ direction: "desc"
5430
+ },
5431
+ limit: query.limit ?? 200
5432
+ };
5433
+ if (query.deviceId !== void 0) filter.where = { deviceId: query.deviceId };
5434
+ const rows = await this.store.query.query({
5435
+ collection: OPS_LOG_COLLECTION,
5436
+ filter
5437
+ });
5438
+ const out = [];
5439
+ for (const r of rows) {
5440
+ const parsed = require_dist.OpsLogEntrySchema.safeParse({
5441
+ id: r.id,
5442
+ ...stripNulls(r.data)
5443
+ });
5444
+ if (parsed.success) out.push(parsed.data);
5445
+ else this.logger.debug("OpsLogStore.list: skipped malformed row", { meta: { id: r.id } });
5446
+ }
5447
+ return out;
5448
+ }
5449
+ };
5450
+ /** SQLite stores nullable columns as `null`; the row schema uses `.nullable()`
5451
+ * for deviceId/detail (accepts null) but drop any stray null on required
5452
+ * fields defensively before parse. */
5453
+ function stripNulls(data) {
5454
+ const out = {};
5455
+ for (const [k, v] of Object.entries(data)) {
5456
+ if (v === null && k !== "deviceId" && k !== "detail") continue;
5457
+ out[k] = v;
5458
+ }
5459
+ return out;
5460
+ }
5461
+ //#endregion
5226
5462
  //#region src/pipeline-analytics/store/sensor-event-store.ts
5227
5463
  var SENSOR_EVENTS_COLLECTION = "pipeline-analytics:sensor-events";
5228
5464
  var SENSOR_EVENT_COLUMNS = [
@@ -5401,6 +5637,7 @@ var PERSON_COLOR = "#22c55e";
5401
5637
  var VEHICLE_COLOR = "#3b82f6";
5402
5638
  var ANIMAL_COLOR = "#f97316";
5403
5639
  var GENERIC_DETECTION_COLOR = "#64748b";
5640
+ var PACKAGE_COLOR = "#a855f7";
5404
5641
  var VEHICLE_CLASSES = new Set([
5405
5642
  "vehicle",
5406
5643
  "car",
@@ -5478,6 +5715,34 @@ async function composeEventKinds(deps, deviceId) {
5478
5715
  } catch (err) {
5479
5716
  deps.onError?.("observedClassNames", err);
5480
5717
  }
5718
+ try {
5719
+ if (deps.packageZonesEnabled && await deps.packageZonesEnabled(deviceId)) {
5720
+ out.push({
5721
+ kind: "package-delivered",
5722
+ label: "Package delivered",
5723
+ color: PACKAGE_COLOR,
5724
+ icon: "package",
5725
+ category: "package",
5726
+ source: {
5727
+ capName: "pipeline-analytics",
5728
+ deviceId
5729
+ }
5730
+ });
5731
+ out.push({
5732
+ kind: "package-picked-up",
5733
+ label: "Package picked up",
5734
+ color: PACKAGE_COLOR,
5735
+ icon: "package",
5736
+ category: "package",
5737
+ source: {
5738
+ capName: "pipeline-analytics",
5739
+ deviceId
5740
+ }
5741
+ });
5742
+ }
5743
+ } catch (err) {
5744
+ deps.onError?.("packageZonesEnabled", err);
5745
+ }
5481
5746
  try {
5482
5747
  const { devices } = await deps.linkedDevices.getLinkedDevices({ deviceId });
5483
5748
  const seen = /* @__PURE__ */ new Set();
@@ -7330,6 +7595,208 @@ function resolveMediaSettings(raw) {
7330
7595
  snapshotMaxIdleMs: pick("snapshotMaxIdleMs")
7331
7596
  };
7332
7597
  }
7598
+ //#endregion
7599
+ //#region src/pipeline-analytics/package-settings.ts
7600
+ /**
7601
+ * Per-device package-drop detector settings (surface C of the
7602
+ * detection-config-exposure plan — a per-device post-analysis section).
7603
+ * Cascade: a per-device override on top of the declared default, resolved
7604
+ * per field (an invalid/missing value falls back to its default — parse
7605
+ * never throws). Mirrors `media-settings` / `tracking-settings`.
7606
+ *
7607
+ * Keys are namespaced (`packageDrop*`) because the device store is a FLAT
7608
+ * blob shared across every post-analysis section — a bare `enabled` would
7609
+ * collide with another section.
7610
+ *
7611
+ * See docs/superpowers/specs/2026-07-17-package-zones-design.md §3.3 +
7612
+ * docs/superpowers/specs/2026-07-17-detection-config-exposure-design.md
7613
+ * (surface C). Dwell default is 15s per operator decision (the design's
7614
+ * §3.3 draft said 45s).
7615
+ */
7616
+ var PackageDropSettingsSchema = require_dist.object({
7617
+ /** Master switch — off by default; opt-in per camera (porch/door cams). */
7618
+ packageDropEnabled: require_dist.boolean().default(false),
7619
+ /**
7620
+ * Minimum OBSERVED dwell (seconds, since first-seen) before a newly
7621
+ * promoted stationary package counts as a delivery. Kills a bag briefly
7622
+ * set down and snatched back. Operator default 15s. Values beyond the
7623
+ * stationary promotion window (30s) are satisfied only once the entry
7624
+ * has actually been observed that long.
7625
+ */
7626
+ packageDropDwellSec: require_dist.number().int().min(0).max(3600).default(15),
7627
+ /** Reject tiny far-field blobs: min bbox area as a fraction of the frame. */
7628
+ packageDropMinBboxAreaFrac: require_dist.number().min(0).max(1).default(.004),
7629
+ /** Emit `package-picked-up` when a delivered package's entry departs. */
7630
+ packageDropPickupEnabled: require_dist.boolean().default(true),
7631
+ /**
7632
+ * Stationary classes that count as a package. MODEL-AGNOSTIC: defaults to
7633
+ * the single `package` class a dedicated parcel model emits; the interim
7634
+ * COCO vector (suitcase/backpack/handbag) already maps to `package`
7635
+ * upstream, so this stays `['package']`.
7636
+ */
7637
+ packageDropClassFilter: require_dist.array(require_dist.string()).default(["package"])
7638
+ });
7639
+ var PACKAGE_DROP_DEFAULTS = PackageDropSettingsSchema.parse({});
7640
+ /**
7641
+ * Resolve a per-device store blob into typed package-drop settings.
7642
+ * Unknown/invalid fields fall back to the default for that field (never
7643
+ * throws on a bad blob).
7644
+ */
7645
+ function resolvePackageDropSettings(raw) {
7646
+ const pick = (key) => {
7647
+ const parsed = PackageDropSettingsSchema.shape[key].safeParse(raw[key]);
7648
+ return parsed.success ? parsed.data : PACKAGE_DROP_DEFAULTS[key];
7649
+ };
7650
+ return {
7651
+ packageDropEnabled: pick("packageDropEnabled"),
7652
+ packageDropDwellSec: pick("packageDropDwellSec"),
7653
+ packageDropMinBboxAreaFrac: pick("packageDropMinBboxAreaFrac"),
7654
+ packageDropPickupEnabled: pick("packageDropPickupEnabled"),
7655
+ packageDropClassFilter: pick("packageDropClassFilter")
7656
+ };
7657
+ }
7658
+ //#endregion
7659
+ //#region src/pipeline-analytics/pipeline/package-drop-detector.ts
7660
+ /** The class every durable package event carries. */
7661
+ var PACKAGE_EVENT_CLASS = "package";
7662
+ /** Fixed high importance — package delivery is inherently high-signal (§6). */
7663
+ var PACKAGE_IMPORTANCE = 1;
7664
+ /** Object-event `state` used for a delivery (a parked package) / a pick-up
7665
+ * (its departure). Both are valid `TrackState` enum members so the events
7666
+ * stay `getObjectEvents`-output-valid. */
7667
+ var DELIVERED_STATE = "idle";
7668
+ var PICKED_UP_STATE = "left";
7669
+ /** Deterministic durable-event ids keyed on the stationary entry id. */
7670
+ function deliveredEventId(entryId) {
7671
+ return `pa-pkg-${entryId}-delivered`;
7672
+ }
7673
+ function pickedUpEventId(entryId) {
7674
+ return `pa-pkg-${entryId}-pickedup`;
7675
+ }
7676
+ var PackageDropDetector = class {
7677
+ deps;
7678
+ constructor(deps) {
7679
+ this.deps = deps;
7680
+ }
7681
+ /** Single entrypoint — bridge the registry's `onChange` here. Never
7682
+ * throws (telemetry-lossy, D8): a failure only misses one package event. */
7683
+ async onStationaryChange(change) {
7684
+ try {
7685
+ if (change.phase === "appeared") await this.onAppeared(change.entry, change.timestamp);
7686
+ else await this.onDeparted(change.entry, change.timestamp);
7687
+ } catch (err) {
7688
+ this.deps.onError?.("onStationaryChange", err);
7689
+ this.deps.logger.warn("package-drop detector failed on change", {
7690
+ tags: { deviceId: change.entry.deviceId },
7691
+ meta: {
7692
+ phase: change.phase,
7693
+ entryId: change.entry.id,
7694
+ error: err instanceof Error ? err.message : String(err)
7695
+ }
7696
+ });
7697
+ }
7698
+ }
7699
+ async onAppeared(entry, timestamp) {
7700
+ const settings = await this.deps.resolveSettings(entry.deviceId);
7701
+ if (!settings.packageDropEnabled) return;
7702
+ if (!settings.packageDropClassFilter.includes(entry.className)) return;
7703
+ const rules = (await this.deps.resolvePackageRules(entry.deviceId)).filter((r) => r.enabled !== false);
7704
+ if (rules.length === 0) return;
7705
+ const ruleZoneIds = new Set(rules.flatMap((r) => r.zoneIds));
7706
+ const hitZones = computeStationaryEntryZones(entry, await this.deps.resolveZones(entry.deviceId)).filter((z) => ruleZoneIds.has(z));
7707
+ if (hitZones.length === 0) return;
7708
+ if (timestamp - entry.firstSeenAt < settings.packageDropDwellSec * 1e3) return;
7709
+ const frameArea = entry.frameWidth * entry.frameHeight;
7710
+ if (frameArea <= 0) return;
7711
+ if (entry.bbox.w * entry.bbox.h / frameArea < settings.packageDropMinBboxAreaFrac) return;
7712
+ const eventId = deliveredEventId(entry.id);
7713
+ if ((await this.deps.events.queryObject({
7714
+ deviceId: entry.deviceId,
7715
+ classFilter: "package"
7716
+ })).some((e) => e.id === eventId)) return;
7717
+ const ev = {
7718
+ id: eventId,
7719
+ kind: "object",
7720
+ deviceId: entry.deviceId,
7721
+ timestamp,
7722
+ source: "pipeline",
7723
+ trackId: entry.sourceTrackId ?? entry.id,
7724
+ className: PACKAGE_EVENT_CLASS,
7725
+ ...entry.label !== void 0 ? { label: entry.label } : {},
7726
+ confidence: PACKAGE_IMPORTANCE,
7727
+ bbox: {
7728
+ x: entry.bbox.x,
7729
+ y: entry.bbox.y,
7730
+ w: entry.bbox.w,
7731
+ h: entry.bbox.h
7732
+ },
7733
+ zones: hitZones,
7734
+ state: DELIVERED_STATE,
7735
+ frameWidth: entry.frameWidth,
7736
+ frameHeight: entry.frameHeight,
7737
+ ...entry.keyFrameMediaKey !== void 0 ? { mediaKey: entry.keyFrameMediaKey } : {},
7738
+ importance: PACKAGE_IMPORTANCE
7739
+ };
7740
+ await this.deps.events.insertObject(ev);
7741
+ this.deps.emit.delivered({
7742
+ deviceId: entry.deviceId,
7743
+ entryId: entry.id,
7744
+ eventId,
7745
+ className: entry.className,
7746
+ zoneIds: hitZones,
7747
+ ...entry.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: entry.keyFrameMediaKey } : {},
7748
+ bbox: {
7749
+ x: entry.bbox.x,
7750
+ y: entry.bbox.y,
7751
+ w: entry.bbox.w,
7752
+ h: entry.bbox.h
7753
+ },
7754
+ timestamp
7755
+ });
7756
+ }
7757
+ async onDeparted(entry, timestamp) {
7758
+ if (!(await this.deps.resolveSettings(entry.deviceId)).packageDropPickupEnabled) return;
7759
+ const deliveredId = deliveredEventId(entry.id);
7760
+ const pickedUpId = pickedUpEventId(entry.id);
7761
+ const rows = await this.deps.events.queryObject({
7762
+ deviceId: entry.deviceId,
7763
+ classFilter: PACKAGE_EVENT_CLASS
7764
+ });
7765
+ const delivered = rows.find((e) => e.id === deliveredId);
7766
+ if (delivered === void 0) return;
7767
+ if (rows.some((e) => e.id === pickedUpId)) return;
7768
+ const ev = {
7769
+ id: pickedUpId,
7770
+ kind: "object",
7771
+ deviceId: entry.deviceId,
7772
+ timestamp,
7773
+ source: "pipeline",
7774
+ trackId: entry.sourceTrackId ?? entry.id,
7775
+ className: PACKAGE_EVENT_CLASS,
7776
+ ...entry.label !== void 0 ? { label: entry.label } : {},
7777
+ confidence: PACKAGE_IMPORTANCE,
7778
+ bbox: {
7779
+ x: entry.bbox.x,
7780
+ y: entry.bbox.y,
7781
+ w: entry.bbox.w,
7782
+ h: entry.bbox.h
7783
+ },
7784
+ ...delivered.zones !== void 0 ? { zones: delivered.zones } : {},
7785
+ state: PICKED_UP_STATE,
7786
+ frameWidth: entry.frameWidth,
7787
+ frameHeight: entry.frameHeight,
7788
+ importance: PACKAGE_IMPORTANCE
7789
+ };
7790
+ await this.deps.events.insertObject(ev);
7791
+ this.deps.emit.pickedUp({
7792
+ deviceId: entry.deviceId,
7793
+ entryId: entry.id,
7794
+ deliveredEventId: deliveredId,
7795
+ className: entry.className,
7796
+ timestamp
7797
+ });
7798
+ }
7799
+ };
7333
7800
  function centroidOf(b) {
7334
7801
  return {
7335
7802
  x: b.x + b.w / 2,
@@ -10733,6 +11200,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
10733
11200
  stationaryRegistry = null;
10734
11201
  mediaStore = null;
10735
11202
  eventStore = null;
11203
+ /** Events-domain ops-log (footprint/prune/manual-delete audit). Null until onInitialize. */
11204
+ eventsOpsLog = null;
10736
11205
  /** Per-camera history of LINKED-device sensor state changes (Part B). */
10737
11206
  sensorEventStore = null;
10738
11207
  /** Ingest-side reverse index (sensor device → linked camera ids), TTL-cached
@@ -10810,6 +11279,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
10810
11279
  /** GLOBAL face-recognition master switch (addon store), TTL-cached. */
10811
11280
  faceGlobalEnabledCache = null;
10812
11281
  mediaCacheByDevice = /* @__PURE__ */ new Map();
11282
+ packageDropCacheByDevice = /* @__PURE__ */ new Map();
11283
+ /** Turns stationary appear/depart into package-delivered/picked-up events. */
11284
+ packageDropDetector = null;
10813
11285
  /** Consecutive detector-dropout frames skipped per (deviceId, source) key. */
10814
11286
  dropoutSkipsByKey = /* @__PURE__ */ new Map();
10815
11287
  /** Best (highest-confidence) frame per track — drives the single overwrite
@@ -10892,11 +11364,12 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
10892
11364
  await VehicleStore.declare(api.settingsStore);
10893
11365
  await ObjectEmbeddingStore.declare(api.settingsStore);
10894
11366
  await StationaryObjectRegistry.declare(api.settingsStore);
11367
+ await OpsLogStore.declare(api.settingsStore);
10895
11368
  const logger = this.ctx.logger;
10896
11369
  let storage = this.ctx.kernel.storage;
10897
11370
  const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
10898
11371
  if (mediaRoot) {
10899
- const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-K1C_KC3d.js"));
11372
+ const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-DYa6O693.js"));
10900
11373
  storage = new FilesystemStorageProvider(mediaRoot);
10901
11374
  logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
10902
11375
  }
@@ -10927,6 +11400,11 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
10927
11400
  timestamp
10928
11401
  }
10929
11402
  });
11403
+ this.packageDropDetector?.onStationaryChange({
11404
+ phase,
11405
+ entry,
11406
+ timestamp
11407
+ });
10930
11408
  }
10931
11409
  });
10932
11410
  await this.stationaryRegistry.load();
@@ -10935,15 +11413,55 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
10935
11413
  store: api.settingsStore,
10936
11414
  logger: logger.child("MediaStore")
10937
11415
  });
10938
- this.eventStore = new EventStore({
11416
+ const eventStore = new EventStore({
10939
11417
  store: api.settingsStore,
10940
11418
  logger: logger.child("EventStore"),
10941
11419
  media: this.mediaStore
10942
11420
  });
11421
+ this.eventStore = eventStore;
10943
11422
  this.sensorEventStore = new SensorEventStore({
10944
11423
  store: api.settingsStore,
10945
11424
  logger: logger.child("SensorEventStore")
10946
11425
  });
11426
+ this.packageDropDetector = new PackageDropDetector({
11427
+ events: eventStore,
11428
+ emit: {
11429
+ delivered: (payload) => {
11430
+ this.ctx.eventBus.emit({
11431
+ id: `pa-package-delivered-${payload.entryId}`,
11432
+ timestamp: new Date(payload.timestamp),
11433
+ source: {
11434
+ type: "addon",
11435
+ id: "pipeline-analytics",
11436
+ addonId: "pipeline-analytics"
11437
+ },
11438
+ category: require_dist.EventCategory.PipelineAnalyticsPackageDelivered,
11439
+ data: { ...payload }
11440
+ });
11441
+ },
11442
+ pickedUp: (payload) => {
11443
+ this.ctx.eventBus.emit({
11444
+ id: `pa-package-pickedup-${payload.entryId}`,
11445
+ timestamp: new Date(payload.timestamp),
11446
+ source: {
11447
+ type: "addon",
11448
+ id: "pipeline-analytics",
11449
+ addonId: "pipeline-analytics"
11450
+ },
11451
+ category: require_dist.EventCategory.PipelineAnalyticsPackagePickedUp,
11452
+ data: { ...payload }
11453
+ });
11454
+ }
11455
+ },
11456
+ logger: logger.child("PackageDropDetector"),
11457
+ resolveZones: (deviceId) => this.resolveDeviceZones(deviceId),
11458
+ resolvePackageRules: (deviceId) => this.resolveDevicePackageRules(deviceId),
11459
+ resolveSettings: (deviceId) => this.resolveDevicePackageDropSettings(deviceId),
11460
+ onError: (scope, err) => logger.warn("package-drop detector error", { meta: {
11461
+ scope,
11462
+ error: require_dist.errMsg(err)
11463
+ } })
11464
+ });
10947
11465
  this.linkedCamerasCache = new LinkedCamerasCache({
10948
11466
  cameras: { listCameraIds: async () => {
10949
11467
  return (await api.deviceManager.listAll.query({})).filter((d) => d.isCamera).map((d) => d.id);
@@ -10972,6 +11490,11 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
10972
11490
  });
10973
11491
  const rawNodeId = this.ctx.kernel.localNodeId ?? this.ctx.id;
10974
11492
  const ownNodeId = rawNodeId.includes("/") ? rawNodeId.split("/")[0] : rawNodeId;
11493
+ this.eventsOpsLog = new OpsLogStore({
11494
+ store: api.settingsStore,
11495
+ logger: logger.child("ops-log"),
11496
+ nodeId: ownNodeId
11497
+ });
10975
11498
  {
10976
11499
  const designated = await this.postProcessingNodeState.get();
10977
11500
  this.isPostProcessingNode = ownNodeId === designated;
@@ -11223,6 +11746,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
11223
11746
  this.trackingCacheByDevice.delete(data.deviceId);
11224
11747
  this.faceCacheByDevice.delete(data.deviceId);
11225
11748
  this.mediaCacheByDevice.delete(data.deviceId);
11749
+ this.packageDropCacheByDevice.delete(data.deviceId);
11226
11750
  }
11227
11751
  });
11228
11752
  this.unsubDeviceUnreg = this.ctx.eventBus.subscribe({ category: require_dist.EventCategory.DeviceUnregistered }, (ev) => {
@@ -11238,6 +11762,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
11238
11762
  this.trackingCacheByDevice.delete(deviceId);
11239
11763
  this.faceCacheByDevice.delete(deviceId);
11240
11764
  this.mediaCacheByDevice.delete(deviceId);
11765
+ this.packageDropCacheByDevice.delete(deviceId);
11241
11766
  this.bindingCache?.invalidate(deviceId);
11242
11767
  this.zoneAnalytics?.forgetDevice(deviceId);
11243
11768
  this.audioMetrics?.forgetDevice(deviceId);
@@ -11585,6 +12110,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
11585
12110
  this.faceCacheByDevice.clear();
11586
12111
  this.faceGlobalEnabledCache = null;
11587
12112
  this.mediaCacheByDevice.clear();
12113
+ this.packageDropCacheByDevice.clear();
11588
12114
  this.trackStore?.clearAll();
11589
12115
  this.stationaryRegistry = null;
11590
12116
  this.bindingCache?.clearAll();
@@ -12060,6 +12586,32 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12060
12586
  });
12061
12587
  return settings;
12062
12588
  }
12589
+ async resolveDevicePackageDropSettings(deviceId) {
12590
+ const now = Date.now();
12591
+ const cached = this.packageDropCacheByDevice.get(deviceId);
12592
+ if (cached && now < cached.expiresAt) return cached.settings;
12593
+ const settings = resolvePackageDropSettings(await this.ctx?.settings?.readDeviceStore(deviceId) ?? {});
12594
+ this.packageDropCacheByDevice.set(deviceId, {
12595
+ settings,
12596
+ expiresAt: now + SETTINGS_CACHE_TTL_MS
12597
+ });
12598
+ return settings;
12599
+ }
12600
+ /**
12601
+ * Resolve a device's ENABLED `package`-stage zone rules independent of the
12602
+ * live frame path (mirrors `resolveDeviceZones`). Warms the proxy and,
12603
+ * when the cached slice is empty, forces one refresh. The `package` slice
12604
+ * is written by the orchestrator's package-stage provider (a later slice);
12605
+ * until then this returns `[]` and no package events fire.
12606
+ */
12607
+ async resolveDevicePackageRules(deviceId) {
12608
+ const proxy = await this.ensureProxy(deviceId);
12609
+ if (!proxy) return [];
12610
+ const cached = proxy.state.zoneRules.value?.package;
12611
+ if (cached && cached.length > 0) return cached;
12612
+ await proxy.state.zoneRules.refresh().catch(() => void 0);
12613
+ return proxy.state.zoneRules.value?.package ?? [];
12614
+ }
12063
12615
  /**
12064
12616
  * Route one track's `runDetailSubtree` results (two-plane detail dispatch)
12065
12617
  * into the EXISTING per-track consumers, discriminated by payload SHAPE:
@@ -13161,6 +13713,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13161
13713
  linkedDevices: { getLinkedDevices: (i) => api.deviceManager.getLinkedDevices.query(i) },
13162
13714
  bindings: { getBindings: (i) => api.deviceManager.getBindings.query(i) },
13163
13715
  observedClassNames: async (deviceId) => this.trackStore?.observedClassNames(deviceId) ?? [],
13716
+ packageZonesEnabled: async (deviceId) => {
13717
+ return (await this.resolveDevicePackageRules(deviceId)).some((r) => r.enabled !== false);
13718
+ },
13164
13719
  onError: (scope, err) => this.ctx.logger.warn("listEventKinds: partial compose", {
13165
13720
  tags: { deviceId: input.deviceId },
13166
13721
  meta: {
@@ -13445,6 +14000,120 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13445
14000
  };
13446
14001
  }
13447
14002
  /**
14003
+ * Durable event-store footprint for the events-management UI: event ROWS
14004
+ * (motion + object + audio) counted per camera (indexed `count`) + total, and
14005
+ * event-owned media BYTES on disk per camera (paged `sizeBytes` sum) + total.
14006
+ * The device set is the union of cameras that have media OR persisted tracks;
14007
+ * cameras with neither rows nor bytes are omitted.
14008
+ */
14009
+ async getEventStoreFootprint() {
14010
+ const eventStore = this.eventStore;
14011
+ const mediaStore = this.mediaStore;
14012
+ if (!eventStore || !mediaStore) return {
14013
+ totalRows: 0,
14014
+ totalBytes: 0,
14015
+ devices: []
14016
+ };
14017
+ const mediaByDevice = await mediaStore.footprintByDevice();
14018
+ const deviceIds = new Set(mediaByDevice.keys());
14019
+ if (this.trackStore) for (const id of await this.trackStore.listDeviceIds()) deviceIds.add(id);
14020
+ const devices = [];
14021
+ let totalRows = 0;
14022
+ let totalBytes = 0;
14023
+ for (const deviceId of deviceIds) {
14024
+ const rows = await eventStore.countForDevice(deviceId);
14025
+ const bytes = mediaByDevice.get(deviceId)?.bytes ?? 0;
14026
+ if (rows === 0 && bytes === 0) continue;
14027
+ totalRows += rows;
14028
+ totalBytes += bytes;
14029
+ devices.push({
14030
+ deviceId,
14031
+ rows,
14032
+ bytes
14033
+ });
14034
+ }
14035
+ devices.sort((a, b) => b.bytes - a.bytes);
14036
+ return {
14037
+ totalRows,
14038
+ totalBytes,
14039
+ devices
14040
+ };
14041
+ }
14042
+ /**
14043
+ * Cluster-wide prune of events with `timestamp < olderThanMs` across every
14044
+ * camera (via the device-agnostic `evictBefore`), deleting each pruned event's
14045
+ * media in lockstep. Logs one global (`deviceId:null`) events ops-log row.
14046
+ * `bytesReclaimed` is 0 (media deletion does not surface freed bytes).
14047
+ */
14048
+ async pruneEvents(input) {
14049
+ const eventStore = this.eventStore;
14050
+ if (!eventStore) return {
14051
+ motion: 0,
14052
+ object: 0,
14053
+ audio: 0
14054
+ };
14055
+ const cutoff = input.olderThanMs;
14056
+ const evicted = await eventStore.evictBefore({
14057
+ motionCutoffMs: cutoff,
14058
+ objectCutoffMs: cutoff,
14059
+ audioCutoffMs: cutoff
14060
+ });
14061
+ const ids = [
14062
+ ...evicted.motion,
14063
+ ...evicted.object,
14064
+ ...evicted.audio
14065
+ ];
14066
+ if (ids.length > 0 && this.mediaStore) await this.mediaStore.deleteForEvents(ids);
14067
+ const counts = {
14068
+ motion: evicted.motion.length,
14069
+ object: evicted.object.length,
14070
+ audio: evicted.audio.length
14071
+ };
14072
+ const total = counts.motion + counts.object + counts.audio;
14073
+ await this.eventsOpsLog?.append({
14074
+ op: "prune",
14075
+ reason: input.reason ?? "retention",
14076
+ deviceId: null,
14077
+ itemsAffected: total,
14078
+ bytesReclaimed: 0,
14079
+ detail: `pruned events older than ${cutoff}`
14080
+ });
14081
+ if (total > 0) this.ctx.logger.info("analytics events pruned (cluster)", { meta: {
14082
+ cutoffMs: cutoff,
14083
+ ...counts
14084
+ } });
14085
+ return counts;
14086
+ }
14087
+ /**
14088
+ * Manually delete EVERY event (motion + object + audio) for one camera and its
14089
+ * event-owned media in lockstep. Logs a manual events ops-log row.
14090
+ */
14091
+ async deleteDeviceEvents(input) {
14092
+ if (!this.eventStore) return {
14093
+ motion: 0,
14094
+ object: 0,
14095
+ audio: 0
14096
+ };
14097
+ const counts = await this.pruneEventsBefore({
14098
+ deviceId: input.deviceId,
14099
+ cutoffMs: Number.MAX_SAFE_INTEGER
14100
+ });
14101
+ const total = counts.motion + counts.object + counts.audio;
14102
+ await this.eventsOpsLog?.append({
14103
+ op: "manual-delete",
14104
+ reason: "manual",
14105
+ deviceId: input.deviceId,
14106
+ itemsAffected: total,
14107
+ bytesReclaimed: 0,
14108
+ detail: "deleted all events for device"
14109
+ });
14110
+ return counts;
14111
+ }
14112
+ /** The events ops-log rows (newest-first), optionally scoped to one camera. */
14113
+ async listOpsLog(input) {
14114
+ return await this.eventsOpsLog?.list(input) ?? [];
14115
+ }
14116
+ /**
13448
14117
  * Track-centric time-based retention (design §5.1). Drains every persisted
13449
14118
  * track for the device whose `lastSeen < cutoffMs`, page by page, through the
13450
14119
  * widened cascade — enrolled faces/plates + identity media are exempt (design
@@ -13824,6 +14493,51 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13824
14493
  }
13825
14494
  ]
13826
14495
  },
14496
+ {
14497
+ id: "package-drop",
14498
+ title: "Package detection",
14499
+ description: "Detect a delivered package (a package-class object left parked inside a package zone) and, symmetrically, its pick-up. Draw the package zone in the zone editor; enable the package class in the object-detection step. Per-device override.",
14500
+ columns: 2,
14501
+ fields: [
14502
+ {
14503
+ type: "boolean",
14504
+ key: "packageDropEnabled",
14505
+ label: "Enable package detection",
14506
+ description: "Turn package delivered / picked-up detection on for this camera (off elsewhere).",
14507
+ default: PACKAGE_DROP_DEFAULTS.packageDropEnabled
14508
+ },
14509
+ {
14510
+ type: "boolean",
14511
+ key: "packageDropPickupEnabled",
14512
+ label: "Emit pick-up",
14513
+ description: "Also emit a package-picked-up event when the delivered package leaves.",
14514
+ default: PACKAGE_DROP_DEFAULTS.packageDropPickupEnabled
14515
+ },
14516
+ {
14517
+ type: "slider",
14518
+ key: "packageDropDwellSec",
14519
+ label: "Minimum dwell",
14520
+ description: "How long a package must stay parked before it counts as a delivery. Higher = fewer false positives from bags briefly set down.",
14521
+ min: 0,
14522
+ max: 300,
14523
+ step: 5,
14524
+ default: PACKAGE_DROP_DEFAULTS.packageDropDwellSec,
14525
+ showValue: true,
14526
+ unit: "s"
14527
+ },
14528
+ {
14529
+ type: "slider",
14530
+ key: "packageDropMinBboxAreaFrac",
14531
+ label: "Minimum package size",
14532
+ description: "Reject tiny far-field blobs: the package box must cover at least this fraction of the frame.",
14533
+ min: 0,
14534
+ max: .1,
14535
+ step: .001,
14536
+ default: PACKAGE_DROP_DEFAULTS.packageDropMinBboxAreaFrac,
14537
+ showValue: true
14538
+ }
14539
+ ]
14540
+ },
13827
14541
  {
13828
14542
  id: "audio-detection",
13829
14543
  title: "Audio detection",