@camstack/addon-post-analysis 1.1.33 → 1.1.35
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.
- package/dist/{dist-CA0GikiM.mjs → dist-8DTQLWKO.mjs} +589 -48
- package/dist/{dist-V8XOHDoj.js → dist-Bc4L7FGf.js} +618 -47
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-E-m2CYYM.js → node-BRwocT7C.js} +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BbAKLasA.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CMH9Gs68.mjs} +3 -3
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DLzy78dH.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-lUENPwl7.mjs → hostInit-Bhr2CNHt.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +596 -127
- package/dist/pipeline-analytics/index.mjs +595 -126
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-ai-NpPOn.mjs +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as string, S as
|
|
1
|
+
import { C as hydrateSchema, D as object, E as number, O as string, S as createEvent, T as boolean, _ as videoclipsCapability, a as OpsLogEntrySchema, b as BaseAddon, c as buildEventKindDescriptor, d as faceGalleryCapability, g as subKindsOf, h as plateGalleryCapability, i as MACRO_LABELS, k as EventCategory, l as cosineSimilarity, m as pipelineAnalyticsCapability, n as EVENT_KIND_BY_CAP, o as addonWidgetsSourceCapability, p as nodePin, r as EVENT_PAD_MS, s as audioMetricsCapability, t as DEFAULT_EVENT_COLOR, v as zoneAnalyticsCapability, w as array, x as DeviceType, y as errMsg } from "../dist-8DTQLWKO.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import sharp from "sharp";
|
|
4
4
|
//#region src/pipeline-analytics/videoclips-provider.ts
|
|
@@ -160,13 +160,13 @@ var CLASS_RANK_VEHICLE = .8;
|
|
|
160
160
|
var CLASS_RANK_ANIMAL = .5;
|
|
161
161
|
var CLASS_RANK_DEFAULT = .25;
|
|
162
162
|
var PERSON_CLASSES = new Set(["person", "face"]);
|
|
163
|
-
var VEHICLE_CLASSES
|
|
163
|
+
var VEHICLE_CLASSES = new Set([
|
|
164
164
|
"vehicle",
|
|
165
165
|
"car",
|
|
166
166
|
"truck",
|
|
167
167
|
"bus"
|
|
168
168
|
]);
|
|
169
|
-
var ANIMAL_CLASSES
|
|
169
|
+
var ANIMAL_CLASSES = new Set([
|
|
170
170
|
"animal",
|
|
171
171
|
"dog",
|
|
172
172
|
"cat"
|
|
@@ -182,8 +182,8 @@ function clamp01(x) {
|
|
|
182
182
|
function classRank(className) {
|
|
183
183
|
const c = className.toLowerCase();
|
|
184
184
|
if (PERSON_CLASSES.has(c)) return 1;
|
|
185
|
-
if (VEHICLE_CLASSES
|
|
186
|
-
if (ANIMAL_CLASSES
|
|
185
|
+
if (VEHICLE_CLASSES.has(c)) return CLASS_RANK_VEHICLE;
|
|
186
|
+
if (ANIMAL_CLASSES.has(c)) return CLASS_RANK_ANIMAL;
|
|
187
187
|
return CLASS_RANK_DEFAULT;
|
|
188
188
|
}
|
|
189
189
|
/**
|
|
@@ -3314,6 +3314,10 @@ var TRACKS_COLUMNS = [
|
|
|
3314
3314
|
name: "label",
|
|
3315
3315
|
type: "TEXT"
|
|
3316
3316
|
},
|
|
3317
|
+
{
|
|
3318
|
+
name: "source",
|
|
3319
|
+
type: "TEXT"
|
|
3320
|
+
},
|
|
3317
3321
|
{
|
|
3318
3322
|
name: "firstSeen",
|
|
3319
3323
|
type: "INTEGER",
|
|
@@ -4004,6 +4008,34 @@ var TrackStore = class {
|
|
|
4004
4008
|
const row = records[0];
|
|
4005
4009
|
return this.rowToTrack(row.id, row.data);
|
|
4006
4010
|
}
|
|
4011
|
+
/**
|
|
4012
|
+
* Persist a SYNTHETIC track (a sensor/control event projected into the
|
|
4013
|
+
* unified track store). Unlike `persistCompleted` there is no live in-RAM
|
|
4014
|
+
* state and no trajectory envelope (synthetic tracks carry no positions) —
|
|
4015
|
+
* the row is written directly. `source: 'sensor'` marks it so spatial
|
|
4016
|
+
* consumers skip it; the existing time-based clustering joins it by
|
|
4017
|
+
* timestamp like any other track.
|
|
4018
|
+
*/
|
|
4019
|
+
async persistSyntheticTrack(t) {
|
|
4020
|
+
await this.store.set.mutate({
|
|
4021
|
+
collection: TRACKS_COLLECTION,
|
|
4022
|
+
key: t.trackId,
|
|
4023
|
+
value: {
|
|
4024
|
+
deviceId: t.deviceId,
|
|
4025
|
+
className: t.className,
|
|
4026
|
+
...t.label !== void 0 ? { label: t.label } : {},
|
|
4027
|
+
source: t.source ?? "sensor",
|
|
4028
|
+
firstSeen: t.firstSeen,
|
|
4029
|
+
lastSeen: t.lastSeen,
|
|
4030
|
+
positions: [...t.positions],
|
|
4031
|
+
snapshots: [...t.snapshots],
|
|
4032
|
+
zonesVisited: [...t.zonesVisited],
|
|
4033
|
+
...t.classes !== void 0 ? { classes: [...t.classes] } : {},
|
|
4034
|
+
totalDistance: t.totalDistance,
|
|
4035
|
+
state: t.state
|
|
4036
|
+
}
|
|
4037
|
+
});
|
|
4038
|
+
}
|
|
4007
4039
|
async persistCompleted(t) {
|
|
4008
4040
|
const dims = this.frameDims?.(t.deviceId);
|
|
4009
4041
|
const envelope = computeTrackEnvelope(t.positions, dims?.w, dims?.h);
|
|
@@ -4014,6 +4046,7 @@ var TrackStore = class {
|
|
|
4014
4046
|
deviceId: t.deviceId,
|
|
4015
4047
|
className: t.className,
|
|
4016
4048
|
...t.label !== void 0 ? { label: t.label } : {},
|
|
4049
|
+
...t.source !== void 0 ? { source: t.source } : {},
|
|
4017
4050
|
firstSeen: t.firstSeen,
|
|
4018
4051
|
lastSeen: t.lastSeen,
|
|
4019
4052
|
positions: [...t.positions],
|
|
@@ -4052,6 +4085,7 @@ var TrackStore = class {
|
|
|
4052
4085
|
const zones = data["zonesVisited"] ?? [];
|
|
4053
4086
|
const classes = data["classes"];
|
|
4054
4087
|
const label = data["label"];
|
|
4088
|
+
const source = data["source"];
|
|
4055
4089
|
const importance = data["importance"];
|
|
4056
4090
|
const bestEventId = data["bestEventId"];
|
|
4057
4091
|
const importanceReason = data["importanceReason"];
|
|
@@ -4071,6 +4105,7 @@ var TrackStore = class {
|
|
|
4071
4105
|
deviceId: Number(data["deviceId"]),
|
|
4072
4106
|
className: String(data["className"]),
|
|
4073
4107
|
...typeof label === "string" ? { label } : {},
|
|
4108
|
+
...source === "sensor" || source === "pipeline" ? { source } : {},
|
|
4074
4109
|
firstSeen: Number(data["firstSeen"]),
|
|
4075
4110
|
lastSeen: Number(data["lastSeen"]),
|
|
4076
4111
|
positions,
|
|
@@ -4393,6 +4428,48 @@ var MediaStore = class {
|
|
|
4393
4428
|
}
|
|
4394
4429
|
return removed;
|
|
4395
4430
|
}
|
|
4431
|
+
/**
|
|
4432
|
+
* On-demand footprint aggregation for the events-management UI: sum
|
|
4433
|
+
* `sizeBytes` and count rows per device across the whole media collection.
|
|
4434
|
+
* Pages by `id` (offset-based) so a large collection never loads at once.
|
|
4435
|
+
* Metadata rows are small (no blob) — the image bytes live on the storage
|
|
4436
|
+
* provider and are counted via the persisted `sizeBytes` column.
|
|
4437
|
+
*/
|
|
4438
|
+
async footprintByDevice() {
|
|
4439
|
+
const PAGE = 1e3;
|
|
4440
|
+
const out = /* @__PURE__ */ new Map();
|
|
4441
|
+
let offset = 0;
|
|
4442
|
+
for (;;) {
|
|
4443
|
+
const rows = await this.store.query.query({
|
|
4444
|
+
collection: MEDIA_COLLECTION,
|
|
4445
|
+
filter: {
|
|
4446
|
+
orderBy: {
|
|
4447
|
+
field: "id",
|
|
4448
|
+
direction: "asc"
|
|
4449
|
+
},
|
|
4450
|
+
limit: PAGE,
|
|
4451
|
+
offset
|
|
4452
|
+
}
|
|
4453
|
+
});
|
|
4454
|
+
if (rows.length === 0) break;
|
|
4455
|
+
for (const row of rows) {
|
|
4456
|
+
const data = row.data;
|
|
4457
|
+
const deviceId = Number(data["deviceId"]);
|
|
4458
|
+
const sizeBytes = Number(data["sizeBytes"]);
|
|
4459
|
+
if (!Number.isFinite(deviceId)) continue;
|
|
4460
|
+
const acc = out.get(deviceId) ?? {
|
|
4461
|
+
bytes: 0,
|
|
4462
|
+
rows: 0
|
|
4463
|
+
};
|
|
4464
|
+
acc.bytes += Number.isFinite(sizeBytes) ? sizeBytes : 0;
|
|
4465
|
+
acc.rows += 1;
|
|
4466
|
+
out.set(deviceId, acc);
|
|
4467
|
+
}
|
|
4468
|
+
if (rows.length < PAGE) break;
|
|
4469
|
+
offset += PAGE;
|
|
4470
|
+
}
|
|
4471
|
+
return out;
|
|
4472
|
+
}
|
|
4396
4473
|
/** Move a single media entry from one owner to another.
|
|
4397
4474
|
* The new copy is written before the old one is removed.
|
|
4398
4475
|
* The old-entry deletion is best-effort: on failure a warning is logged
|
|
@@ -4754,12 +4831,12 @@ var EventStore = class {
|
|
|
4754
4831
|
collection: MOTION_EVENTS_COLLECTION,
|
|
4755
4832
|
filter: this.buildFilter(q)
|
|
4756
4833
|
});
|
|
4757
|
-
if (q.projection === "slim") return rows.map((r) => slimMotion(r.id, stripNulls(r.data)));
|
|
4834
|
+
if (q.projection === "slim") return rows.map((r) => slimMotion(r.id, stripNulls$1(r.data)));
|
|
4758
4835
|
return rows.map((r) => {
|
|
4759
4836
|
return {
|
|
4760
4837
|
id: r.id,
|
|
4761
4838
|
kind: "motion",
|
|
4762
|
-
...stripNulls(r.data)
|
|
4839
|
+
...stripNulls$1(r.data)
|
|
4763
4840
|
};
|
|
4764
4841
|
});
|
|
4765
4842
|
}
|
|
@@ -4773,12 +4850,12 @@ var EventStore = class {
|
|
|
4773
4850
|
collection: OBJECT_EVENTS_COLLECTION,
|
|
4774
4851
|
filter
|
|
4775
4852
|
});
|
|
4776
|
-
if (q.projection === "slim") return rows.map((r) => slimObject(r.id, stripNulls(r.data)));
|
|
4853
|
+
if (q.projection === "slim") return rows.map((r) => slimObject(r.id, stripNulls$1(r.data)));
|
|
4777
4854
|
return rows.map((r) => {
|
|
4778
4855
|
return {
|
|
4779
4856
|
id: r.id,
|
|
4780
4857
|
kind: "object",
|
|
4781
|
-
...stripNulls(r.data)
|
|
4858
|
+
...stripNulls$1(r.data)
|
|
4782
4859
|
};
|
|
4783
4860
|
});
|
|
4784
4861
|
}
|
|
@@ -4787,12 +4864,12 @@ var EventStore = class {
|
|
|
4787
4864
|
collection: AUDIO_EVENTS_COLLECTION,
|
|
4788
4865
|
filter: this.buildFilter(q)
|
|
4789
4866
|
});
|
|
4790
|
-
if (q.projection === "slim") return rows.map((r) => slimAudio(r.id, stripNulls(r.data)));
|
|
4867
|
+
if (q.projection === "slim") return rows.map((r) => slimAudio(r.id, stripNulls$1(r.data)));
|
|
4791
4868
|
return rows.map((r) => {
|
|
4792
4869
|
return {
|
|
4793
4870
|
id: r.id,
|
|
4794
4871
|
kind: "audio",
|
|
4795
|
-
...stripNulls(r.data)
|
|
4872
|
+
...stripNulls$1(r.data)
|
|
4796
4873
|
};
|
|
4797
4874
|
});
|
|
4798
4875
|
}
|
|
@@ -4938,6 +5015,23 @@ var EventStore = class {
|
|
|
4938
5015
|
return updated;
|
|
4939
5016
|
}
|
|
4940
5017
|
/**
|
|
5018
|
+
* Total persisted event rows (motion + object + audio) for a device — the
|
|
5019
|
+
* "rows" figure in the events-management footprint. Uses the indexed `count`
|
|
5020
|
+
* aggregate per collection, run in parallel.
|
|
5021
|
+
*/
|
|
5022
|
+
async countForDevice(deviceId) {
|
|
5023
|
+
const one = (collection) => this.store.count.query({
|
|
5024
|
+
collection,
|
|
5025
|
+
filter: { where: { deviceId } }
|
|
5026
|
+
});
|
|
5027
|
+
const [motion, object, audio] = await Promise.all([
|
|
5028
|
+
one(MOTION_EVENTS_COLLECTION),
|
|
5029
|
+
one(OBJECT_EVENTS_COLLECTION),
|
|
5030
|
+
one(AUDIO_EVENTS_COLLECTION)
|
|
5031
|
+
]);
|
|
5032
|
+
return motion + object + audio;
|
|
5033
|
+
}
|
|
5034
|
+
/**
|
|
4941
5035
|
* Return per-kind event counts in equal-width time buckets.
|
|
4942
5036
|
*
|
|
4943
5037
|
* Each bucket maps to a `bucketStart = since + i * bucketMs`.
|
|
@@ -5212,12 +5306,189 @@ function bboxAreaFrac(data) {
|
|
|
5212
5306
|
if (w <= 0 || h <= 0) return 0;
|
|
5213
5307
|
return w * h / (fw * fh);
|
|
5214
5308
|
}
|
|
5215
|
-
function stripNulls(data) {
|
|
5309
|
+
function stripNulls$1(data) {
|
|
5216
5310
|
const out = {};
|
|
5217
5311
|
for (const [k, v] of Object.entries(data)) if (v !== null) out[k] = v;
|
|
5218
5312
|
return out;
|
|
5219
5313
|
}
|
|
5220
5314
|
//#endregion
|
|
5315
|
+
//#region src/pipeline-analytics/store/ops-log-store.ts
|
|
5316
|
+
/**
|
|
5317
|
+
* OpsLogStore — the EVENTS-domain operations audit for pipeline-analytics.
|
|
5318
|
+
*
|
|
5319
|
+
* pipeline-analytics already owns SQLite collections, so (unlike the recorder's
|
|
5320
|
+
* DurableState ring) the events ops-log is a DECLARED SQL-backed collection.
|
|
5321
|
+
* MUST be declared in `onInitialize` via `declare()` before the first insert —
|
|
5322
|
+
* an undeclared collection crash-loops the runner.
|
|
5323
|
+
*
|
|
5324
|
+
* Collection name: pipeline-analytics:ops-log
|
|
5325
|
+
*
|
|
5326
|
+
* Append is BEST-EFFORT (`append` catches + logs, never throws) — a failed
|
|
5327
|
+
* audit write must not fail the operation it records.
|
|
5328
|
+
*/
|
|
5329
|
+
var OPS_LOG_COLLECTION = "pipeline-analytics:ops-log";
|
|
5330
|
+
var OPS_LOG_COLUMNS = [
|
|
5331
|
+
{
|
|
5332
|
+
name: "id",
|
|
5333
|
+
type: "TEXT",
|
|
5334
|
+
primaryKey: true,
|
|
5335
|
+
notNull: true
|
|
5336
|
+
},
|
|
5337
|
+
{
|
|
5338
|
+
name: "at",
|
|
5339
|
+
type: "INTEGER",
|
|
5340
|
+
notNull: true
|
|
5341
|
+
},
|
|
5342
|
+
{
|
|
5343
|
+
name: "domain",
|
|
5344
|
+
type: "TEXT",
|
|
5345
|
+
notNull: true
|
|
5346
|
+
},
|
|
5347
|
+
{
|
|
5348
|
+
name: "op",
|
|
5349
|
+
type: "TEXT",
|
|
5350
|
+
notNull: true
|
|
5351
|
+
},
|
|
5352
|
+
{
|
|
5353
|
+
name: "reason",
|
|
5354
|
+
type: "TEXT",
|
|
5355
|
+
notNull: true
|
|
5356
|
+
},
|
|
5357
|
+
{
|
|
5358
|
+
name: "deviceId",
|
|
5359
|
+
type: "INTEGER"
|
|
5360
|
+
},
|
|
5361
|
+
{
|
|
5362
|
+
name: "nodeId",
|
|
5363
|
+
type: "TEXT",
|
|
5364
|
+
notNull: true
|
|
5365
|
+
},
|
|
5366
|
+
{
|
|
5367
|
+
name: "itemsAffected",
|
|
5368
|
+
type: "INTEGER",
|
|
5369
|
+
notNull: true
|
|
5370
|
+
},
|
|
5371
|
+
{
|
|
5372
|
+
name: "bytesReclaimed",
|
|
5373
|
+
type: "INTEGER",
|
|
5374
|
+
notNull: true
|
|
5375
|
+
},
|
|
5376
|
+
{
|
|
5377
|
+
name: "detail",
|
|
5378
|
+
type: "TEXT"
|
|
5379
|
+
},
|
|
5380
|
+
{
|
|
5381
|
+
name: "actor",
|
|
5382
|
+
type: "TEXT",
|
|
5383
|
+
notNull: true
|
|
5384
|
+
}
|
|
5385
|
+
];
|
|
5386
|
+
var OPS_LOG_INDEXES = [{
|
|
5387
|
+
name: "idx_opslog_at",
|
|
5388
|
+
columns: ["at"]
|
|
5389
|
+
}, {
|
|
5390
|
+
name: "idx_opslog_device_at",
|
|
5391
|
+
columns: ["deviceId", "at"]
|
|
5392
|
+
}];
|
|
5393
|
+
var OpsLogStore = class {
|
|
5394
|
+
store;
|
|
5395
|
+
logger;
|
|
5396
|
+
nodeId;
|
|
5397
|
+
now;
|
|
5398
|
+
newId;
|
|
5399
|
+
constructor(deps) {
|
|
5400
|
+
this.store = deps.store;
|
|
5401
|
+
this.logger = deps.logger;
|
|
5402
|
+
this.nodeId = deps.nodeId;
|
|
5403
|
+
this.now = deps.now ?? (() => Date.now());
|
|
5404
|
+
this.newId = deps.newId ?? (() => globalThis.crypto.randomUUID());
|
|
5405
|
+
}
|
|
5406
|
+
static async declare(store) {
|
|
5407
|
+
await store.declareCollection.mutate({
|
|
5408
|
+
collection: OPS_LOG_COLLECTION,
|
|
5409
|
+
columns: [...OPS_LOG_COLUMNS],
|
|
5410
|
+
indexes: [...OPS_LOG_INDEXES]
|
|
5411
|
+
});
|
|
5412
|
+
}
|
|
5413
|
+
/**
|
|
5414
|
+
* Append one events-domain ops-log row. Best-effort — stamps
|
|
5415
|
+
* `domain:'events'`, `nodeId`, `id`, `at`, validates, inserts, and swallows
|
|
5416
|
+
* (logs) any failure so it can never fail the operation being audited.
|
|
5417
|
+
*/
|
|
5418
|
+
async append(input) {
|
|
5419
|
+
const entry = {
|
|
5420
|
+
id: this.newId(),
|
|
5421
|
+
at: this.now(),
|
|
5422
|
+
domain: "events",
|
|
5423
|
+
op: input.op,
|
|
5424
|
+
reason: input.reason,
|
|
5425
|
+
deviceId: input.deviceId,
|
|
5426
|
+
nodeId: this.nodeId,
|
|
5427
|
+
itemsAffected: input.itemsAffected,
|
|
5428
|
+
bytesReclaimed: input.bytesReclaimed,
|
|
5429
|
+
detail: input.detail ?? null,
|
|
5430
|
+
actor: input.actor ?? "operator"
|
|
5431
|
+
};
|
|
5432
|
+
try {
|
|
5433
|
+
const { id, ...rest } = OpsLogEntrySchema.parse(entry);
|
|
5434
|
+
await this.store.insert.mutate({
|
|
5435
|
+
collection: OPS_LOG_COLLECTION,
|
|
5436
|
+
record: {
|
|
5437
|
+
id,
|
|
5438
|
+
data: rest
|
|
5439
|
+
}
|
|
5440
|
+
});
|
|
5441
|
+
} catch (err) {
|
|
5442
|
+
this.logger.warn("OpsLogStore.append failed (best-effort)", {
|
|
5443
|
+
tags: { deviceId: input.deviceId ?? void 0 },
|
|
5444
|
+
meta: {
|
|
5445
|
+
op: input.op,
|
|
5446
|
+
error: String(err)
|
|
5447
|
+
}
|
|
5448
|
+
});
|
|
5449
|
+
}
|
|
5450
|
+
}
|
|
5451
|
+
/**
|
|
5452
|
+
* List events ops-log rows newest-first, optionally scoped to one device,
|
|
5453
|
+
* capped at `limit` (default {@link OPS_LOG_DEFAULT_LIMIT}).
|
|
5454
|
+
*/
|
|
5455
|
+
async list(query) {
|
|
5456
|
+
const filter = {
|
|
5457
|
+
orderBy: {
|
|
5458
|
+
field: "at",
|
|
5459
|
+
direction: "desc"
|
|
5460
|
+
},
|
|
5461
|
+
limit: query.limit ?? 200
|
|
5462
|
+
};
|
|
5463
|
+
if (query.deviceId !== void 0) filter.where = { deviceId: query.deviceId };
|
|
5464
|
+
const rows = await this.store.query.query({
|
|
5465
|
+
collection: OPS_LOG_COLLECTION,
|
|
5466
|
+
filter
|
|
5467
|
+
});
|
|
5468
|
+
const out = [];
|
|
5469
|
+
for (const r of rows) {
|
|
5470
|
+
const parsed = OpsLogEntrySchema.safeParse({
|
|
5471
|
+
id: r.id,
|
|
5472
|
+
...stripNulls(r.data)
|
|
5473
|
+
});
|
|
5474
|
+
if (parsed.success) out.push(parsed.data);
|
|
5475
|
+
else this.logger.debug("OpsLogStore.list: skipped malformed row", { meta: { id: r.id } });
|
|
5476
|
+
}
|
|
5477
|
+
return out;
|
|
5478
|
+
}
|
|
5479
|
+
};
|
|
5480
|
+
/** SQLite stores nullable columns as `null`; the row schema uses `.nullable()`
|
|
5481
|
+
* for deviceId/detail (accepts null) but drop any stray null on required
|
|
5482
|
+
* fields defensively before parse. */
|
|
5483
|
+
function stripNulls(data) {
|
|
5484
|
+
const out = {};
|
|
5485
|
+
for (const [k, v] of Object.entries(data)) {
|
|
5486
|
+
if (v === null && k !== "deviceId" && k !== "detail") continue;
|
|
5487
|
+
out[k] = v;
|
|
5488
|
+
}
|
|
5489
|
+
return out;
|
|
5490
|
+
}
|
|
5491
|
+
//#endregion
|
|
5221
5492
|
//#region src/pipeline-analytics/store/sensor-event-store.ts
|
|
5222
5493
|
var SENSOR_EVENTS_COLLECTION = "pipeline-analytics:sensor-events";
|
|
5223
5494
|
var SENSOR_EVENT_COLUMNS = [
|
|
@@ -5390,114 +5661,49 @@ function isRecord(x) {
|
|
|
5390
5661
|
* linked camera ids) with a TTL, so the `DeviceStateChanged` handler stays
|
|
5391
5662
|
* cheap at bus rate.
|
|
5392
5663
|
*/
|
|
5393
|
-
var MOTION_COLOR = "#f59e0b";
|
|
5394
|
-
var AUDIO_COLOR = "#06b6d4";
|
|
5395
|
-
var PERSON_COLOR = "#22c55e";
|
|
5396
|
-
var VEHICLE_COLOR = "#3b82f6";
|
|
5397
|
-
var ANIMAL_COLOR = "#f97316";
|
|
5398
|
-
var GENERIC_DETECTION_COLOR = "#64748b";
|
|
5399
|
-
var PACKAGE_COLOR = "#a855f7";
|
|
5400
|
-
var VEHICLE_CLASSES = new Set([
|
|
5401
|
-
"vehicle",
|
|
5402
|
-
"car",
|
|
5403
|
-
"truck",
|
|
5404
|
-
"bus",
|
|
5405
|
-
"motorcycle",
|
|
5406
|
-
"bicycle",
|
|
5407
|
-
"boat",
|
|
5408
|
-
"train"
|
|
5409
|
-
]);
|
|
5410
|
-
var ANIMAL_CLASSES = new Set([
|
|
5411
|
-
"animal",
|
|
5412
|
-
"dog",
|
|
5413
|
-
"cat",
|
|
5414
|
-
"bird",
|
|
5415
|
-
"horse",
|
|
5416
|
-
"cow",
|
|
5417
|
-
"sheep"
|
|
5418
|
-
]);
|
|
5419
|
-
function detectionIcon(className) {
|
|
5420
|
-
if (className === "person") return "person";
|
|
5421
|
-
if (VEHICLE_CLASSES.has(className)) return "vehicle";
|
|
5422
|
-
if (ANIMAL_CLASSES.has(className)) return "animal";
|
|
5423
|
-
return "generic";
|
|
5424
|
-
}
|
|
5425
|
-
function detectionColor(className) {
|
|
5426
|
-
if (className === "person") return PERSON_COLOR;
|
|
5427
|
-
if (VEHICLE_CLASSES.has(className)) return VEHICLE_COLOR;
|
|
5428
|
-
if (ANIMAL_CLASSES.has(className)) return ANIMAL_COLOR;
|
|
5429
|
-
return GENERIC_DETECTION_COLOR;
|
|
5430
|
-
}
|
|
5431
|
-
function titleCase(s) {
|
|
5432
|
-
return s.length > 0 ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
5433
|
-
}
|
|
5434
5664
|
/**
|
|
5435
|
-
* Full event-kind
|
|
5436
|
-
*
|
|
5437
|
-
*
|
|
5665
|
+
* Full event-kind taxonomy a camera CAN emit (config-derived):
|
|
5666
|
+
* (a) motion built-in — always;
|
|
5667
|
+
* (b) each enabled detection macro + its taxonomy subs (reverse COCO);
|
|
5668
|
+
* (c) audio macro + its subs when the classifier is enabled;
|
|
5669
|
+
* (d) package kinds when a package zone rule is enabled;
|
|
5670
|
+
* (e) sensor/control kinds from LINKED devices (binding-driven), deduped
|
|
5671
|
+
* per (kind, source deviceId).
|
|
5672
|
+
* Every descriptor carries parentKind/level/iconId/labelKey via the single
|
|
5673
|
+
* taxonomy dictionary — no color/icon is declared here.
|
|
5438
5674
|
*/
|
|
5439
5675
|
async function composeEventKinds(deps, deviceId) {
|
|
5440
|
-
const out = [
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
source
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
}, {
|
|
5451
|
-
kind: "audio",
|
|
5452
|
-
label: "Audio",
|
|
5453
|
-
color: AUDIO_COLOR,
|
|
5454
|
-
icon: "audio",
|
|
5455
|
-
category: "audio",
|
|
5456
|
-
source: {
|
|
5457
|
-
capName: "pipeline-analytics",
|
|
5458
|
-
deviceId
|
|
5459
|
-
}
|
|
5460
|
-
}];
|
|
5676
|
+
const out = [];
|
|
5677
|
+
const cameraSource = {
|
|
5678
|
+
capName: "pipeline-analytics",
|
|
5679
|
+
deviceId
|
|
5680
|
+
};
|
|
5681
|
+
const pushKind = (kind, source = cameraSource) => {
|
|
5682
|
+
const d = buildEventKindDescriptor(kind, source);
|
|
5683
|
+
if (d !== null) out.push(d);
|
|
5684
|
+
};
|
|
5685
|
+
pushKind("motion");
|
|
5461
5686
|
try {
|
|
5462
|
-
const
|
|
5463
|
-
for (const
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
icon: detectionIcon(className),
|
|
5468
|
-
category: "detection",
|
|
5469
|
-
source: {
|
|
5470
|
-
capName: "pipeline-analytics",
|
|
5471
|
-
deviceId
|
|
5472
|
-
}
|
|
5473
|
-
});
|
|
5687
|
+
const macros = await deps.enabledMacroClasses(deviceId);
|
|
5688
|
+
for (const macro of macros) {
|
|
5689
|
+
pushKind(macro);
|
|
5690
|
+
for (const sub of subKindsOf(macro)) pushKind(sub.kind);
|
|
5691
|
+
}
|
|
5474
5692
|
} catch (err) {
|
|
5475
|
-
deps.onError?.("
|
|
5693
|
+
deps.onError?.("enabledMacroClasses", err);
|
|
5694
|
+
}
|
|
5695
|
+
try {
|
|
5696
|
+
if (await deps.audioEnabled(deviceId)) {
|
|
5697
|
+
pushKind("audio");
|
|
5698
|
+
for (const sub of subKindsOf("audio")) pushKind(sub.kind);
|
|
5699
|
+
}
|
|
5700
|
+
} catch (err) {
|
|
5701
|
+
deps.onError?.("audioEnabled", err);
|
|
5476
5702
|
}
|
|
5477
5703
|
try {
|
|
5478
5704
|
if (deps.packageZonesEnabled && await deps.packageZonesEnabled(deviceId)) {
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
label: "Package delivered",
|
|
5482
|
-
color: PACKAGE_COLOR,
|
|
5483
|
-
icon: "package",
|
|
5484
|
-
category: "package",
|
|
5485
|
-
source: {
|
|
5486
|
-
capName: "pipeline-analytics",
|
|
5487
|
-
deviceId
|
|
5488
|
-
}
|
|
5489
|
-
});
|
|
5490
|
-
out.push({
|
|
5491
|
-
kind: "package-picked-up",
|
|
5492
|
-
label: "Package picked up",
|
|
5493
|
-
color: PACKAGE_COLOR,
|
|
5494
|
-
icon: "package",
|
|
5495
|
-
category: "package",
|
|
5496
|
-
source: {
|
|
5497
|
-
capName: "pipeline-analytics",
|
|
5498
|
-
deviceId
|
|
5499
|
-
}
|
|
5500
|
-
});
|
|
5705
|
+
pushKind("package");
|
|
5706
|
+
for (const sub of subKindsOf("package")) pushKind(sub.kind);
|
|
5501
5707
|
}
|
|
5502
5708
|
} catch (err) {
|
|
5503
5709
|
deps.onError?.("packageZonesEnabled", err);
|
|
@@ -5520,16 +5726,9 @@ async function composeEventKinds(deps, deviceId) {
|
|
|
5520
5726
|
const dedupeKey = `${descriptor.kind}:${linked.deviceId}`;
|
|
5521
5727
|
if (seen.has(dedupeKey)) continue;
|
|
5522
5728
|
seen.add(dedupeKey);
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
color: descriptor.color,
|
|
5527
|
-
icon: descriptor.icon,
|
|
5528
|
-
category: descriptor.category,
|
|
5529
|
-
source: {
|
|
5530
|
-
capName,
|
|
5531
|
-
deviceId: linked.deviceId
|
|
5532
|
-
}
|
|
5729
|
+
pushKind(descriptor.kind, {
|
|
5730
|
+
capName,
|
|
5731
|
+
deviceId: linked.deviceId
|
|
5533
5732
|
});
|
|
5534
5733
|
}
|
|
5535
5734
|
}
|
|
@@ -5625,6 +5824,110 @@ async function ingestSensorStateChange(deps, data, timestamp) {
|
|
|
5625
5824
|
return inserted;
|
|
5626
5825
|
}
|
|
5627
5826
|
//#endregion
|
|
5827
|
+
//#region src/pipeline-analytics/services/synthetic-sensor-track.ts
|
|
5828
|
+
/**
|
|
5829
|
+
* Synthetic sensor/control tracks (event-kinds-taxonomy §8).
|
|
5830
|
+
*
|
|
5831
|
+
* A linked sensor/control state change is projected into the UNIFIED track
|
|
5832
|
+
* store as a SYNTHETIC track: `className` = the event kind (`contact` /
|
|
5833
|
+
* `lock` / `switch` …), `source: 'sensor'`, media = an on-demand snapshot of
|
|
5834
|
+
* the linked camera in the SAME `getTrackMedia` shape the UIs consume (no
|
|
5835
|
+
* bbox → full frame), `positions: []`. It joins the timeline via the existing
|
|
5836
|
+
* time-based clustering — no new join mechanism.
|
|
5837
|
+
*
|
|
5838
|
+
* The raw `SensorEvent` remains the durable record; this track is its
|
|
5839
|
+
* projection. Snapshots are debounced per `(camera, kind)` so a chattery
|
|
5840
|
+
* sensor doesn't hammer the snapshot cap. Spatial consumers MUST skip
|
|
5841
|
+
* `source:'sensor'` tracks (they carry no trajectory).
|
|
5842
|
+
*/
|
|
5843
|
+
var DEFAULT_DEBOUNCE_MS = 1e4;
|
|
5844
|
+
/** Full-frame, position-less placeholder (spatial consumers skip these). */
|
|
5845
|
+
function zeroPosition(timestamp) {
|
|
5846
|
+
return {
|
|
5847
|
+
x: 0,
|
|
5848
|
+
y: 0,
|
|
5849
|
+
timestamp,
|
|
5850
|
+
bbox: {
|
|
5851
|
+
x: 0,
|
|
5852
|
+
y: 0,
|
|
5853
|
+
w: 0,
|
|
5854
|
+
h: 0
|
|
5855
|
+
}
|
|
5856
|
+
};
|
|
5857
|
+
}
|
|
5858
|
+
var SyntheticSensorTrackMaterializer = class {
|
|
5859
|
+
deps;
|
|
5860
|
+
debounceMs;
|
|
5861
|
+
makeId;
|
|
5862
|
+
now;
|
|
5863
|
+
/** Last snapshot time per `${cameraId}:${kind}`. */
|
|
5864
|
+
lastAt = /* @__PURE__ */ new Map();
|
|
5865
|
+
constructor(deps) {
|
|
5866
|
+
this.deps = deps;
|
|
5867
|
+
this.debounceMs = deps.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
5868
|
+
this.makeId = deps.makeId ?? (() => `pa-synth-${randomUUID()}`);
|
|
5869
|
+
this.now = deps.now ?? Date.now;
|
|
5870
|
+
}
|
|
5871
|
+
/**
|
|
5872
|
+
* Materialize a synthetic track for a sensor/control event on `cameraId`.
|
|
5873
|
+
* Returns the persisted track, or null when debounced. Snapshot failure
|
|
5874
|
+
* still lands a track (with no media) — it never blocks the sensor record.
|
|
5875
|
+
*/
|
|
5876
|
+
async materialize(input) {
|
|
5877
|
+
const key = `${input.cameraId}:${input.kind}`;
|
|
5878
|
+
const last = this.lastAt.get(key);
|
|
5879
|
+
if (last !== void 0 && this.now() - last < this.debounceMs) return null;
|
|
5880
|
+
this.lastAt.set(key, this.now());
|
|
5881
|
+
const trackId = this.makeId();
|
|
5882
|
+
const ts = input.timestamp;
|
|
5883
|
+
let mediaKey = null;
|
|
5884
|
+
try {
|
|
5885
|
+
const snap = await this.deps.snapshot.getSnapshot({
|
|
5886
|
+
deviceId: input.cameraId,
|
|
5887
|
+
force: true
|
|
5888
|
+
});
|
|
5889
|
+
if (snap !== null) {
|
|
5890
|
+
const data = Buffer.from(snap.base64, "base64");
|
|
5891
|
+
mediaKey = await this.deps.media.put({
|
|
5892
|
+
deviceId: input.cameraId,
|
|
5893
|
+
ownerKind: "track",
|
|
5894
|
+
ownerId: trackId,
|
|
5895
|
+
kind: "snapshot",
|
|
5896
|
+
timestamp: ts,
|
|
5897
|
+
data
|
|
5898
|
+
});
|
|
5899
|
+
} else this.deps.onError?.("getSnapshot", /* @__PURE__ */ new Error("snapshot returned null"));
|
|
5900
|
+
} catch (err) {
|
|
5901
|
+
this.deps.onError?.("snapshotMedia", err);
|
|
5902
|
+
}
|
|
5903
|
+
const track = {
|
|
5904
|
+
trackId,
|
|
5905
|
+
deviceId: input.cameraId,
|
|
5906
|
+
className: input.kind,
|
|
5907
|
+
source: "sensor",
|
|
5908
|
+
firstSeen: ts,
|
|
5909
|
+
lastSeen: ts,
|
|
5910
|
+
positions: [],
|
|
5911
|
+
snapshots: mediaKey !== null ? [{
|
|
5912
|
+
timestamp: ts,
|
|
5913
|
+
position: zeroPosition(ts),
|
|
5914
|
+
mediaKey
|
|
5915
|
+
}] : [],
|
|
5916
|
+
zonesVisited: [],
|
|
5917
|
+
totalDistance: 0,
|
|
5918
|
+
state: "idle",
|
|
5919
|
+
active: false
|
|
5920
|
+
};
|
|
5921
|
+
try {
|
|
5922
|
+
await this.deps.tracks.persistSyntheticTrack(track);
|
|
5923
|
+
} catch (err) {
|
|
5924
|
+
this.deps.onError?.("persistSyntheticTrack", err);
|
|
5925
|
+
return null;
|
|
5926
|
+
}
|
|
5927
|
+
return track;
|
|
5928
|
+
}
|
|
5929
|
+
};
|
|
5930
|
+
//#endregion
|
|
5628
5931
|
//#region src/shared/frame/resolve-frame.ts
|
|
5629
5932
|
/**
|
|
5630
5933
|
* Resolve the pixels a `FrameHandle` refers to via the node-routed fetch.
|
|
@@ -5672,7 +5975,7 @@ function squareSafeCropRegion(bbox, frame, padding) {
|
|
|
5672
5975
|
}
|
|
5673
5976
|
//#endregion
|
|
5674
5977
|
//#region src/shared/frame/box-drawer.ts
|
|
5675
|
-
var DEFAULT_COLOR =
|
|
5978
|
+
var DEFAULT_COLOR = DEFAULT_EVENT_COLOR;
|
|
5676
5979
|
var DEFAULT_QUALITY = 80;
|
|
5677
5980
|
var STROKE_WIDTH = 3;
|
|
5678
5981
|
function escapeXml(s) {
|
|
@@ -10959,11 +11262,15 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
10959
11262
|
stationaryRegistry = null;
|
|
10960
11263
|
mediaStore = null;
|
|
10961
11264
|
eventStore = null;
|
|
11265
|
+
/** Events-domain ops-log (footprint/prune/manual-delete audit). Null until onInitialize. */
|
|
11266
|
+
eventsOpsLog = null;
|
|
10962
11267
|
/** Per-camera history of LINKED-device sensor state changes (Part B). */
|
|
10963
11268
|
sensorEventStore = null;
|
|
10964
11269
|
/** Ingest-side reverse index (sensor device → linked camera ids), TTL-cached
|
|
10965
11270
|
* so the DeviceStateChanged handler stays cheap. */
|
|
10966
11271
|
linkedCamerasCache = null;
|
|
11272
|
+
/** Projects sensor/control events into synthetic snapshot-carrying tracks. */
|
|
11273
|
+
syntheticSensorTracks = null;
|
|
10967
11274
|
identityStore = null;
|
|
10968
11275
|
faceStore = null;
|
|
10969
11276
|
faceRecognizer = null;
|
|
@@ -11121,6 +11428,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
11121
11428
|
await VehicleStore.declare(api.settingsStore);
|
|
11122
11429
|
await ObjectEmbeddingStore.declare(api.settingsStore);
|
|
11123
11430
|
await StationaryObjectRegistry.declare(api.settingsStore);
|
|
11431
|
+
await OpsLogStore.declare(api.settingsStore);
|
|
11124
11432
|
const logger = this.ctx.logger;
|
|
11125
11433
|
let storage = this.ctx.kernel.storage;
|
|
11126
11434
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
@@ -11130,11 +11438,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
11130
11438
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
11131
11439
|
}
|
|
11132
11440
|
if (!storage) throw new Error("pipeline-analytics requires ctx.kernel.storage");
|
|
11133
|
-
|
|
11441
|
+
const trackStore = new TrackStore({
|
|
11134
11442
|
store: api.settingsStore,
|
|
11135
11443
|
logger: logger.child("TrackStore"),
|
|
11136
11444
|
frameDims: (deviceId) => this.lastFrameDimsByDevice.get(deviceId)
|
|
11137
11445
|
});
|
|
11446
|
+
this.trackStore = trackStore;
|
|
11138
11447
|
this.stationaryRegistry = new StationaryObjectRegistry({
|
|
11139
11448
|
store: api.settingsStore,
|
|
11140
11449
|
logger: logger.child("StationaryRegistry"),
|
|
@@ -11164,11 +11473,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
11164
11473
|
}
|
|
11165
11474
|
});
|
|
11166
11475
|
await this.stationaryRegistry.load();
|
|
11167
|
-
|
|
11476
|
+
const mediaStore = new MediaStore({
|
|
11168
11477
|
storage,
|
|
11169
11478
|
store: api.settingsStore,
|
|
11170
11479
|
logger: logger.child("MediaStore")
|
|
11171
11480
|
});
|
|
11481
|
+
this.mediaStore = mediaStore;
|
|
11172
11482
|
const eventStore = new EventStore({
|
|
11173
11483
|
store: api.settingsStore,
|
|
11174
11484
|
logger: logger.child("EventStore"),
|
|
@@ -11228,6 +11538,15 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
11228
11538
|
error: errMsg(err)
|
|
11229
11539
|
} })
|
|
11230
11540
|
});
|
|
11541
|
+
this.syntheticSensorTracks = new SyntheticSensorTrackMaterializer({
|
|
11542
|
+
snapshot: { getSnapshot: (i) => api.snapshot.getSnapshot.query(i) },
|
|
11543
|
+
media: mediaStore,
|
|
11544
|
+
tracks: trackStore,
|
|
11545
|
+
onError: (scope, err) => logger.warn("synthetic sensor-track error", { meta: {
|
|
11546
|
+
scope,
|
|
11547
|
+
error: errMsg(err)
|
|
11548
|
+
} })
|
|
11549
|
+
});
|
|
11231
11550
|
this.identityStore = new IdentityStore({
|
|
11232
11551
|
store: api.settingsStore,
|
|
11233
11552
|
logger: logger.child("IdentityStore")
|
|
@@ -11246,6 +11565,11 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
11246
11565
|
});
|
|
11247
11566
|
const rawNodeId = this.ctx.kernel.localNodeId ?? this.ctx.id;
|
|
11248
11567
|
const ownNodeId = rawNodeId.includes("/") ? rawNodeId.split("/")[0] : rawNodeId;
|
|
11568
|
+
this.eventsOpsLog = new OpsLogStore({
|
|
11569
|
+
store: api.settingsStore,
|
|
11570
|
+
logger: logger.child("ops-log"),
|
|
11571
|
+
nodeId: ownNodeId
|
|
11572
|
+
});
|
|
11249
11573
|
{
|
|
11250
11574
|
const designated = await this.postProcessingNodeState.get();
|
|
11251
11575
|
this.isPostProcessingNode = ownNodeId === designated;
|
|
@@ -13460,10 +13784,21 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13460
13784
|
*/
|
|
13461
13785
|
async listEventKinds(input) {
|
|
13462
13786
|
const api = this.ctx.api;
|
|
13787
|
+
const status = await api.pipelineOrchestrator.getCameraStatus.query({ deviceId: input.deviceId }).catch((err) => {
|
|
13788
|
+
this.ctx.logger.warn("listEventKinds: getCameraStatus failed", {
|
|
13789
|
+
tags: { deviceId: input.deviceId },
|
|
13790
|
+
meta: { error: errMsg(err) }
|
|
13791
|
+
});
|
|
13792
|
+
return null;
|
|
13793
|
+
});
|
|
13794
|
+
const detectionOn = status?.detection != null;
|
|
13795
|
+
const audioOn = status?.audio?.enabled === true;
|
|
13796
|
+
const detectionMacros = MACRO_LABELS.map((l) => l.id).filter((id) => id !== "package");
|
|
13463
13797
|
return composeEventKinds({
|
|
13464
13798
|
linkedDevices: { getLinkedDevices: (i) => api.deviceManager.getLinkedDevices.query(i) },
|
|
13465
13799
|
bindings: { getBindings: (i) => api.deviceManager.getBindings.query(i) },
|
|
13466
|
-
|
|
13800
|
+
enabledMacroClasses: async () => detectionOn ? detectionMacros : [],
|
|
13801
|
+
audioEnabled: async () => audioOn,
|
|
13467
13802
|
packageZonesEnabled: async (deviceId) => {
|
|
13468
13803
|
return (await this.resolveDevicePackageRules(deviceId)).some((r) => r.enabled !== false);
|
|
13469
13804
|
},
|
|
@@ -13503,6 +13838,26 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13503
13838
|
}
|
|
13504
13839
|
});
|
|
13505
13840
|
}
|
|
13841
|
+
const materializer = this.syntheticSensorTracks;
|
|
13842
|
+
const descriptor = EVENT_KIND_BY_CAP[data.capName];
|
|
13843
|
+
if (materializer === null || descriptor === void 0) return;
|
|
13844
|
+
try {
|
|
13845
|
+
const cameraIds = await cache.camerasFor(data.deviceId);
|
|
13846
|
+
for (const cameraId of cameraIds) await materializer.materialize({
|
|
13847
|
+
cameraId,
|
|
13848
|
+
sourceDeviceId: data.deviceId,
|
|
13849
|
+
kind: descriptor.kind,
|
|
13850
|
+
timestamp
|
|
13851
|
+
});
|
|
13852
|
+
} catch (err) {
|
|
13853
|
+
this.ctx.logger.warn("synthetic sensor-track materialize failed", {
|
|
13854
|
+
tags: { deviceId: data.deviceId },
|
|
13855
|
+
meta: {
|
|
13856
|
+
capName: data.capName,
|
|
13857
|
+
error: errMsg(err)
|
|
13858
|
+
}
|
|
13859
|
+
});
|
|
13860
|
+
}
|
|
13506
13861
|
}
|
|
13507
13862
|
async clearTracks(input) {
|
|
13508
13863
|
this.trackStore?.clearDevice(input.deviceId);
|
|
@@ -13751,6 +14106,120 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13751
14106
|
};
|
|
13752
14107
|
}
|
|
13753
14108
|
/**
|
|
14109
|
+
* Durable event-store footprint for the events-management UI: event ROWS
|
|
14110
|
+
* (motion + object + audio) counted per camera (indexed `count`) + total, and
|
|
14111
|
+
* event-owned media BYTES on disk per camera (paged `sizeBytes` sum) + total.
|
|
14112
|
+
* The device set is the union of cameras that have media OR persisted tracks;
|
|
14113
|
+
* cameras with neither rows nor bytes are omitted.
|
|
14114
|
+
*/
|
|
14115
|
+
async getEventStoreFootprint() {
|
|
14116
|
+
const eventStore = this.eventStore;
|
|
14117
|
+
const mediaStore = this.mediaStore;
|
|
14118
|
+
if (!eventStore || !mediaStore) return {
|
|
14119
|
+
totalRows: 0,
|
|
14120
|
+
totalBytes: 0,
|
|
14121
|
+
devices: []
|
|
14122
|
+
};
|
|
14123
|
+
const mediaByDevice = await mediaStore.footprintByDevice();
|
|
14124
|
+
const deviceIds = new Set(mediaByDevice.keys());
|
|
14125
|
+
if (this.trackStore) for (const id of await this.trackStore.listDeviceIds()) deviceIds.add(id);
|
|
14126
|
+
const devices = [];
|
|
14127
|
+
let totalRows = 0;
|
|
14128
|
+
let totalBytes = 0;
|
|
14129
|
+
for (const deviceId of deviceIds) {
|
|
14130
|
+
const rows = await eventStore.countForDevice(deviceId);
|
|
14131
|
+
const bytes = mediaByDevice.get(deviceId)?.bytes ?? 0;
|
|
14132
|
+
if (rows === 0 && bytes === 0) continue;
|
|
14133
|
+
totalRows += rows;
|
|
14134
|
+
totalBytes += bytes;
|
|
14135
|
+
devices.push({
|
|
14136
|
+
deviceId,
|
|
14137
|
+
rows,
|
|
14138
|
+
bytes
|
|
14139
|
+
});
|
|
14140
|
+
}
|
|
14141
|
+
devices.sort((a, b) => b.bytes - a.bytes);
|
|
14142
|
+
return {
|
|
14143
|
+
totalRows,
|
|
14144
|
+
totalBytes,
|
|
14145
|
+
devices
|
|
14146
|
+
};
|
|
14147
|
+
}
|
|
14148
|
+
/**
|
|
14149
|
+
* Cluster-wide prune of events with `timestamp < olderThanMs` across every
|
|
14150
|
+
* camera (via the device-agnostic `evictBefore`), deleting each pruned event's
|
|
14151
|
+
* media in lockstep. Logs one global (`deviceId:null`) events ops-log row.
|
|
14152
|
+
* `bytesReclaimed` is 0 (media deletion does not surface freed bytes).
|
|
14153
|
+
*/
|
|
14154
|
+
async pruneEvents(input) {
|
|
14155
|
+
const eventStore = this.eventStore;
|
|
14156
|
+
if (!eventStore) return {
|
|
14157
|
+
motion: 0,
|
|
14158
|
+
object: 0,
|
|
14159
|
+
audio: 0
|
|
14160
|
+
};
|
|
14161
|
+
const cutoff = input.olderThanMs;
|
|
14162
|
+
const evicted = await eventStore.evictBefore({
|
|
14163
|
+
motionCutoffMs: cutoff,
|
|
14164
|
+
objectCutoffMs: cutoff,
|
|
14165
|
+
audioCutoffMs: cutoff
|
|
14166
|
+
});
|
|
14167
|
+
const ids = [
|
|
14168
|
+
...evicted.motion,
|
|
14169
|
+
...evicted.object,
|
|
14170
|
+
...evicted.audio
|
|
14171
|
+
];
|
|
14172
|
+
if (ids.length > 0 && this.mediaStore) await this.mediaStore.deleteForEvents(ids);
|
|
14173
|
+
const counts = {
|
|
14174
|
+
motion: evicted.motion.length,
|
|
14175
|
+
object: evicted.object.length,
|
|
14176
|
+
audio: evicted.audio.length
|
|
14177
|
+
};
|
|
14178
|
+
const total = counts.motion + counts.object + counts.audio;
|
|
14179
|
+
await this.eventsOpsLog?.append({
|
|
14180
|
+
op: "prune",
|
|
14181
|
+
reason: input.reason ?? "retention",
|
|
14182
|
+
deviceId: null,
|
|
14183
|
+
itemsAffected: total,
|
|
14184
|
+
bytesReclaimed: 0,
|
|
14185
|
+
detail: `pruned events older than ${cutoff}`
|
|
14186
|
+
});
|
|
14187
|
+
if (total > 0) this.ctx.logger.info("analytics events pruned (cluster)", { meta: {
|
|
14188
|
+
cutoffMs: cutoff,
|
|
14189
|
+
...counts
|
|
14190
|
+
} });
|
|
14191
|
+
return counts;
|
|
14192
|
+
}
|
|
14193
|
+
/**
|
|
14194
|
+
* Manually delete EVERY event (motion + object + audio) for one camera and its
|
|
14195
|
+
* event-owned media in lockstep. Logs a manual events ops-log row.
|
|
14196
|
+
*/
|
|
14197
|
+
async deleteDeviceEvents(input) {
|
|
14198
|
+
if (!this.eventStore) return {
|
|
14199
|
+
motion: 0,
|
|
14200
|
+
object: 0,
|
|
14201
|
+
audio: 0
|
|
14202
|
+
};
|
|
14203
|
+
const counts = await this.pruneEventsBefore({
|
|
14204
|
+
deviceId: input.deviceId,
|
|
14205
|
+
cutoffMs: Number.MAX_SAFE_INTEGER
|
|
14206
|
+
});
|
|
14207
|
+
const total = counts.motion + counts.object + counts.audio;
|
|
14208
|
+
await this.eventsOpsLog?.append({
|
|
14209
|
+
op: "manual-delete",
|
|
14210
|
+
reason: "manual",
|
|
14211
|
+
deviceId: input.deviceId,
|
|
14212
|
+
itemsAffected: total,
|
|
14213
|
+
bytesReclaimed: 0,
|
|
14214
|
+
detail: "deleted all events for device"
|
|
14215
|
+
});
|
|
14216
|
+
return counts;
|
|
14217
|
+
}
|
|
14218
|
+
/** The events ops-log rows (newest-first), optionally scoped to one camera. */
|
|
14219
|
+
async listOpsLog(input) {
|
|
14220
|
+
return await this.eventsOpsLog?.list(input) ?? [];
|
|
14221
|
+
}
|
|
14222
|
+
/**
|
|
13754
14223
|
* Track-centric time-based retention (design §5.1). Drains every persisted
|
|
13755
14224
|
* track for the device whose `lastSeen < cutoffMs`, page by page, through the
|
|
13756
14225
|
* widened cascade — enrolled faces/plates + identity media are exempt (design
|