@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
|
@@ -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-
|
|
5
|
+
const require_dist = require("../dist-Bc4L7FGf.js");
|
|
6
6
|
let node_crypto = require("node:crypto");
|
|
7
7
|
let sharp = require("sharp");
|
|
8
8
|
sharp = require_dist.__toESM(sharp);
|
|
@@ -165,13 +165,13 @@ var CLASS_RANK_VEHICLE = .8;
|
|
|
165
165
|
var CLASS_RANK_ANIMAL = .5;
|
|
166
166
|
var CLASS_RANK_DEFAULT = .25;
|
|
167
167
|
var PERSON_CLASSES = new Set(["person", "face"]);
|
|
168
|
-
var VEHICLE_CLASSES
|
|
168
|
+
var VEHICLE_CLASSES = new Set([
|
|
169
169
|
"vehicle",
|
|
170
170
|
"car",
|
|
171
171
|
"truck",
|
|
172
172
|
"bus"
|
|
173
173
|
]);
|
|
174
|
-
var ANIMAL_CLASSES
|
|
174
|
+
var ANIMAL_CLASSES = new Set([
|
|
175
175
|
"animal",
|
|
176
176
|
"dog",
|
|
177
177
|
"cat"
|
|
@@ -187,8 +187,8 @@ function clamp01(x) {
|
|
|
187
187
|
function classRank(className) {
|
|
188
188
|
const c = className.toLowerCase();
|
|
189
189
|
if (PERSON_CLASSES.has(c)) return 1;
|
|
190
|
-
if (VEHICLE_CLASSES
|
|
191
|
-
if (ANIMAL_CLASSES
|
|
190
|
+
if (VEHICLE_CLASSES.has(c)) return CLASS_RANK_VEHICLE;
|
|
191
|
+
if (ANIMAL_CLASSES.has(c)) return CLASS_RANK_ANIMAL;
|
|
192
192
|
return CLASS_RANK_DEFAULT;
|
|
193
193
|
}
|
|
194
194
|
/**
|
|
@@ -3319,6 +3319,10 @@ var TRACKS_COLUMNS = [
|
|
|
3319
3319
|
name: "label",
|
|
3320
3320
|
type: "TEXT"
|
|
3321
3321
|
},
|
|
3322
|
+
{
|
|
3323
|
+
name: "source",
|
|
3324
|
+
type: "TEXT"
|
|
3325
|
+
},
|
|
3322
3326
|
{
|
|
3323
3327
|
name: "firstSeen",
|
|
3324
3328
|
type: "INTEGER",
|
|
@@ -4009,6 +4013,34 @@ var TrackStore = class {
|
|
|
4009
4013
|
const row = records[0];
|
|
4010
4014
|
return this.rowToTrack(row.id, row.data);
|
|
4011
4015
|
}
|
|
4016
|
+
/**
|
|
4017
|
+
* Persist a SYNTHETIC track (a sensor/control event projected into the
|
|
4018
|
+
* unified track store). Unlike `persistCompleted` there is no live in-RAM
|
|
4019
|
+
* state and no trajectory envelope (synthetic tracks carry no positions) —
|
|
4020
|
+
* the row is written directly. `source: 'sensor'` marks it so spatial
|
|
4021
|
+
* consumers skip it; the existing time-based clustering joins it by
|
|
4022
|
+
* timestamp like any other track.
|
|
4023
|
+
*/
|
|
4024
|
+
async persistSyntheticTrack(t) {
|
|
4025
|
+
await this.store.set.mutate({
|
|
4026
|
+
collection: TRACKS_COLLECTION,
|
|
4027
|
+
key: t.trackId,
|
|
4028
|
+
value: {
|
|
4029
|
+
deviceId: t.deviceId,
|
|
4030
|
+
className: t.className,
|
|
4031
|
+
...t.label !== void 0 ? { label: t.label } : {},
|
|
4032
|
+
source: t.source ?? "sensor",
|
|
4033
|
+
firstSeen: t.firstSeen,
|
|
4034
|
+
lastSeen: t.lastSeen,
|
|
4035
|
+
positions: [...t.positions],
|
|
4036
|
+
snapshots: [...t.snapshots],
|
|
4037
|
+
zonesVisited: [...t.zonesVisited],
|
|
4038
|
+
...t.classes !== void 0 ? { classes: [...t.classes] } : {},
|
|
4039
|
+
totalDistance: t.totalDistance,
|
|
4040
|
+
state: t.state
|
|
4041
|
+
}
|
|
4042
|
+
});
|
|
4043
|
+
}
|
|
4012
4044
|
async persistCompleted(t) {
|
|
4013
4045
|
const dims = this.frameDims?.(t.deviceId);
|
|
4014
4046
|
const envelope = computeTrackEnvelope(t.positions, dims?.w, dims?.h);
|
|
@@ -4019,6 +4051,7 @@ var TrackStore = class {
|
|
|
4019
4051
|
deviceId: t.deviceId,
|
|
4020
4052
|
className: t.className,
|
|
4021
4053
|
...t.label !== void 0 ? { label: t.label } : {},
|
|
4054
|
+
...t.source !== void 0 ? { source: t.source } : {},
|
|
4022
4055
|
firstSeen: t.firstSeen,
|
|
4023
4056
|
lastSeen: t.lastSeen,
|
|
4024
4057
|
positions: [...t.positions],
|
|
@@ -4057,6 +4090,7 @@ var TrackStore = class {
|
|
|
4057
4090
|
const zones = data["zonesVisited"] ?? [];
|
|
4058
4091
|
const classes = data["classes"];
|
|
4059
4092
|
const label = data["label"];
|
|
4093
|
+
const source = data["source"];
|
|
4060
4094
|
const importance = data["importance"];
|
|
4061
4095
|
const bestEventId = data["bestEventId"];
|
|
4062
4096
|
const importanceReason = data["importanceReason"];
|
|
@@ -4076,6 +4110,7 @@ var TrackStore = class {
|
|
|
4076
4110
|
deviceId: Number(data["deviceId"]),
|
|
4077
4111
|
className: String(data["className"]),
|
|
4078
4112
|
...typeof label === "string" ? { label } : {},
|
|
4113
|
+
...source === "sensor" || source === "pipeline" ? { source } : {},
|
|
4079
4114
|
firstSeen: Number(data["firstSeen"]),
|
|
4080
4115
|
lastSeen: Number(data["lastSeen"]),
|
|
4081
4116
|
positions,
|
|
@@ -4398,6 +4433,48 @@ var MediaStore = class {
|
|
|
4398
4433
|
}
|
|
4399
4434
|
return removed;
|
|
4400
4435
|
}
|
|
4436
|
+
/**
|
|
4437
|
+
* On-demand footprint aggregation for the events-management UI: sum
|
|
4438
|
+
* `sizeBytes` and count rows per device across the whole media collection.
|
|
4439
|
+
* Pages by `id` (offset-based) so a large collection never loads at once.
|
|
4440
|
+
* Metadata rows are small (no blob) — the image bytes live on the storage
|
|
4441
|
+
* provider and are counted via the persisted `sizeBytes` column.
|
|
4442
|
+
*/
|
|
4443
|
+
async footprintByDevice() {
|
|
4444
|
+
const PAGE = 1e3;
|
|
4445
|
+
const out = /* @__PURE__ */ new Map();
|
|
4446
|
+
let offset = 0;
|
|
4447
|
+
for (;;) {
|
|
4448
|
+
const rows = await this.store.query.query({
|
|
4449
|
+
collection: MEDIA_COLLECTION,
|
|
4450
|
+
filter: {
|
|
4451
|
+
orderBy: {
|
|
4452
|
+
field: "id",
|
|
4453
|
+
direction: "asc"
|
|
4454
|
+
},
|
|
4455
|
+
limit: PAGE,
|
|
4456
|
+
offset
|
|
4457
|
+
}
|
|
4458
|
+
});
|
|
4459
|
+
if (rows.length === 0) break;
|
|
4460
|
+
for (const row of rows) {
|
|
4461
|
+
const data = row.data;
|
|
4462
|
+
const deviceId = Number(data["deviceId"]);
|
|
4463
|
+
const sizeBytes = Number(data["sizeBytes"]);
|
|
4464
|
+
if (!Number.isFinite(deviceId)) continue;
|
|
4465
|
+
const acc = out.get(deviceId) ?? {
|
|
4466
|
+
bytes: 0,
|
|
4467
|
+
rows: 0
|
|
4468
|
+
};
|
|
4469
|
+
acc.bytes += Number.isFinite(sizeBytes) ? sizeBytes : 0;
|
|
4470
|
+
acc.rows += 1;
|
|
4471
|
+
out.set(deviceId, acc);
|
|
4472
|
+
}
|
|
4473
|
+
if (rows.length < PAGE) break;
|
|
4474
|
+
offset += PAGE;
|
|
4475
|
+
}
|
|
4476
|
+
return out;
|
|
4477
|
+
}
|
|
4401
4478
|
/** Move a single media entry from one owner to another.
|
|
4402
4479
|
* The new copy is written before the old one is removed.
|
|
4403
4480
|
* The old-entry deletion is best-effort: on failure a warning is logged
|
|
@@ -4759,12 +4836,12 @@ var EventStore = class {
|
|
|
4759
4836
|
collection: MOTION_EVENTS_COLLECTION,
|
|
4760
4837
|
filter: this.buildFilter(q)
|
|
4761
4838
|
});
|
|
4762
|
-
if (q.projection === "slim") return rows.map((r) => slimMotion(r.id, stripNulls(r.data)));
|
|
4839
|
+
if (q.projection === "slim") return rows.map((r) => slimMotion(r.id, stripNulls$1(r.data)));
|
|
4763
4840
|
return rows.map((r) => {
|
|
4764
4841
|
return {
|
|
4765
4842
|
id: r.id,
|
|
4766
4843
|
kind: "motion",
|
|
4767
|
-
...stripNulls(r.data)
|
|
4844
|
+
...stripNulls$1(r.data)
|
|
4768
4845
|
};
|
|
4769
4846
|
});
|
|
4770
4847
|
}
|
|
@@ -4778,12 +4855,12 @@ var EventStore = class {
|
|
|
4778
4855
|
collection: OBJECT_EVENTS_COLLECTION,
|
|
4779
4856
|
filter
|
|
4780
4857
|
});
|
|
4781
|
-
if (q.projection === "slim") return rows.map((r) => slimObject(r.id, stripNulls(r.data)));
|
|
4858
|
+
if (q.projection === "slim") return rows.map((r) => slimObject(r.id, stripNulls$1(r.data)));
|
|
4782
4859
|
return rows.map((r) => {
|
|
4783
4860
|
return {
|
|
4784
4861
|
id: r.id,
|
|
4785
4862
|
kind: "object",
|
|
4786
|
-
...stripNulls(r.data)
|
|
4863
|
+
...stripNulls$1(r.data)
|
|
4787
4864
|
};
|
|
4788
4865
|
});
|
|
4789
4866
|
}
|
|
@@ -4792,12 +4869,12 @@ var EventStore = class {
|
|
|
4792
4869
|
collection: AUDIO_EVENTS_COLLECTION,
|
|
4793
4870
|
filter: this.buildFilter(q)
|
|
4794
4871
|
});
|
|
4795
|
-
if (q.projection === "slim") return rows.map((r) => slimAudio(r.id, stripNulls(r.data)));
|
|
4872
|
+
if (q.projection === "slim") return rows.map((r) => slimAudio(r.id, stripNulls$1(r.data)));
|
|
4796
4873
|
return rows.map((r) => {
|
|
4797
4874
|
return {
|
|
4798
4875
|
id: r.id,
|
|
4799
4876
|
kind: "audio",
|
|
4800
|
-
...stripNulls(r.data)
|
|
4877
|
+
...stripNulls$1(r.data)
|
|
4801
4878
|
};
|
|
4802
4879
|
});
|
|
4803
4880
|
}
|
|
@@ -4943,6 +5020,23 @@ var EventStore = class {
|
|
|
4943
5020
|
return updated;
|
|
4944
5021
|
}
|
|
4945
5022
|
/**
|
|
5023
|
+
* Total persisted event rows (motion + object + audio) for a device — the
|
|
5024
|
+
* "rows" figure in the events-management footprint. Uses the indexed `count`
|
|
5025
|
+
* aggregate per collection, run in parallel.
|
|
5026
|
+
*/
|
|
5027
|
+
async countForDevice(deviceId) {
|
|
5028
|
+
const one = (collection) => this.store.count.query({
|
|
5029
|
+
collection,
|
|
5030
|
+
filter: { where: { deviceId } }
|
|
5031
|
+
});
|
|
5032
|
+
const [motion, object, audio] = await Promise.all([
|
|
5033
|
+
one(MOTION_EVENTS_COLLECTION),
|
|
5034
|
+
one(OBJECT_EVENTS_COLLECTION),
|
|
5035
|
+
one(AUDIO_EVENTS_COLLECTION)
|
|
5036
|
+
]);
|
|
5037
|
+
return motion + object + audio;
|
|
5038
|
+
}
|
|
5039
|
+
/**
|
|
4946
5040
|
* Return per-kind event counts in equal-width time buckets.
|
|
4947
5041
|
*
|
|
4948
5042
|
* Each bucket maps to a `bucketStart = since + i * bucketMs`.
|
|
@@ -5217,12 +5311,189 @@ function bboxAreaFrac(data) {
|
|
|
5217
5311
|
if (w <= 0 || h <= 0) return 0;
|
|
5218
5312
|
return w * h / (fw * fh);
|
|
5219
5313
|
}
|
|
5220
|
-
function stripNulls(data) {
|
|
5314
|
+
function stripNulls$1(data) {
|
|
5221
5315
|
const out = {};
|
|
5222
5316
|
for (const [k, v] of Object.entries(data)) if (v !== null) out[k] = v;
|
|
5223
5317
|
return out;
|
|
5224
5318
|
}
|
|
5225
5319
|
//#endregion
|
|
5320
|
+
//#region src/pipeline-analytics/store/ops-log-store.ts
|
|
5321
|
+
/**
|
|
5322
|
+
* OpsLogStore — the EVENTS-domain operations audit for pipeline-analytics.
|
|
5323
|
+
*
|
|
5324
|
+
* pipeline-analytics already owns SQLite collections, so (unlike the recorder's
|
|
5325
|
+
* DurableState ring) the events ops-log is a DECLARED SQL-backed collection.
|
|
5326
|
+
* MUST be declared in `onInitialize` via `declare()` before the first insert —
|
|
5327
|
+
* an undeclared collection crash-loops the runner.
|
|
5328
|
+
*
|
|
5329
|
+
* Collection name: pipeline-analytics:ops-log
|
|
5330
|
+
*
|
|
5331
|
+
* Append is BEST-EFFORT (`append` catches + logs, never throws) — a failed
|
|
5332
|
+
* audit write must not fail the operation it records.
|
|
5333
|
+
*/
|
|
5334
|
+
var OPS_LOG_COLLECTION = "pipeline-analytics:ops-log";
|
|
5335
|
+
var OPS_LOG_COLUMNS = [
|
|
5336
|
+
{
|
|
5337
|
+
name: "id",
|
|
5338
|
+
type: "TEXT",
|
|
5339
|
+
primaryKey: true,
|
|
5340
|
+
notNull: true
|
|
5341
|
+
},
|
|
5342
|
+
{
|
|
5343
|
+
name: "at",
|
|
5344
|
+
type: "INTEGER",
|
|
5345
|
+
notNull: true
|
|
5346
|
+
},
|
|
5347
|
+
{
|
|
5348
|
+
name: "domain",
|
|
5349
|
+
type: "TEXT",
|
|
5350
|
+
notNull: true
|
|
5351
|
+
},
|
|
5352
|
+
{
|
|
5353
|
+
name: "op",
|
|
5354
|
+
type: "TEXT",
|
|
5355
|
+
notNull: true
|
|
5356
|
+
},
|
|
5357
|
+
{
|
|
5358
|
+
name: "reason",
|
|
5359
|
+
type: "TEXT",
|
|
5360
|
+
notNull: true
|
|
5361
|
+
},
|
|
5362
|
+
{
|
|
5363
|
+
name: "deviceId",
|
|
5364
|
+
type: "INTEGER"
|
|
5365
|
+
},
|
|
5366
|
+
{
|
|
5367
|
+
name: "nodeId",
|
|
5368
|
+
type: "TEXT",
|
|
5369
|
+
notNull: true
|
|
5370
|
+
},
|
|
5371
|
+
{
|
|
5372
|
+
name: "itemsAffected",
|
|
5373
|
+
type: "INTEGER",
|
|
5374
|
+
notNull: true
|
|
5375
|
+
},
|
|
5376
|
+
{
|
|
5377
|
+
name: "bytesReclaimed",
|
|
5378
|
+
type: "INTEGER",
|
|
5379
|
+
notNull: true
|
|
5380
|
+
},
|
|
5381
|
+
{
|
|
5382
|
+
name: "detail",
|
|
5383
|
+
type: "TEXT"
|
|
5384
|
+
},
|
|
5385
|
+
{
|
|
5386
|
+
name: "actor",
|
|
5387
|
+
type: "TEXT",
|
|
5388
|
+
notNull: true
|
|
5389
|
+
}
|
|
5390
|
+
];
|
|
5391
|
+
var OPS_LOG_INDEXES = [{
|
|
5392
|
+
name: "idx_opslog_at",
|
|
5393
|
+
columns: ["at"]
|
|
5394
|
+
}, {
|
|
5395
|
+
name: "idx_opslog_device_at",
|
|
5396
|
+
columns: ["deviceId", "at"]
|
|
5397
|
+
}];
|
|
5398
|
+
var OpsLogStore = class {
|
|
5399
|
+
store;
|
|
5400
|
+
logger;
|
|
5401
|
+
nodeId;
|
|
5402
|
+
now;
|
|
5403
|
+
newId;
|
|
5404
|
+
constructor(deps) {
|
|
5405
|
+
this.store = deps.store;
|
|
5406
|
+
this.logger = deps.logger;
|
|
5407
|
+
this.nodeId = deps.nodeId;
|
|
5408
|
+
this.now = deps.now ?? (() => Date.now());
|
|
5409
|
+
this.newId = deps.newId ?? (() => globalThis.crypto.randomUUID());
|
|
5410
|
+
}
|
|
5411
|
+
static async declare(store) {
|
|
5412
|
+
await store.declareCollection.mutate({
|
|
5413
|
+
collection: OPS_LOG_COLLECTION,
|
|
5414
|
+
columns: [...OPS_LOG_COLUMNS],
|
|
5415
|
+
indexes: [...OPS_LOG_INDEXES]
|
|
5416
|
+
});
|
|
5417
|
+
}
|
|
5418
|
+
/**
|
|
5419
|
+
* Append one events-domain ops-log row. Best-effort — stamps
|
|
5420
|
+
* `domain:'events'`, `nodeId`, `id`, `at`, validates, inserts, and swallows
|
|
5421
|
+
* (logs) any failure so it can never fail the operation being audited.
|
|
5422
|
+
*/
|
|
5423
|
+
async append(input) {
|
|
5424
|
+
const entry = {
|
|
5425
|
+
id: this.newId(),
|
|
5426
|
+
at: this.now(),
|
|
5427
|
+
domain: "events",
|
|
5428
|
+
op: input.op,
|
|
5429
|
+
reason: input.reason,
|
|
5430
|
+
deviceId: input.deviceId,
|
|
5431
|
+
nodeId: this.nodeId,
|
|
5432
|
+
itemsAffected: input.itemsAffected,
|
|
5433
|
+
bytesReclaimed: input.bytesReclaimed,
|
|
5434
|
+
detail: input.detail ?? null,
|
|
5435
|
+
actor: input.actor ?? "operator"
|
|
5436
|
+
};
|
|
5437
|
+
try {
|
|
5438
|
+
const { id, ...rest } = require_dist.OpsLogEntrySchema.parse(entry);
|
|
5439
|
+
await this.store.insert.mutate({
|
|
5440
|
+
collection: OPS_LOG_COLLECTION,
|
|
5441
|
+
record: {
|
|
5442
|
+
id,
|
|
5443
|
+
data: rest
|
|
5444
|
+
}
|
|
5445
|
+
});
|
|
5446
|
+
} catch (err) {
|
|
5447
|
+
this.logger.warn("OpsLogStore.append failed (best-effort)", {
|
|
5448
|
+
tags: { deviceId: input.deviceId ?? void 0 },
|
|
5449
|
+
meta: {
|
|
5450
|
+
op: input.op,
|
|
5451
|
+
error: String(err)
|
|
5452
|
+
}
|
|
5453
|
+
});
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
/**
|
|
5457
|
+
* List events ops-log rows newest-first, optionally scoped to one device,
|
|
5458
|
+
* capped at `limit` (default {@link OPS_LOG_DEFAULT_LIMIT}).
|
|
5459
|
+
*/
|
|
5460
|
+
async list(query) {
|
|
5461
|
+
const filter = {
|
|
5462
|
+
orderBy: {
|
|
5463
|
+
field: "at",
|
|
5464
|
+
direction: "desc"
|
|
5465
|
+
},
|
|
5466
|
+
limit: query.limit ?? 200
|
|
5467
|
+
};
|
|
5468
|
+
if (query.deviceId !== void 0) filter.where = { deviceId: query.deviceId };
|
|
5469
|
+
const rows = await this.store.query.query({
|
|
5470
|
+
collection: OPS_LOG_COLLECTION,
|
|
5471
|
+
filter
|
|
5472
|
+
});
|
|
5473
|
+
const out = [];
|
|
5474
|
+
for (const r of rows) {
|
|
5475
|
+
const parsed = require_dist.OpsLogEntrySchema.safeParse({
|
|
5476
|
+
id: r.id,
|
|
5477
|
+
...stripNulls(r.data)
|
|
5478
|
+
});
|
|
5479
|
+
if (parsed.success) out.push(parsed.data);
|
|
5480
|
+
else this.logger.debug("OpsLogStore.list: skipped malformed row", { meta: { id: r.id } });
|
|
5481
|
+
}
|
|
5482
|
+
return out;
|
|
5483
|
+
}
|
|
5484
|
+
};
|
|
5485
|
+
/** SQLite stores nullable columns as `null`; the row schema uses `.nullable()`
|
|
5486
|
+
* for deviceId/detail (accepts null) but drop any stray null on required
|
|
5487
|
+
* fields defensively before parse. */
|
|
5488
|
+
function stripNulls(data) {
|
|
5489
|
+
const out = {};
|
|
5490
|
+
for (const [k, v] of Object.entries(data)) {
|
|
5491
|
+
if (v === null && k !== "deviceId" && k !== "detail") continue;
|
|
5492
|
+
out[k] = v;
|
|
5493
|
+
}
|
|
5494
|
+
return out;
|
|
5495
|
+
}
|
|
5496
|
+
//#endregion
|
|
5226
5497
|
//#region src/pipeline-analytics/store/sensor-event-store.ts
|
|
5227
5498
|
var SENSOR_EVENTS_COLLECTION = "pipeline-analytics:sensor-events";
|
|
5228
5499
|
var SENSOR_EVENT_COLUMNS = [
|
|
@@ -5395,114 +5666,49 @@ function isRecord(x) {
|
|
|
5395
5666
|
* linked camera ids) with a TTL, so the `DeviceStateChanged` handler stays
|
|
5396
5667
|
* cheap at bus rate.
|
|
5397
5668
|
*/
|
|
5398
|
-
var MOTION_COLOR = "#f59e0b";
|
|
5399
|
-
var AUDIO_COLOR = "#06b6d4";
|
|
5400
|
-
var PERSON_COLOR = "#22c55e";
|
|
5401
|
-
var VEHICLE_COLOR = "#3b82f6";
|
|
5402
|
-
var ANIMAL_COLOR = "#f97316";
|
|
5403
|
-
var GENERIC_DETECTION_COLOR = "#64748b";
|
|
5404
|
-
var PACKAGE_COLOR = "#a855f7";
|
|
5405
|
-
var VEHICLE_CLASSES = new Set([
|
|
5406
|
-
"vehicle",
|
|
5407
|
-
"car",
|
|
5408
|
-
"truck",
|
|
5409
|
-
"bus",
|
|
5410
|
-
"motorcycle",
|
|
5411
|
-
"bicycle",
|
|
5412
|
-
"boat",
|
|
5413
|
-
"train"
|
|
5414
|
-
]);
|
|
5415
|
-
var ANIMAL_CLASSES = new Set([
|
|
5416
|
-
"animal",
|
|
5417
|
-
"dog",
|
|
5418
|
-
"cat",
|
|
5419
|
-
"bird",
|
|
5420
|
-
"horse",
|
|
5421
|
-
"cow",
|
|
5422
|
-
"sheep"
|
|
5423
|
-
]);
|
|
5424
|
-
function detectionIcon(className) {
|
|
5425
|
-
if (className === "person") return "person";
|
|
5426
|
-
if (VEHICLE_CLASSES.has(className)) return "vehicle";
|
|
5427
|
-
if (ANIMAL_CLASSES.has(className)) return "animal";
|
|
5428
|
-
return "generic";
|
|
5429
|
-
}
|
|
5430
|
-
function detectionColor(className) {
|
|
5431
|
-
if (className === "person") return PERSON_COLOR;
|
|
5432
|
-
if (VEHICLE_CLASSES.has(className)) return VEHICLE_COLOR;
|
|
5433
|
-
if (ANIMAL_CLASSES.has(className)) return ANIMAL_COLOR;
|
|
5434
|
-
return GENERIC_DETECTION_COLOR;
|
|
5435
|
-
}
|
|
5436
|
-
function titleCase(s) {
|
|
5437
|
-
return s.length > 0 ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
5438
|
-
}
|
|
5439
5669
|
/**
|
|
5440
|
-
* Full event-kind
|
|
5441
|
-
*
|
|
5442
|
-
*
|
|
5670
|
+
* Full event-kind taxonomy a camera CAN emit (config-derived):
|
|
5671
|
+
* (a) motion built-in — always;
|
|
5672
|
+
* (b) each enabled detection macro + its taxonomy subs (reverse COCO);
|
|
5673
|
+
* (c) audio macro + its subs when the classifier is enabled;
|
|
5674
|
+
* (d) package kinds when a package zone rule is enabled;
|
|
5675
|
+
* (e) sensor/control kinds from LINKED devices (binding-driven), deduped
|
|
5676
|
+
* per (kind, source deviceId).
|
|
5677
|
+
* Every descriptor carries parentKind/level/iconId/labelKey via the single
|
|
5678
|
+
* taxonomy dictionary — no color/icon is declared here.
|
|
5443
5679
|
*/
|
|
5444
5680
|
async function composeEventKinds(deps, deviceId) {
|
|
5445
|
-
const out = [
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
source
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
}, {
|
|
5456
|
-
kind: "audio",
|
|
5457
|
-
label: "Audio",
|
|
5458
|
-
color: AUDIO_COLOR,
|
|
5459
|
-
icon: "audio",
|
|
5460
|
-
category: "audio",
|
|
5461
|
-
source: {
|
|
5462
|
-
capName: "pipeline-analytics",
|
|
5463
|
-
deviceId
|
|
5464
|
-
}
|
|
5465
|
-
}];
|
|
5681
|
+
const out = [];
|
|
5682
|
+
const cameraSource = {
|
|
5683
|
+
capName: "pipeline-analytics",
|
|
5684
|
+
deviceId
|
|
5685
|
+
};
|
|
5686
|
+
const pushKind = (kind, source = cameraSource) => {
|
|
5687
|
+
const d = require_dist.buildEventKindDescriptor(kind, source);
|
|
5688
|
+
if (d !== null) out.push(d);
|
|
5689
|
+
};
|
|
5690
|
+
pushKind("motion");
|
|
5466
5691
|
try {
|
|
5467
|
-
const
|
|
5468
|
-
for (const
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
icon: detectionIcon(className),
|
|
5473
|
-
category: "detection",
|
|
5474
|
-
source: {
|
|
5475
|
-
capName: "pipeline-analytics",
|
|
5476
|
-
deviceId
|
|
5477
|
-
}
|
|
5478
|
-
});
|
|
5692
|
+
const macros = await deps.enabledMacroClasses(deviceId);
|
|
5693
|
+
for (const macro of macros) {
|
|
5694
|
+
pushKind(macro);
|
|
5695
|
+
for (const sub of require_dist.subKindsOf(macro)) pushKind(sub.kind);
|
|
5696
|
+
}
|
|
5479
5697
|
} catch (err) {
|
|
5480
|
-
deps.onError?.("
|
|
5698
|
+
deps.onError?.("enabledMacroClasses", err);
|
|
5699
|
+
}
|
|
5700
|
+
try {
|
|
5701
|
+
if (await deps.audioEnabled(deviceId)) {
|
|
5702
|
+
pushKind("audio");
|
|
5703
|
+
for (const sub of require_dist.subKindsOf("audio")) pushKind(sub.kind);
|
|
5704
|
+
}
|
|
5705
|
+
} catch (err) {
|
|
5706
|
+
deps.onError?.("audioEnabled", err);
|
|
5481
5707
|
}
|
|
5482
5708
|
try {
|
|
5483
5709
|
if (deps.packageZonesEnabled && await deps.packageZonesEnabled(deviceId)) {
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
label: "Package delivered",
|
|
5487
|
-
color: PACKAGE_COLOR,
|
|
5488
|
-
icon: "package",
|
|
5489
|
-
category: "package",
|
|
5490
|
-
source: {
|
|
5491
|
-
capName: "pipeline-analytics",
|
|
5492
|
-
deviceId
|
|
5493
|
-
}
|
|
5494
|
-
});
|
|
5495
|
-
out.push({
|
|
5496
|
-
kind: "package-picked-up",
|
|
5497
|
-
label: "Package picked up",
|
|
5498
|
-
color: PACKAGE_COLOR,
|
|
5499
|
-
icon: "package",
|
|
5500
|
-
category: "package",
|
|
5501
|
-
source: {
|
|
5502
|
-
capName: "pipeline-analytics",
|
|
5503
|
-
deviceId
|
|
5504
|
-
}
|
|
5505
|
-
});
|
|
5710
|
+
pushKind("package");
|
|
5711
|
+
for (const sub of require_dist.subKindsOf("package")) pushKind(sub.kind);
|
|
5506
5712
|
}
|
|
5507
5713
|
} catch (err) {
|
|
5508
5714
|
deps.onError?.("packageZonesEnabled", err);
|
|
@@ -5525,16 +5731,9 @@ async function composeEventKinds(deps, deviceId) {
|
|
|
5525
5731
|
const dedupeKey = `${descriptor.kind}:${linked.deviceId}`;
|
|
5526
5732
|
if (seen.has(dedupeKey)) continue;
|
|
5527
5733
|
seen.add(dedupeKey);
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
color: descriptor.color,
|
|
5532
|
-
icon: descriptor.icon,
|
|
5533
|
-
category: descriptor.category,
|
|
5534
|
-
source: {
|
|
5535
|
-
capName,
|
|
5536
|
-
deviceId: linked.deviceId
|
|
5537
|
-
}
|
|
5734
|
+
pushKind(descriptor.kind, {
|
|
5735
|
+
capName,
|
|
5736
|
+
deviceId: linked.deviceId
|
|
5538
5737
|
});
|
|
5539
5738
|
}
|
|
5540
5739
|
}
|
|
@@ -5630,6 +5829,110 @@ async function ingestSensorStateChange(deps, data, timestamp) {
|
|
|
5630
5829
|
return inserted;
|
|
5631
5830
|
}
|
|
5632
5831
|
//#endregion
|
|
5832
|
+
//#region src/pipeline-analytics/services/synthetic-sensor-track.ts
|
|
5833
|
+
/**
|
|
5834
|
+
* Synthetic sensor/control tracks (event-kinds-taxonomy §8).
|
|
5835
|
+
*
|
|
5836
|
+
* A linked sensor/control state change is projected into the UNIFIED track
|
|
5837
|
+
* store as a SYNTHETIC track: `className` = the event kind (`contact` /
|
|
5838
|
+
* `lock` / `switch` …), `source: 'sensor'`, media = an on-demand snapshot of
|
|
5839
|
+
* the linked camera in the SAME `getTrackMedia` shape the UIs consume (no
|
|
5840
|
+
* bbox → full frame), `positions: []`. It joins the timeline via the existing
|
|
5841
|
+
* time-based clustering — no new join mechanism.
|
|
5842
|
+
*
|
|
5843
|
+
* The raw `SensorEvent` remains the durable record; this track is its
|
|
5844
|
+
* projection. Snapshots are debounced per `(camera, kind)` so a chattery
|
|
5845
|
+
* sensor doesn't hammer the snapshot cap. Spatial consumers MUST skip
|
|
5846
|
+
* `source:'sensor'` tracks (they carry no trajectory).
|
|
5847
|
+
*/
|
|
5848
|
+
var DEFAULT_DEBOUNCE_MS = 1e4;
|
|
5849
|
+
/** Full-frame, position-less placeholder (spatial consumers skip these). */
|
|
5850
|
+
function zeroPosition(timestamp) {
|
|
5851
|
+
return {
|
|
5852
|
+
x: 0,
|
|
5853
|
+
y: 0,
|
|
5854
|
+
timestamp,
|
|
5855
|
+
bbox: {
|
|
5856
|
+
x: 0,
|
|
5857
|
+
y: 0,
|
|
5858
|
+
w: 0,
|
|
5859
|
+
h: 0
|
|
5860
|
+
}
|
|
5861
|
+
};
|
|
5862
|
+
}
|
|
5863
|
+
var SyntheticSensorTrackMaterializer = class {
|
|
5864
|
+
deps;
|
|
5865
|
+
debounceMs;
|
|
5866
|
+
makeId;
|
|
5867
|
+
now;
|
|
5868
|
+
/** Last snapshot time per `${cameraId}:${kind}`. */
|
|
5869
|
+
lastAt = /* @__PURE__ */ new Map();
|
|
5870
|
+
constructor(deps) {
|
|
5871
|
+
this.deps = deps;
|
|
5872
|
+
this.debounceMs = deps.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
5873
|
+
this.makeId = deps.makeId ?? (() => `pa-synth-${(0, node_crypto.randomUUID)()}`);
|
|
5874
|
+
this.now = deps.now ?? Date.now;
|
|
5875
|
+
}
|
|
5876
|
+
/**
|
|
5877
|
+
* Materialize a synthetic track for a sensor/control event on `cameraId`.
|
|
5878
|
+
* Returns the persisted track, or null when debounced. Snapshot failure
|
|
5879
|
+
* still lands a track (with no media) — it never blocks the sensor record.
|
|
5880
|
+
*/
|
|
5881
|
+
async materialize(input) {
|
|
5882
|
+
const key = `${input.cameraId}:${input.kind}`;
|
|
5883
|
+
const last = this.lastAt.get(key);
|
|
5884
|
+
if (last !== void 0 && this.now() - last < this.debounceMs) return null;
|
|
5885
|
+
this.lastAt.set(key, this.now());
|
|
5886
|
+
const trackId = this.makeId();
|
|
5887
|
+
const ts = input.timestamp;
|
|
5888
|
+
let mediaKey = null;
|
|
5889
|
+
try {
|
|
5890
|
+
const snap = await this.deps.snapshot.getSnapshot({
|
|
5891
|
+
deviceId: input.cameraId,
|
|
5892
|
+
force: true
|
|
5893
|
+
});
|
|
5894
|
+
if (snap !== null) {
|
|
5895
|
+
const data = Buffer.from(snap.base64, "base64");
|
|
5896
|
+
mediaKey = await this.deps.media.put({
|
|
5897
|
+
deviceId: input.cameraId,
|
|
5898
|
+
ownerKind: "track",
|
|
5899
|
+
ownerId: trackId,
|
|
5900
|
+
kind: "snapshot",
|
|
5901
|
+
timestamp: ts,
|
|
5902
|
+
data
|
|
5903
|
+
});
|
|
5904
|
+
} else this.deps.onError?.("getSnapshot", /* @__PURE__ */ new Error("snapshot returned null"));
|
|
5905
|
+
} catch (err) {
|
|
5906
|
+
this.deps.onError?.("snapshotMedia", err);
|
|
5907
|
+
}
|
|
5908
|
+
const track = {
|
|
5909
|
+
trackId,
|
|
5910
|
+
deviceId: input.cameraId,
|
|
5911
|
+
className: input.kind,
|
|
5912
|
+
source: "sensor",
|
|
5913
|
+
firstSeen: ts,
|
|
5914
|
+
lastSeen: ts,
|
|
5915
|
+
positions: [],
|
|
5916
|
+
snapshots: mediaKey !== null ? [{
|
|
5917
|
+
timestamp: ts,
|
|
5918
|
+
position: zeroPosition(ts),
|
|
5919
|
+
mediaKey
|
|
5920
|
+
}] : [],
|
|
5921
|
+
zonesVisited: [],
|
|
5922
|
+
totalDistance: 0,
|
|
5923
|
+
state: "idle",
|
|
5924
|
+
active: false
|
|
5925
|
+
};
|
|
5926
|
+
try {
|
|
5927
|
+
await this.deps.tracks.persistSyntheticTrack(track);
|
|
5928
|
+
} catch (err) {
|
|
5929
|
+
this.deps.onError?.("persistSyntheticTrack", err);
|
|
5930
|
+
return null;
|
|
5931
|
+
}
|
|
5932
|
+
return track;
|
|
5933
|
+
}
|
|
5934
|
+
};
|
|
5935
|
+
//#endregion
|
|
5633
5936
|
//#region src/shared/frame/resolve-frame.ts
|
|
5634
5937
|
/**
|
|
5635
5938
|
* Resolve the pixels a `FrameHandle` refers to via the node-routed fetch.
|
|
@@ -5677,7 +5980,7 @@ function squareSafeCropRegion(bbox, frame, padding) {
|
|
|
5677
5980
|
}
|
|
5678
5981
|
//#endregion
|
|
5679
5982
|
//#region src/shared/frame/box-drawer.ts
|
|
5680
|
-
var DEFAULT_COLOR =
|
|
5983
|
+
var DEFAULT_COLOR = require_dist.DEFAULT_EVENT_COLOR;
|
|
5681
5984
|
var DEFAULT_QUALITY = 80;
|
|
5682
5985
|
var STROKE_WIDTH = 3;
|
|
5683
5986
|
function escapeXml(s) {
|
|
@@ -10964,11 +11267,15 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
10964
11267
|
stationaryRegistry = null;
|
|
10965
11268
|
mediaStore = null;
|
|
10966
11269
|
eventStore = null;
|
|
11270
|
+
/** Events-domain ops-log (footprint/prune/manual-delete audit). Null until onInitialize. */
|
|
11271
|
+
eventsOpsLog = null;
|
|
10967
11272
|
/** Per-camera history of LINKED-device sensor state changes (Part B). */
|
|
10968
11273
|
sensorEventStore = null;
|
|
10969
11274
|
/** Ingest-side reverse index (sensor device → linked camera ids), TTL-cached
|
|
10970
11275
|
* so the DeviceStateChanged handler stays cheap. */
|
|
10971
11276
|
linkedCamerasCache = null;
|
|
11277
|
+
/** Projects sensor/control events into synthetic snapshot-carrying tracks. */
|
|
11278
|
+
syntheticSensorTracks = null;
|
|
10972
11279
|
identityStore = null;
|
|
10973
11280
|
faceStore = null;
|
|
10974
11281
|
faceRecognizer = null;
|
|
@@ -11126,20 +11433,22 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
11126
11433
|
await VehicleStore.declare(api.settingsStore);
|
|
11127
11434
|
await ObjectEmbeddingStore.declare(api.settingsStore);
|
|
11128
11435
|
await StationaryObjectRegistry.declare(api.settingsStore);
|
|
11436
|
+
await OpsLogStore.declare(api.settingsStore);
|
|
11129
11437
|
const logger = this.ctx.logger;
|
|
11130
11438
|
let storage = this.ctx.kernel.storage;
|
|
11131
11439
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
11132
11440
|
if (mediaRoot) {
|
|
11133
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
11441
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-BRwocT7C.js"));
|
|
11134
11442
|
storage = new FilesystemStorageProvider(mediaRoot);
|
|
11135
11443
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
11136
11444
|
}
|
|
11137
11445
|
if (!storage) throw new Error("pipeline-analytics requires ctx.kernel.storage");
|
|
11138
|
-
|
|
11446
|
+
const trackStore = new TrackStore({
|
|
11139
11447
|
store: api.settingsStore,
|
|
11140
11448
|
logger: logger.child("TrackStore"),
|
|
11141
11449
|
frameDims: (deviceId) => this.lastFrameDimsByDevice.get(deviceId)
|
|
11142
11450
|
});
|
|
11451
|
+
this.trackStore = trackStore;
|
|
11143
11452
|
this.stationaryRegistry = new StationaryObjectRegistry({
|
|
11144
11453
|
store: api.settingsStore,
|
|
11145
11454
|
logger: logger.child("StationaryRegistry"),
|
|
@@ -11169,11 +11478,12 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
11169
11478
|
}
|
|
11170
11479
|
});
|
|
11171
11480
|
await this.stationaryRegistry.load();
|
|
11172
|
-
|
|
11481
|
+
const mediaStore = new MediaStore({
|
|
11173
11482
|
storage,
|
|
11174
11483
|
store: api.settingsStore,
|
|
11175
11484
|
logger: logger.child("MediaStore")
|
|
11176
11485
|
});
|
|
11486
|
+
this.mediaStore = mediaStore;
|
|
11177
11487
|
const eventStore = new EventStore({
|
|
11178
11488
|
store: api.settingsStore,
|
|
11179
11489
|
logger: logger.child("EventStore"),
|
|
@@ -11233,6 +11543,15 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
11233
11543
|
error: require_dist.errMsg(err)
|
|
11234
11544
|
} })
|
|
11235
11545
|
});
|
|
11546
|
+
this.syntheticSensorTracks = new SyntheticSensorTrackMaterializer({
|
|
11547
|
+
snapshot: { getSnapshot: (i) => api.snapshot.getSnapshot.query(i) },
|
|
11548
|
+
media: mediaStore,
|
|
11549
|
+
tracks: trackStore,
|
|
11550
|
+
onError: (scope, err) => logger.warn("synthetic sensor-track error", { meta: {
|
|
11551
|
+
scope,
|
|
11552
|
+
error: require_dist.errMsg(err)
|
|
11553
|
+
} })
|
|
11554
|
+
});
|
|
11236
11555
|
this.identityStore = new IdentityStore({
|
|
11237
11556
|
store: api.settingsStore,
|
|
11238
11557
|
logger: logger.child("IdentityStore")
|
|
@@ -11251,6 +11570,11 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
11251
11570
|
});
|
|
11252
11571
|
const rawNodeId = this.ctx.kernel.localNodeId ?? this.ctx.id;
|
|
11253
11572
|
const ownNodeId = rawNodeId.includes("/") ? rawNodeId.split("/")[0] : rawNodeId;
|
|
11573
|
+
this.eventsOpsLog = new OpsLogStore({
|
|
11574
|
+
store: api.settingsStore,
|
|
11575
|
+
logger: logger.child("ops-log"),
|
|
11576
|
+
nodeId: ownNodeId
|
|
11577
|
+
});
|
|
11254
11578
|
{
|
|
11255
11579
|
const designated = await this.postProcessingNodeState.get();
|
|
11256
11580
|
this.isPostProcessingNode = ownNodeId === designated;
|
|
@@ -13465,10 +13789,21 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13465
13789
|
*/
|
|
13466
13790
|
async listEventKinds(input) {
|
|
13467
13791
|
const api = this.ctx.api;
|
|
13792
|
+
const status = await api.pipelineOrchestrator.getCameraStatus.query({ deviceId: input.deviceId }).catch((err) => {
|
|
13793
|
+
this.ctx.logger.warn("listEventKinds: getCameraStatus failed", {
|
|
13794
|
+
tags: { deviceId: input.deviceId },
|
|
13795
|
+
meta: { error: require_dist.errMsg(err) }
|
|
13796
|
+
});
|
|
13797
|
+
return null;
|
|
13798
|
+
});
|
|
13799
|
+
const detectionOn = status?.detection != null;
|
|
13800
|
+
const audioOn = status?.audio?.enabled === true;
|
|
13801
|
+
const detectionMacros = require_dist.MACRO_LABELS.map((l) => l.id).filter((id) => id !== "package");
|
|
13468
13802
|
return composeEventKinds({
|
|
13469
13803
|
linkedDevices: { getLinkedDevices: (i) => api.deviceManager.getLinkedDevices.query(i) },
|
|
13470
13804
|
bindings: { getBindings: (i) => api.deviceManager.getBindings.query(i) },
|
|
13471
|
-
|
|
13805
|
+
enabledMacroClasses: async () => detectionOn ? detectionMacros : [],
|
|
13806
|
+
audioEnabled: async () => audioOn,
|
|
13472
13807
|
packageZonesEnabled: async (deviceId) => {
|
|
13473
13808
|
return (await this.resolveDevicePackageRules(deviceId)).some((r) => r.enabled !== false);
|
|
13474
13809
|
},
|
|
@@ -13508,6 +13843,26 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13508
13843
|
}
|
|
13509
13844
|
});
|
|
13510
13845
|
}
|
|
13846
|
+
const materializer = this.syntheticSensorTracks;
|
|
13847
|
+
const descriptor = require_dist.EVENT_KIND_BY_CAP[data.capName];
|
|
13848
|
+
if (materializer === null || descriptor === void 0) return;
|
|
13849
|
+
try {
|
|
13850
|
+
const cameraIds = await cache.camerasFor(data.deviceId);
|
|
13851
|
+
for (const cameraId of cameraIds) await materializer.materialize({
|
|
13852
|
+
cameraId,
|
|
13853
|
+
sourceDeviceId: data.deviceId,
|
|
13854
|
+
kind: descriptor.kind,
|
|
13855
|
+
timestamp
|
|
13856
|
+
});
|
|
13857
|
+
} catch (err) {
|
|
13858
|
+
this.ctx.logger.warn("synthetic sensor-track materialize failed", {
|
|
13859
|
+
tags: { deviceId: data.deviceId },
|
|
13860
|
+
meta: {
|
|
13861
|
+
capName: data.capName,
|
|
13862
|
+
error: require_dist.errMsg(err)
|
|
13863
|
+
}
|
|
13864
|
+
});
|
|
13865
|
+
}
|
|
13511
13866
|
}
|
|
13512
13867
|
async clearTracks(input) {
|
|
13513
13868
|
this.trackStore?.clearDevice(input.deviceId);
|
|
@@ -13756,6 +14111,120 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13756
14111
|
};
|
|
13757
14112
|
}
|
|
13758
14113
|
/**
|
|
14114
|
+
* Durable event-store footprint for the events-management UI: event ROWS
|
|
14115
|
+
* (motion + object + audio) counted per camera (indexed `count`) + total, and
|
|
14116
|
+
* event-owned media BYTES on disk per camera (paged `sizeBytes` sum) + total.
|
|
14117
|
+
* The device set is the union of cameras that have media OR persisted tracks;
|
|
14118
|
+
* cameras with neither rows nor bytes are omitted.
|
|
14119
|
+
*/
|
|
14120
|
+
async getEventStoreFootprint() {
|
|
14121
|
+
const eventStore = this.eventStore;
|
|
14122
|
+
const mediaStore = this.mediaStore;
|
|
14123
|
+
if (!eventStore || !mediaStore) return {
|
|
14124
|
+
totalRows: 0,
|
|
14125
|
+
totalBytes: 0,
|
|
14126
|
+
devices: []
|
|
14127
|
+
};
|
|
14128
|
+
const mediaByDevice = await mediaStore.footprintByDevice();
|
|
14129
|
+
const deviceIds = new Set(mediaByDevice.keys());
|
|
14130
|
+
if (this.trackStore) for (const id of await this.trackStore.listDeviceIds()) deviceIds.add(id);
|
|
14131
|
+
const devices = [];
|
|
14132
|
+
let totalRows = 0;
|
|
14133
|
+
let totalBytes = 0;
|
|
14134
|
+
for (const deviceId of deviceIds) {
|
|
14135
|
+
const rows = await eventStore.countForDevice(deviceId);
|
|
14136
|
+
const bytes = mediaByDevice.get(deviceId)?.bytes ?? 0;
|
|
14137
|
+
if (rows === 0 && bytes === 0) continue;
|
|
14138
|
+
totalRows += rows;
|
|
14139
|
+
totalBytes += bytes;
|
|
14140
|
+
devices.push({
|
|
14141
|
+
deviceId,
|
|
14142
|
+
rows,
|
|
14143
|
+
bytes
|
|
14144
|
+
});
|
|
14145
|
+
}
|
|
14146
|
+
devices.sort((a, b) => b.bytes - a.bytes);
|
|
14147
|
+
return {
|
|
14148
|
+
totalRows,
|
|
14149
|
+
totalBytes,
|
|
14150
|
+
devices
|
|
14151
|
+
};
|
|
14152
|
+
}
|
|
14153
|
+
/**
|
|
14154
|
+
* Cluster-wide prune of events with `timestamp < olderThanMs` across every
|
|
14155
|
+
* camera (via the device-agnostic `evictBefore`), deleting each pruned event's
|
|
14156
|
+
* media in lockstep. Logs one global (`deviceId:null`) events ops-log row.
|
|
14157
|
+
* `bytesReclaimed` is 0 (media deletion does not surface freed bytes).
|
|
14158
|
+
*/
|
|
14159
|
+
async pruneEvents(input) {
|
|
14160
|
+
const eventStore = this.eventStore;
|
|
14161
|
+
if (!eventStore) return {
|
|
14162
|
+
motion: 0,
|
|
14163
|
+
object: 0,
|
|
14164
|
+
audio: 0
|
|
14165
|
+
};
|
|
14166
|
+
const cutoff = input.olderThanMs;
|
|
14167
|
+
const evicted = await eventStore.evictBefore({
|
|
14168
|
+
motionCutoffMs: cutoff,
|
|
14169
|
+
objectCutoffMs: cutoff,
|
|
14170
|
+
audioCutoffMs: cutoff
|
|
14171
|
+
});
|
|
14172
|
+
const ids = [
|
|
14173
|
+
...evicted.motion,
|
|
14174
|
+
...evicted.object,
|
|
14175
|
+
...evicted.audio
|
|
14176
|
+
];
|
|
14177
|
+
if (ids.length > 0 && this.mediaStore) await this.mediaStore.deleteForEvents(ids);
|
|
14178
|
+
const counts = {
|
|
14179
|
+
motion: evicted.motion.length,
|
|
14180
|
+
object: evicted.object.length,
|
|
14181
|
+
audio: evicted.audio.length
|
|
14182
|
+
};
|
|
14183
|
+
const total = counts.motion + counts.object + counts.audio;
|
|
14184
|
+
await this.eventsOpsLog?.append({
|
|
14185
|
+
op: "prune",
|
|
14186
|
+
reason: input.reason ?? "retention",
|
|
14187
|
+
deviceId: null,
|
|
14188
|
+
itemsAffected: total,
|
|
14189
|
+
bytesReclaimed: 0,
|
|
14190
|
+
detail: `pruned events older than ${cutoff}`
|
|
14191
|
+
});
|
|
14192
|
+
if (total > 0) this.ctx.logger.info("analytics events pruned (cluster)", { meta: {
|
|
14193
|
+
cutoffMs: cutoff,
|
|
14194
|
+
...counts
|
|
14195
|
+
} });
|
|
14196
|
+
return counts;
|
|
14197
|
+
}
|
|
14198
|
+
/**
|
|
14199
|
+
* Manually delete EVERY event (motion + object + audio) for one camera and its
|
|
14200
|
+
* event-owned media in lockstep. Logs a manual events ops-log row.
|
|
14201
|
+
*/
|
|
14202
|
+
async deleteDeviceEvents(input) {
|
|
14203
|
+
if (!this.eventStore) return {
|
|
14204
|
+
motion: 0,
|
|
14205
|
+
object: 0,
|
|
14206
|
+
audio: 0
|
|
14207
|
+
};
|
|
14208
|
+
const counts = await this.pruneEventsBefore({
|
|
14209
|
+
deviceId: input.deviceId,
|
|
14210
|
+
cutoffMs: Number.MAX_SAFE_INTEGER
|
|
14211
|
+
});
|
|
14212
|
+
const total = counts.motion + counts.object + counts.audio;
|
|
14213
|
+
await this.eventsOpsLog?.append({
|
|
14214
|
+
op: "manual-delete",
|
|
14215
|
+
reason: "manual",
|
|
14216
|
+
deviceId: input.deviceId,
|
|
14217
|
+
itemsAffected: total,
|
|
14218
|
+
bytesReclaimed: 0,
|
|
14219
|
+
detail: "deleted all events for device"
|
|
14220
|
+
});
|
|
14221
|
+
return counts;
|
|
14222
|
+
}
|
|
14223
|
+
/** The events ops-log rows (newest-first), optionally scoped to one camera. */
|
|
14224
|
+
async listOpsLog(input) {
|
|
14225
|
+
return await this.eventsOpsLog?.list(input) ?? [];
|
|
14226
|
+
}
|
|
14227
|
+
/**
|
|
13759
14228
|
* Track-centric time-based retention (design §5.1). Drains every persisted
|
|
13760
14229
|
* track for the device whose `lastSeen < cutoffMs`, page by page, through the
|
|
13761
14230
|
* widened cascade — enrolled faces/plates + identity media are exempt (design
|