@camstack/addon-post-analysis 1.2.621 → 1.2.623
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/THIRD_PARTY_MODELS.md +2 -2
- package/dist/{clip-model-registry-BBzoIoKO.mjs → clip-model-registry-C58fyELd.mjs} +412 -66
- package/dist/{clip-model-registry-CcgzpELQ.js → clip-model-registry-KKT83z9U.js} +429 -65
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +2 -2
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CkNZAMox.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-_ahOxwNA.mjs} +3 -3
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-V5-s2hmZ.mjs +26 -0
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CssYIiFD.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B1HZu0i3.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-wlnNaaYX.mjs → hostInit-CdRVx4vF.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +888 -155
- package/dist/pipeline-analytics/index.mjs +888 -155
- 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_types__loadShare__.js-BSMSev-b.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_clip_model_registry = require("../clip-model-registry-
|
|
5
|
+
const require_clip_model_registry = require("../clip-model-registry-KKT83z9U.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_clip_model_registry.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -5402,40 +5402,118 @@ async function notificationLinkEndpoints(api, logger) {
|
|
|
5402
5402
|
*/
|
|
5403
5403
|
var NC_TRACKS_AT_INSTANT_LIMIT = 25;
|
|
5404
5404
|
/**
|
|
5405
|
-
*
|
|
5406
|
-
*
|
|
5407
|
-
*
|
|
5405
|
+
* How far outside its own life a track may still be drawn: one detection tick.
|
|
5406
|
+
*
|
|
5407
|
+
* The box TOLERANCE (`toleranceMs`, 750 ms) is about how sparse a trail is —
|
|
5408
|
+
* a live track keeps a position every few frames — and it must never stretch
|
|
5409
|
+
* a LIFE. A track born after the frame was not on it: on 2026-10-22 the
|
|
5410
|
+
* tracker split a dog on camera 615 and the second piece, born 676 ms after
|
|
5411
|
+
* the key frame, was drawn at its first box as a second box on the dog. One
|
|
5412
|
+
* tick (~110–150 ms at that camera's detection rate, rounded up) absorbs the
|
|
5413
|
+
* gap between a frame and the detection that confirmed it, and nothing more.
|
|
5414
|
+
*/
|
|
5415
|
+
var NC_TRACK_LIFE_SLACK_MS = 200;
|
|
5416
|
+
/**
|
|
5417
|
+
* A position this close to the instant is drawn as it is: one detection tick,
|
|
5418
|
+
* the same notion as {@link NC_TRACK_LIFE_SLACK_MS}. Anything farther is a
|
|
5419
|
+
* position of a MOVED subject, so it is either interpolated (a position on
|
|
5420
|
+
* each side inside the tolerance) or the track is left out — never drawn at a
|
|
5421
|
+
* later place than it was at the picture's instant.
|
|
5422
|
+
*/
|
|
5423
|
+
var NC_BOX_EXACT_MS = NC_TRACK_LIFE_SLACK_MS;
|
|
5424
|
+
/** The box at `timestamp`: a near position as is, else the bracketing pair interpolated. */
|
|
5425
|
+
function boxAt(positions, timestamp, toleranceMs) {
|
|
5426
|
+
let nearest = null;
|
|
5427
|
+
let before = null;
|
|
5428
|
+
let after = null;
|
|
5429
|
+
for (const p of positions) {
|
|
5430
|
+
const d = p.timestamp - timestamp;
|
|
5431
|
+
if (nearest === null || Math.abs(d) < Math.abs(nearest.timestamp - timestamp)) nearest = p;
|
|
5432
|
+
if (d <= 0 && (before === null || p.timestamp > before.timestamp)) before = p;
|
|
5433
|
+
if (d >= 0 && (after === null || p.timestamp < after.timestamp)) after = p;
|
|
5434
|
+
}
|
|
5435
|
+
if (nearest === null) return null;
|
|
5436
|
+
const dt = nearest.timestamp - timestamp;
|
|
5437
|
+
if (Math.abs(dt) <= NC_BOX_EXACT_MS) return {
|
|
5438
|
+
bbox: nearest.bbox,
|
|
5439
|
+
score: nearest.score,
|
|
5440
|
+
dtMs: dt,
|
|
5441
|
+
interpolated: false
|
|
5442
|
+
};
|
|
5443
|
+
if (before === null || after === null || timestamp - before.timestamp > toleranceMs || after.timestamp - timestamp > toleranceMs) return null;
|
|
5444
|
+
const span = after.timestamp - before.timestamp;
|
|
5445
|
+
const f = span === 0 ? 0 : (timestamp - before.timestamp) / span;
|
|
5446
|
+
const lerp = (a, b) => a + (b - a) * f;
|
|
5447
|
+
return {
|
|
5448
|
+
bbox: {
|
|
5449
|
+
x: lerp(before.bbox.x, after.bbox.x),
|
|
5450
|
+
y: lerp(before.bbox.y, after.bbox.y),
|
|
5451
|
+
w: lerp(before.bbox.w, after.bbox.w),
|
|
5452
|
+
h: lerp(before.bbox.h, after.bbox.h)
|
|
5453
|
+
},
|
|
5454
|
+
score: nearest.score,
|
|
5455
|
+
dtMs: dt,
|
|
5456
|
+
interpolated: true
|
|
5457
|
+
};
|
|
5458
|
+
}
|
|
5459
|
+
/**
|
|
5460
|
+
* Every source track ALIVE at `timestamp` (± {@link NC_TRACK_LIFE_SLACK_MS})
|
|
5461
|
+
* with a box AT that instant: a trail position within {@link NC_BOX_EXACT_MS},
|
|
5462
|
+
* else the linear interpolation of the positions bracketing it (each within
|
|
5463
|
+
* `toleranceMs`). A track with neither is not drawn — `onDrop` names it.
|
|
5464
|
+
* Deduplicated by track id (a live copy and a persisted copy are one track).
|
|
5465
|
+
* Pure.
|
|
5408
5466
|
*/
|
|
5409
|
-
function tracksAtInstant(sources, timestamp, toleranceMs, fallbackDims) {
|
|
5467
|
+
function tracksAtInstant(sources, timestamp, toleranceMs, fallbackDims, onDrop) {
|
|
5410
5468
|
const out = /* @__PURE__ */ new Map();
|
|
5411
5469
|
for (const track of sources) {
|
|
5412
5470
|
if (out.has(track.trackId)) continue;
|
|
5413
|
-
if (track.firstSeen > timestamp +
|
|
5471
|
+
if (track.firstSeen > timestamp + NC_TRACK_LIFE_SLACK_MS || track.lastSeen < timestamp - NC_TRACK_LIFE_SLACK_MS) {
|
|
5472
|
+
onDrop?.({
|
|
5473
|
+
trackId: track.trackId,
|
|
5474
|
+
className: track.className,
|
|
5475
|
+
reason: "outside-life"
|
|
5476
|
+
});
|
|
5477
|
+
continue;
|
|
5478
|
+
}
|
|
5414
5479
|
const w = track.frameWidth ?? fallbackDims?.w ?? 0;
|
|
5415
5480
|
const h = track.frameHeight ?? fallbackDims?.h ?? 0;
|
|
5416
|
-
if (w <= 0 || h <= 0)
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5481
|
+
if (w <= 0 || h <= 0) {
|
|
5482
|
+
onDrop?.({
|
|
5483
|
+
trackId: track.trackId,
|
|
5484
|
+
className: track.className,
|
|
5485
|
+
reason: "no-frame-dims"
|
|
5486
|
+
});
|
|
5487
|
+
continue;
|
|
5488
|
+
}
|
|
5489
|
+
const at = boxAt(track.positions, timestamp, toleranceMs);
|
|
5490
|
+
if (at === null) {
|
|
5491
|
+
const nearestDtMs = track.positions.reduce((best, p) => {
|
|
5492
|
+
const d = p.timestamp - timestamp;
|
|
5493
|
+
return best === void 0 || Math.abs(d) < Math.abs(best) ? d : best;
|
|
5494
|
+
}, void 0);
|
|
5495
|
+
onDrop?.({
|
|
5496
|
+
trackId: track.trackId,
|
|
5497
|
+
className: track.className,
|
|
5498
|
+
reason: "no-position-near-instant",
|
|
5499
|
+
...nearestDtMs !== void 0 ? { nearestDtMs } : {}
|
|
5500
|
+
});
|
|
5501
|
+
continue;
|
|
5425
5502
|
}
|
|
5426
|
-
if (best === null || bestDelta > toleranceMs) continue;
|
|
5427
5503
|
const label = track.subLabel ?? track.label;
|
|
5428
5504
|
out.set(track.trackId, {
|
|
5429
5505
|
trackId: track.trackId,
|
|
5430
5506
|
className: track.className,
|
|
5431
5507
|
...label !== void 0 && label !== track.className ? { label } : {},
|
|
5432
|
-
...
|
|
5508
|
+
...at.score !== void 0 ? { confidence: at.score } : {},
|
|
5433
5509
|
bbox: {
|
|
5434
|
-
x:
|
|
5435
|
-
y:
|
|
5436
|
-
w:
|
|
5437
|
-
h:
|
|
5438
|
-
}
|
|
5510
|
+
x: at.bbox.x / w,
|
|
5511
|
+
y: at.bbox.y / h,
|
|
5512
|
+
w: at.bbox.w / w,
|
|
5513
|
+
h: at.bbox.h / h
|
|
5514
|
+
},
|
|
5515
|
+
dtMs: at.dtMs,
|
|
5516
|
+
interpolated: at.interpolated
|
|
5439
5517
|
});
|
|
5440
5518
|
}
|
|
5441
5519
|
return [...out.values()];
|
|
@@ -19337,11 +19415,16 @@ var DebugNoteArchiveStore = class {
|
|
|
19337
19415
|
};
|
|
19338
19416
|
//#endregion
|
|
19339
19417
|
//#region src/pipeline-analytics/store/event-ledger-collection.ts
|
|
19418
|
+
/**
|
|
19419
|
+
* One event ledger (motion, audio, sensor, faces, plates …) over its
|
|
19420
|
+
* `ctx.records` kind. Every node has `ctx.records`, so there is no other path.
|
|
19421
|
+
*/
|
|
19340
19422
|
/** Matches {@link EventStore.LEDGER_PAGE_SIZE} — kept local to avoid a circular import. */
|
|
19341
19423
|
var LEDGER_PAGE_SIZE = 500;
|
|
19342
19424
|
var EventLedgerCollection = class EventLedgerCollection {
|
|
19343
19425
|
collection;
|
|
19344
19426
|
kind;
|
|
19427
|
+
records;
|
|
19345
19428
|
autoIntegerId;
|
|
19346
19429
|
legacyAtField;
|
|
19347
19430
|
deviceIdMode;
|
|
@@ -19357,6 +19440,7 @@ var EventLedgerCollection = class EventLedgerCollection {
|
|
|
19357
19440
|
this.logger = deps.logger;
|
|
19358
19441
|
this.deriveBody = deps.deriveBody;
|
|
19359
19442
|
this.indexFields = deps.definition.declaration.indexes.map((i) => i.fields);
|
|
19443
|
+
this.records = deps.records;
|
|
19360
19444
|
const handle = deps.records.kind(deps.definition);
|
|
19361
19445
|
this.kind = handle;
|
|
19362
19446
|
}
|
|
@@ -19375,6 +19459,31 @@ var EventLedgerCollection = class EventLedgerCollection {
|
|
|
19375
19459
|
}
|
|
19376
19460
|
await this.kind.put(put);
|
|
19377
19461
|
}
|
|
19462
|
+
/**
|
|
19463
|
+
* `upsertRow` as an op of {@link EventLedgerCollection.transact}. A row that
|
|
19464
|
+
* cannot be mapped THROWS here: inside a transaction a silently skipped write
|
|
19465
|
+
* would let the rest of the call commit without it.
|
|
19466
|
+
*/
|
|
19467
|
+
async txUpsert(id, value) {
|
|
19468
|
+
const put = this.buildRecordPut(id, value);
|
|
19469
|
+
if (put === null) throw new require_clip_model_registry.InvalidRecordError(`${this.collection}/${id}: the record could not be mapped`);
|
|
19470
|
+
return this.kind.txPut(put);
|
|
19471
|
+
}
|
|
19472
|
+
/** `update` as an op of {@link EventLedgerCollection.transact}. */
|
|
19473
|
+
txUpdate(id, patch) {
|
|
19474
|
+
return this.kind.txPatch(id, this.derived(patch, "patch"));
|
|
19475
|
+
}
|
|
19476
|
+
/** `deleteKey` as an op of {@link EventLedgerCollection.transact}. */
|
|
19477
|
+
txDelete(id) {
|
|
19478
|
+
return this.kind.txDelete(id);
|
|
19479
|
+
}
|
|
19480
|
+
/**
|
|
19481
|
+
* Run ops (built by the `tx*` methods of ledgers whose kinds share a
|
|
19482
|
+
* `transactFamily`) in ONE transaction: all land or none does.
|
|
19483
|
+
*/
|
|
19484
|
+
transact(ops) {
|
|
19485
|
+
return this.records.transact(ops);
|
|
19486
|
+
}
|
|
19378
19487
|
async insert(record) {
|
|
19379
19488
|
const idNum = this.autoIntegerId ? await this.kind.allocateId() : null;
|
|
19380
19489
|
const id = idNum !== null ? String(idNum) : typeof record["id"] === "string" ? record["id"] : null;
|
|
@@ -20451,8 +20560,8 @@ var FACE_REF_COLUMNS = ["deviceId"];
|
|
|
20451
20560
|
* D312) a predicate that silently matched nothing. The faces ledger therefore derives a
|
|
20452
20561
|
* boolean `recognized` from every write that touches `recognizedIdentityId`
|
|
20453
20562
|
* (`face-recognized.ts`), and the views are plain ranges over the `recognized_ts` /
|
|
20454
|
-
* `assigned_recognized_ts` indexes.
|
|
20455
|
-
*
|
|
20563
|
+
* `assigned_recognized_ts` indexes. Rows written before the field existed were
|
|
20564
|
+
* backfilled offline on the operator hubs on 2026-10-22 (the one-shot script is deleted).
|
|
20456
20565
|
*
|
|
20457
20566
|
* `assigned` is `BOOLEAN NOT NULL`; the backend serialises `false` to `0` at
|
|
20458
20567
|
* the bind (`serializeColumnValue`) and decodes `0` back to `false` on read, so
|
|
@@ -22316,8 +22425,9 @@ var RetrainAnnotationStore = class {
|
|
|
22316
22425
|
* The old rows' deletion, the new rows, the frame's `dismissed` set and its
|
|
22317
22426
|
* bumped `revision` run under a per-frame commit lock, guarded by
|
|
22318
22427
|
* `commit.fromRevision`: a concurrent save in this process leaves one winner
|
|
22319
|
-
* and one `conflict` — never a blend. The
|
|
22320
|
-
*
|
|
22428
|
+
* and one `conflict` — never a blend. The deletion, the new rows and the frame
|
|
22429
|
+
* row are ONE records `transact` (the four retrain kinds share a family): a
|
|
22430
|
+
* failure leaves the frame exactly as it was.
|
|
22321
22431
|
*
|
|
22322
22432
|
* Returns the persisted rows so the caller renders ids it can address.
|
|
22323
22433
|
*/
|
|
@@ -22434,21 +22544,14 @@ var RetrainAnnotationStore = class {
|
|
|
22434
22544
|
}
|
|
22435
22545
|
return stamped;
|
|
22436
22546
|
}
|
|
22437
|
-
/**
|
|
22438
|
-
|
|
22439
|
-
|
|
22440
|
-
|
|
22441
|
-
|
|
22442
|
-
|
|
22443
|
-
|
|
22444
|
-
|
|
22445
|
-
meta: {
|
|
22446
|
-
annotationId: row.id,
|
|
22447
|
-
error: String(err)
|
|
22448
|
-
}
|
|
22449
|
-
});
|
|
22450
|
-
}
|
|
22451
|
-
return rows.length;
|
|
22547
|
+
/**
|
|
22548
|
+
* The delete ops of every annotation of a frame, for ONE transaction with
|
|
22549
|
+
* the frame row and its memberships (RetrainService.removeFrame). A failed
|
|
22550
|
+
* read THROWS: "no annotations" must never be the answer to an unreadable
|
|
22551
|
+
* store, because the frame row would then be deleted over live boxes.
|
|
22552
|
+
*/
|
|
22553
|
+
async deleteOpsForFrame(mediaKey) {
|
|
22554
|
+
return (await this.annotations.query({ where: { mediaKey } })).map((row) => this.annotations.txDelete(row.id));
|
|
22452
22555
|
}
|
|
22453
22556
|
async replaceForFrameOnRecords(mediaKey, rows, commit, nextRevision) {
|
|
22454
22557
|
const frame = await this.frames.get(mediaKey);
|
|
@@ -22461,12 +22564,14 @@ var RetrainAnnotationStore = class {
|
|
|
22461
22564
|
kind: "conflict",
|
|
22462
22565
|
currentRevision
|
|
22463
22566
|
};
|
|
22464
|
-
|
|
22465
|
-
for (const
|
|
22466
|
-
await this.
|
|
22567
|
+
const ops = [];
|
|
22568
|
+
for (const existing of await this.annotations.query({ where: { mediaKey } })) ops.push(this.annotations.txDelete(existing.id));
|
|
22569
|
+
for (const row of rows) ops.push(await this.annotations.txUpsert(row.id, this.toRecord(row)));
|
|
22570
|
+
ops.push(await this.frames.txUpdate(mediaKey, {
|
|
22467
22571
|
revision: nextRevision,
|
|
22468
22572
|
...commit.dismissed !== void 0 ? { dismissed: [...commit.dismissed] } : {}
|
|
22469
|
-
});
|
|
22573
|
+
}));
|
|
22574
|
+
await this.frames.transact(ops);
|
|
22470
22575
|
return {
|
|
22471
22576
|
kind: "saved",
|
|
22472
22577
|
rows,
|
|
@@ -22484,8 +22589,10 @@ var RetrainAnnotationStore = class {
|
|
|
22484
22589
|
kind: "conflict",
|
|
22485
22590
|
currentRevision
|
|
22486
22591
|
};
|
|
22487
|
-
|
|
22488
|
-
await this.
|
|
22592
|
+
const ops = [];
|
|
22593
|
+
for (const row of eligible) ops.push(await this.annotations.txUpdate(row.id, { origin: origins.get(row.id) ?? null }));
|
|
22594
|
+
ops.push(await this.frames.txUpdate(mediaKey, { revision: nextRevision }));
|
|
22595
|
+
await this.frames.transact(ops);
|
|
22489
22596
|
return {
|
|
22490
22597
|
kind: "updated",
|
|
22491
22598
|
updated: eligible.length,
|
|
@@ -22656,16 +22763,16 @@ var RetrainCollectionStore = class {
|
|
|
22656
22763
|
}, counts.get(input.id) ?? 0)
|
|
22657
22764
|
};
|
|
22658
22765
|
}
|
|
22659
|
-
/** Remove the collection and its memberships
|
|
22660
|
-
*
|
|
22766
|
+
/** Remove the collection and its memberships in ONE transaction: a failure
|
|
22767
|
+
* leaves the collection with all its members, never memberships pointing at
|
|
22768
|
+
* a name that no longer exists, nor a collection that silently lost some. */
|
|
22661
22769
|
async delete(id) {
|
|
22662
22770
|
if (await this.readRow(id) === null) return {
|
|
22663
22771
|
deleted: false,
|
|
22664
22772
|
memberFrameIds: []
|
|
22665
22773
|
};
|
|
22666
22774
|
const members = await this.memberships.query({ where: { collectionId: id } });
|
|
22667
|
-
|
|
22668
|
-
await this.collections.deleteKey(id);
|
|
22775
|
+
await this.collections.transact([...members.map((m) => this.memberships.txDelete(m.id)), this.collections.txDelete(id)]);
|
|
22669
22776
|
this.logger.info("retrain collection deleted", { meta: {
|
|
22670
22777
|
collectionId: id,
|
|
22671
22778
|
members: members.length
|
|
@@ -22694,11 +22801,29 @@ var RetrainCollectionStore = class {
|
|
|
22694
22801
|
await this.memberships.deleteKey(key);
|
|
22695
22802
|
return true;
|
|
22696
22803
|
}
|
|
22697
|
-
/**
|
|
22698
|
-
|
|
22699
|
-
|
|
22700
|
-
|
|
22701
|
-
|
|
22804
|
+
/**
|
|
22805
|
+
* The delete ops of every membership of a frame, for ONE transaction with
|
|
22806
|
+
* the frame row and its annotations (RetrainService.removeFrame). A failed
|
|
22807
|
+
* read THROWS: the frame row must not be deleted over memberships nobody
|
|
22808
|
+
* could list.
|
|
22809
|
+
*/
|
|
22810
|
+
async forgetFrameOps(frameId) {
|
|
22811
|
+
return (await this.memberships.query({ where: { frameId } })).map((r) => this.memberships.txDelete(r.id));
|
|
22812
|
+
}
|
|
22813
|
+
/**
|
|
22814
|
+
* The op that adds a frame to a collection, for ONE transaction with the
|
|
22815
|
+
* frame's own row (a still stored from the recording). `null` when it is
|
|
22816
|
+
* already a member: there is nothing to write.
|
|
22817
|
+
*/
|
|
22818
|
+
async addFrameOp(input) {
|
|
22819
|
+
const key = membershipKey(input.collectionId, input.frameId);
|
|
22820
|
+
if (await this.memberships.get(key) !== null) return null;
|
|
22821
|
+
return this.memberships.txUpsert(key, {
|
|
22822
|
+
collectionId: input.collectionId,
|
|
22823
|
+
frameId: input.frameId,
|
|
22824
|
+
deviceId: input.deviceId,
|
|
22825
|
+
addedAt: this.now()
|
|
22826
|
+
});
|
|
22702
22827
|
}
|
|
22703
22828
|
/** The frames of a collection, in the order they were added. */
|
|
22704
22829
|
async listFrameIds(collectionId) {
|
|
@@ -23290,7 +23415,7 @@ var RetrainFrameStore = class {
|
|
|
23290
23415
|
* returned untouched; the blob lands before the row; a refusal is named.
|
|
23291
23416
|
* `capturedAt` IS the instant — the camera saw this frame then.
|
|
23292
23417
|
*/
|
|
23293
|
-
async storeRecordingStill(input) {
|
|
23418
|
+
async storeRecordingStill(input, withRow = []) {
|
|
23294
23419
|
const frameId = recordingFrameId(input.deviceId, input.epochMs);
|
|
23295
23420
|
const existing = await this.get(frameId);
|
|
23296
23421
|
if (existing !== null) return {
|
|
@@ -23337,7 +23462,7 @@ var RetrainFrameStore = class {
|
|
|
23337
23462
|
relativePath: path,
|
|
23338
23463
|
data: input.bytes
|
|
23339
23464
|
});
|
|
23340
|
-
await this.ledger.
|
|
23465
|
+
const row = await this.ledger.txUpsert(frameId, {
|
|
23341
23466
|
deviceId: frame.deviceId,
|
|
23342
23467
|
trackId: "",
|
|
23343
23468
|
sourceMediaId: 0,
|
|
@@ -23354,6 +23479,7 @@ var RetrainFrameStore = class {
|
|
|
23354
23479
|
origin: "recording",
|
|
23355
23480
|
epochMs: input.epochMs
|
|
23356
23481
|
});
|
|
23482
|
+
await this.ledger.transact([row, ...withRow]);
|
|
23357
23483
|
};
|
|
23358
23484
|
try {
|
|
23359
23485
|
if (this.writeGate) await this.writeGate.run(persist);
|
|
@@ -23407,22 +23533,33 @@ var RetrainFrameStore = class {
|
|
|
23407
23533
|
/**
|
|
23408
23534
|
* Drop a copy and its blob — the operator un-selecting a frame.
|
|
23409
23535
|
*
|
|
23410
|
-
* The
|
|
23411
|
-
*
|
|
23412
|
-
*
|
|
23536
|
+
* The ROW goes first, in one transaction with `withRow` (the frame's
|
|
23537
|
+
* annotations and collection memberships, which the caller builds): a failure
|
|
23538
|
+
* leaves the frame, its boxes and its memberships all in place, and throws.
|
|
23539
|
+
* The blob goes after: if that fails the row is already gone and the bytes
|
|
23540
|
+
* are reclaimable junk, logged, never a row pointing at missing media.
|
|
23413
23541
|
*/
|
|
23414
|
-
async deleteFrame(frame) {
|
|
23542
|
+
async deleteFrame(frame, withRow = []) {
|
|
23415
23543
|
const remove = async () => {
|
|
23544
|
+
try {
|
|
23545
|
+
await this.ledger.transact([...withRow, this.ledger.txDelete(frame.frameId)]);
|
|
23546
|
+
} catch (err) {
|
|
23547
|
+
this.logger.error("retrain frame delete failed; nothing was removed", {
|
|
23548
|
+
tags: { deviceId: frame.deviceId },
|
|
23549
|
+
meta: {
|
|
23550
|
+
frameId: frame.frameId,
|
|
23551
|
+
error: String(err)
|
|
23552
|
+
}
|
|
23553
|
+
});
|
|
23554
|
+
throw err;
|
|
23555
|
+
}
|
|
23416
23556
|
try {
|
|
23417
23557
|
await this.storage.delete({
|
|
23418
23558
|
location: frame.locationId ?? LEGACY_RETRAIN_MEDIA_LOCATION,
|
|
23419
23559
|
relativePath: framePath(frame)
|
|
23420
23560
|
});
|
|
23421
|
-
} catch {}
|
|
23422
|
-
try {
|
|
23423
|
-
await this.ledger.deleteKey(frame.frameId);
|
|
23424
23561
|
} catch (err) {
|
|
23425
|
-
this.logger.warn("
|
|
23562
|
+
this.logger.warn("retrain frame removed but its blob could not be deleted", {
|
|
23426
23563
|
tags: { deviceId: frame.deviceId },
|
|
23427
23564
|
meta: {
|
|
23428
23565
|
frameId: frame.frameId,
|
|
@@ -28418,7 +28555,9 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
28418
28555
|
const blindForMs = numberField(data, "blindForMs");
|
|
28419
28556
|
if (deviceId === void 0 || blindForMs === void 0) return null;
|
|
28420
28557
|
const identity = lookup(deviceId);
|
|
28421
|
-
const
|
|
28558
|
+
const motionMs = numberField(data, "motionMs") ?? blindForMs;
|
|
28559
|
+
const sessions = numberField(data, "sessions") ?? 1;
|
|
28560
|
+
const minutes = Math.max(1, Math.round(motionMs / 6e4));
|
|
28422
28561
|
return systemSubject({
|
|
28423
28562
|
facts,
|
|
28424
28563
|
kind: "detection-blind",
|
|
@@ -28427,13 +28566,39 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
28427
28566
|
bodyKey: "system.detection-blind.body",
|
|
28428
28567
|
textParams: {
|
|
28429
28568
|
...deviceTextParams(deviceId, identity),
|
|
28430
|
-
minutes: String(minutes)
|
|
28569
|
+
minutes: String(minutes),
|
|
28570
|
+
sessions: String(sessions)
|
|
28431
28571
|
},
|
|
28432
28572
|
textCount: minutes,
|
|
28433
28573
|
deviceId,
|
|
28434
28574
|
...identity !== void 0 ? { deviceType: identity.type } : {}
|
|
28435
28575
|
});
|
|
28436
28576
|
}
|
|
28577
|
+
case "pipeline.motion-noisy":
|
|
28578
|
+
case "pipeline.motion-calm": {
|
|
28579
|
+
const deviceId = numberField(data, "deviceId");
|
|
28580
|
+
const dutyPct = numberField(data, "dutyPct");
|
|
28581
|
+
const sessions = numberField(data, "sessions");
|
|
28582
|
+
const windowMs = numberField(data, "windowMs");
|
|
28583
|
+
if (deviceId === void 0 || dutyPct === void 0 || sessions === void 0 || windowMs === void 0) return null;
|
|
28584
|
+
const noisy = event.category === "pipeline.motion-noisy";
|
|
28585
|
+
const identity = lookup(deviceId);
|
|
28586
|
+
return systemSubject({
|
|
28587
|
+
facts,
|
|
28588
|
+
kind: noisy ? "motion-noisy" : "motion-calm",
|
|
28589
|
+
subject: `motion-noise:${deviceId}`,
|
|
28590
|
+
titleKey: noisy ? "system.motion-noisy.title" : "system.motion-calm.title",
|
|
28591
|
+
bodyKey: noisy ? "system.motion-noisy.body" : "system.motion-calm.body",
|
|
28592
|
+
textParams: {
|
|
28593
|
+
...deviceTextParams(deviceId, identity),
|
|
28594
|
+
minutes: String(Math.max(1, Math.round(windowMs / 6e4))),
|
|
28595
|
+
duty: String(dutyPct),
|
|
28596
|
+
sessions: String(sessions)
|
|
28597
|
+
},
|
|
28598
|
+
deviceId,
|
|
28599
|
+
...identity !== void 0 ? { deviceType: identity.type } : {}
|
|
28600
|
+
});
|
|
28601
|
+
}
|
|
28437
28602
|
case "recording.health.stalled": {
|
|
28438
28603
|
const deviceId = numberField(data, "deviceId");
|
|
28439
28604
|
const idleMs = numberField(data, "idleMs");
|
|
@@ -28515,6 +28680,33 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
28515
28680
|
...nodeId !== void 0 ? { nodeId } : {}
|
|
28516
28681
|
});
|
|
28517
28682
|
}
|
|
28683
|
+
case "notification.rule-broken":
|
|
28684
|
+
case "notification.rule-repaired": {
|
|
28685
|
+
const ruleId = stringField(data, "ruleId");
|
|
28686
|
+
if (ruleId === void 0) return null;
|
|
28687
|
+
const broken = event.category === "notification.rule-broken";
|
|
28688
|
+
const kind = broken ? "rule-broken" : "rule-repaired";
|
|
28689
|
+
const ruleName = stringField(data, "ruleName") ?? ruleId;
|
|
28690
|
+
const reason = stringField(data, "reason") ?? "unknown";
|
|
28691
|
+
const ids = data["offendingIds"];
|
|
28692
|
+
const offendingIds = Array.isArray(ids) ? ids.filter((id) => typeof id === "string").join(", ") : "";
|
|
28693
|
+
return systemSubject({
|
|
28694
|
+
facts,
|
|
28695
|
+
kind,
|
|
28696
|
+
subject: `rule-health:${ruleId}`,
|
|
28697
|
+
titleKey: `system.${kind}.title`,
|
|
28698
|
+
bodyKey: broken ? `system.rule-broken.body.${reason}` : "system.rule-repaired.body",
|
|
28699
|
+
textParams: broken ? {
|
|
28700
|
+
ruleId,
|
|
28701
|
+
ruleName,
|
|
28702
|
+
brokenReason: reason,
|
|
28703
|
+
offendingIds
|
|
28704
|
+
} : {
|
|
28705
|
+
ruleId,
|
|
28706
|
+
ruleName
|
|
28707
|
+
}
|
|
28708
|
+
});
|
|
28709
|
+
}
|
|
28518
28710
|
case "update.available": return incomingFromUpdateAvailable(facts, data);
|
|
28519
28711
|
case "addon.updated": {
|
|
28520
28712
|
const update = parseAddonUpdateComplete(data);
|
|
@@ -28524,7 +28716,7 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
28524
28716
|
case "system.restarting": return incomingFromNodeRestarting(facts, data);
|
|
28525
28717
|
case "recording.export.completed": return incomingFromExportCompleted(facts, data);
|
|
28526
28718
|
case "backup.run-completed": return incomingFromBackupCompleted(facts, data);
|
|
28527
|
-
case "alert.episode": return incomingFromAlertEpisode(facts);
|
|
28719
|
+
case "alert.episode": return incomingFromAlertEpisode(facts, lookup);
|
|
28528
28720
|
default: return null;
|
|
28529
28721
|
}
|
|
28530
28722
|
}
|
|
@@ -28540,9 +28732,11 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
28540
28732
|
* Whether an edge is notified at all (once per episode, reconciled from the
|
|
28541
28733
|
* alerts store) is decided before this point, by `NcAlertEpisodeLedger`.
|
|
28542
28734
|
*/
|
|
28543
|
-
function incomingFromAlertEpisode(facts) {
|
|
28735
|
+
function incomingFromAlertEpisode(facts, lookup) {
|
|
28544
28736
|
const payload = require_clip_model_registry.parseAlertEpisodePayload(facts.data);
|
|
28545
28737
|
if (payload === null) return null;
|
|
28738
|
+
const attention = incomingFromDeviceAttention(facts, payload, lookup);
|
|
28739
|
+
if (attention !== null) return attention;
|
|
28546
28740
|
const kind = require_clip_model_registry.alertEpisodeKind(payload.severity, payload.phase);
|
|
28547
28741
|
if (kind === null) return null;
|
|
28548
28742
|
return systemSubject({
|
|
@@ -28563,6 +28757,38 @@ function incomingFromAlertEpisode(facts) {
|
|
|
28563
28757
|
...payload.nodeId !== void 0 ? { nodeId: payload.nodeId } : {}
|
|
28564
28758
|
});
|
|
28565
28759
|
}
|
|
28760
|
+
/**
|
|
28761
|
+
* A CRITICAL alert whose source is a DEVICE (D911) is announced in the devices
|
|
28762
|
+
* family — `device-attention-required` / `-resolved`, carrying the device id so
|
|
28763
|
+
* a rule's device filters apply — and NOT as `alert-critical`, so one alert
|
|
28764
|
+
* never notifies twice through two families. A device source whose id is not a
|
|
28765
|
+
* device id falls through to the generic alert kinds.
|
|
28766
|
+
*/
|
|
28767
|
+
function incomingFromDeviceAttention(facts, payload, lookup) {
|
|
28768
|
+
if (payload.sourceType !== "device" || payload.sourceId === void 0) return null;
|
|
28769
|
+
const kind = require_clip_model_registry.deviceAttentionKind(payload.severity, payload.phase);
|
|
28770
|
+
if (kind === null) return null;
|
|
28771
|
+
const deviceId = Number(payload.sourceId);
|
|
28772
|
+
if (!Number.isInteger(deviceId)) return null;
|
|
28773
|
+
const identity = lookup?.(deviceId);
|
|
28774
|
+
return systemSubject({
|
|
28775
|
+
facts,
|
|
28776
|
+
kind,
|
|
28777
|
+
subject: `alert:${payload.episodeId}`,
|
|
28778
|
+
titleKey: `system.${kind}.title`,
|
|
28779
|
+
bodyKey: `system.${kind}.body`,
|
|
28780
|
+
textParams: {
|
|
28781
|
+
...deviceTextParams(deviceId, identity),
|
|
28782
|
+
title: payload.title,
|
|
28783
|
+
message: payload.message,
|
|
28784
|
+
category: payload.category,
|
|
28785
|
+
actionUrl: payload.actionUrl ?? ""
|
|
28786
|
+
},
|
|
28787
|
+
deviceId,
|
|
28788
|
+
...identity !== void 0 ? { deviceType: identity.type } : {},
|
|
28789
|
+
...payload.nodeId !== void 0 ? { nodeId: payload.nodeId } : {}
|
|
28790
|
+
});
|
|
28791
|
+
}
|
|
28566
28792
|
/** The shells whose update has its own sentence. Anything else gets the plain
|
|
28567
28793
|
* one: a body that names a shell nobody observed is worse than a vague body. */
|
|
28568
28794
|
var WRAPPER_BODY_KINDS = new Set([
|
|
@@ -30520,8 +30746,8 @@ var en_default = {
|
|
|
30520
30746
|
"occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
|
|
30521
30747
|
"occupancy.op.occupied": "occupied",
|
|
30522
30748
|
"occupancy.op.free": "free",
|
|
30523
|
-
"system.device-online.title": "
|
|
30524
|
-
"system.device-online.body": "Device {{deviceName}} is online",
|
|
30749
|
+
"system.device-online.title": "Back online — {{deviceName}}",
|
|
30750
|
+
"system.device-online.body": "Device {{deviceName}} is back online",
|
|
30525
30751
|
"system.device-offline.title": "Offline — {{deviceName}}",
|
|
30526
30752
|
"system.device-offline.body": "Device {{deviceName}} is offline",
|
|
30527
30753
|
"system.device-disabled.title": "Switched off — {{deviceName}}",
|
|
@@ -30534,14 +30760,14 @@ var en_default = {
|
|
|
30534
30760
|
"system.device-battery-normal.title": "Battery back to normal — {{deviceName}}",
|
|
30535
30761
|
"system.device-battery-normal.body": "{{deviceName}} is back to {{percentage}}% battery",
|
|
30536
30762
|
"system.device-battery-normal.body.binary": "{{deviceName}} no longer reports a low battery",
|
|
30537
|
-
"system.stream-online.title": "Stream online — {{deviceName}}",
|
|
30538
|
-
"system.stream-online.body": "Stream for camera {{deviceName}} is online",
|
|
30539
|
-
"system.stream-online.body.profiled": "{{profile}} stream for camera {{deviceName}} is online",
|
|
30763
|
+
"system.stream-online.title": "Stream back online — {{deviceName}}",
|
|
30764
|
+
"system.stream-online.body": "Stream for camera {{deviceName}} is back online",
|
|
30765
|
+
"system.stream-online.body.profiled": "{{profile}} stream for camera {{deviceName}} is back online",
|
|
30540
30766
|
"system.stream-offline.title": "Stream offline — {{deviceName}}",
|
|
30541
30767
|
"system.stream-offline.body": "Stream for camera {{deviceName}} is offline",
|
|
30542
30768
|
"system.stream-offline.body.profiled": "{{profile}} stream for camera {{deviceName}} is offline",
|
|
30543
|
-
"system.node-online.title": "Node online — {{nodeName}}",
|
|
30544
|
-
"system.node-online.body.agent": "Agent {{nodeName}} is online",
|
|
30769
|
+
"system.node-online.title": "Node back online — {{nodeName}}",
|
|
30770
|
+
"system.node-online.body.agent": "Agent {{nodeName}} is back online",
|
|
30545
30771
|
"system.node-online.body.agent.after": "Agent {{nodeName}} is back online after {{offlineFor}} offline",
|
|
30546
30772
|
"system.node-offline.title": "Node offline — {{nodeName}}",
|
|
30547
30773
|
"system.node-offline.body.agent": "Agent {{nodeName}} is offline",
|
|
@@ -30553,18 +30779,26 @@ var en_default = {
|
|
|
30553
30779
|
"system.addon-crash-loop.body": "The {{addons}} addon on node {{nodeId}} crashed {{count}} times in a row and will NOT be restarted. Everything it provides is offline until it is repaired.",
|
|
30554
30780
|
"system.detection-blind.title": "Detecting nothing — {{deviceName}}",
|
|
30555
30781
|
"system.detection-blind.body": {
|
|
30556
|
-
"one": "Camera {{deviceName}}
|
|
30557
|
-
"other": "Camera {{deviceName}}
|
|
30782
|
+
"one": "Camera {{deviceName}} moved for {{minutes}} minute over {{sessions}} sessions and the detector produced nothing at all, not even a weak candidate",
|
|
30783
|
+
"other": "Camera {{deviceName}} moved for {{minutes}} minutes over {{sessions}} sessions and the detector produced nothing at all, not even a weak candidate"
|
|
30558
30784
|
},
|
|
30785
|
+
"system.motion-noisy.title": "Constant motion, no subject — {{deviceName}}",
|
|
30786
|
+
"system.motion-noisy.body": "Camera {{deviceName}} has been in motion {{duty}}% of the last {{minutes}} minutes ({{sessions}} sessions) without a single detection. The detector is fine — tune this camera's motion zones or sensitivity.",
|
|
30787
|
+
"system.motion-calm.title": "Motion calm again — {{deviceName}}",
|
|
30788
|
+
"system.motion-calm.body": "Camera {{deviceName}} is no longer in constant motion ({{duty}}% of the last {{minutes}} minutes, {{sessions}} sessions)",
|
|
30559
30789
|
"system.recording-stalled.title": "Recording stalled — {{deviceName}}",
|
|
30560
30790
|
"system.recording-stalled.body": {
|
|
30561
30791
|
"one": "Camera {{deviceName}} has not written a recording segment for {{minutes}} minute",
|
|
30562
30792
|
"other": "Camera {{deviceName}} has not written a recording segment for {{minutes}} minutes"
|
|
30563
30793
|
},
|
|
30564
|
-
"system.recording-healthy.title": "Recording
|
|
30794
|
+
"system.recording-healthy.title": "Recording again — {{deviceName}}",
|
|
30565
30795
|
"system.recording-healthy.body": "Camera {{deviceName}} is writing recording segments again",
|
|
30566
30796
|
"system.recording-degraded.title": "Recording degraded — {{deviceName}}",
|
|
30567
30797
|
"system.recording-degraded.body": "Camera {{deviceName}} exhausted its writer restart budget ({{message}})",
|
|
30798
|
+
"system.device-attention-required.title": "{{title}}",
|
|
30799
|
+
"system.device-attention-required.body": "{{message}}",
|
|
30800
|
+
"system.device-attention-resolved.title": "Resolved: {{title}}",
|
|
30801
|
+
"system.device-attention-resolved.body": "{{deviceName}} no longer needs attention",
|
|
30568
30802
|
"system.storage-location-low.title": "Storage low — {{locationName}}",
|
|
30569
30803
|
"system.storage-location-low.body": "Location {{locationName}} is down to {{freePercent}}% free ({{reason}})",
|
|
30570
30804
|
"system.storage-location-critical.title": "Storage critical — {{locationName}}",
|
|
@@ -30822,7 +31056,13 @@ var en_default = {
|
|
|
30822
31056
|
"calendar-events.line.timed": "• {{when}} {{title}}{{where}}",
|
|
30823
31057
|
"calendar-events.line.allDay": "• All day {{title}}{{where}}",
|
|
30824
31058
|
"calendar-events.where": " @ {{location}}",
|
|
30825
|
-
"calendar-events.more": "…and {{count}} more"
|
|
31059
|
+
"calendar-events.more": "…and {{count}} more",
|
|
31060
|
+
"system.rule-broken.title": "Notification rule broken — {{ruleName}}",
|
|
31061
|
+
"system.rule-broken.body.unparseable": "The rule \"{{ruleName}}\" cannot be loaded and sends nothing. Open it and save it again.",
|
|
31062
|
+
"system.rule-broken.body.device-deleted": "The rule \"{{ruleName}}\" still names a deleted device ({{offendingIds}}) and was switched off. Pick the device again.",
|
|
31063
|
+
"system.rule-broken.body.target-deleted": "The rule \"{{ruleName}}\" still sends to a deleted destination ({{offendingIds}}) and was switched off. Pick a destination again.",
|
|
31064
|
+
"system.rule-repaired.title": "Notification rule works again — {{ruleName}}",
|
|
31065
|
+
"system.rule-repaired.body": "The rule \"{{ruleName}}\" loads and names nothing deleted any more."
|
|
30826
31066
|
};
|
|
30827
31067
|
//#endregion
|
|
30828
31068
|
//#region src/notification-center/texts/it.json
|
|
@@ -30907,8 +31147,8 @@ var it_default = {
|
|
|
30907
31147
|
"occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
|
|
30908
31148
|
"occupancy.op.occupied": "occupata",
|
|
30909
31149
|
"occupancy.op.free": "libera",
|
|
30910
|
-
"system.device-online.title": "
|
|
30911
|
-
"system.device-online.body": "Il dispositivo {{deviceName}} è online",
|
|
31150
|
+
"system.device-online.title": "Di nuovo online — {{deviceName}}",
|
|
31151
|
+
"system.device-online.body": "Il dispositivo {{deviceName}} è di nuovo online",
|
|
30912
31152
|
"system.device-offline.title": "Offline — {{deviceName}}",
|
|
30913
31153
|
"system.device-offline.body": "Il dispositivo {{deviceName}} è offline",
|
|
30914
31154
|
"system.device-disabled.title": "Spento — {{deviceName}}",
|
|
@@ -30921,14 +31161,14 @@ var it_default = {
|
|
|
30921
31161
|
"system.device-battery-normal.title": "Batteria rientrata — {{deviceName}}",
|
|
30922
31162
|
"system.device-battery-normal.body": "{{deviceName}} è risalito al {{percentage}}% di batteria",
|
|
30923
31163
|
"system.device-battery-normal.body.binary": "{{deviceName}} non segnala più la batteria scarica",
|
|
30924
|
-
"system.stream-online.title": "Stream online — {{deviceName}}",
|
|
30925
|
-
"system.stream-online.body": "Lo stream della camera {{deviceName}} è online",
|
|
30926
|
-
"system.stream-online.body.profiled": "Lo stream {{profile}} della camera {{deviceName}} è online",
|
|
31164
|
+
"system.stream-online.title": "Stream di nuovo online — {{deviceName}}",
|
|
31165
|
+
"system.stream-online.body": "Lo stream della camera {{deviceName}} è di nuovo online",
|
|
31166
|
+
"system.stream-online.body.profiled": "Lo stream {{profile}} della camera {{deviceName}} è di nuovo online",
|
|
30927
31167
|
"system.stream-offline.title": "Stream offline — {{deviceName}}",
|
|
30928
31168
|
"system.stream-offline.body": "Lo stream della camera {{deviceName}} è offline",
|
|
30929
31169
|
"system.stream-offline.body.profiled": "Lo stream {{profile}} della camera {{deviceName}} è offline",
|
|
30930
|
-
"system.node-online.title": "Nodo online — {{nodeName}}",
|
|
30931
|
-
"system.node-online.body.agent": "L'agent {{nodeName}} è online",
|
|
31170
|
+
"system.node-online.title": "Nodo di nuovo online — {{nodeName}}",
|
|
31171
|
+
"system.node-online.body.agent": "L'agent {{nodeName}} è di nuovo online",
|
|
30932
31172
|
"system.node-online.body.agent.after": "L'agent {{nodeName}} è di nuovo online dopo {{offlineFor}} offline",
|
|
30933
31173
|
"system.node-offline.title": "Nodo offline — {{nodeName}}",
|
|
30934
31174
|
"system.node-offline.body.agent": "L'agent {{nodeName}} è offline",
|
|
@@ -30943,10 +31183,14 @@ var it_default = {
|
|
|
30943
31183
|
"one": "La camera {{deviceName}} non scrive un segmento da {{minutes}} minuto",
|
|
30944
31184
|
"other": "La camera {{deviceName}} non scrive un segmento da {{minutes}} minuti"
|
|
30945
31185
|
},
|
|
30946
|
-
"system.recording-healthy.title": "
|
|
31186
|
+
"system.recording-healthy.title": "Registra di nuovo — {{deviceName}}",
|
|
30947
31187
|
"system.recording-healthy.body": "La camera {{deviceName}} scrive di nuovo i segmenti",
|
|
30948
31188
|
"system.recording-degraded.title": "Registrazione degradata — {{deviceName}}",
|
|
30949
31189
|
"system.recording-degraded.body": "La camera {{deviceName}} ha esaurito i riavvii del writer ({{message}})",
|
|
31190
|
+
"system.device-attention-required.title": "{{title}}",
|
|
31191
|
+
"system.device-attention-required.body": "{{message}}",
|
|
31192
|
+
"system.device-attention-resolved.title": "Risolto: {{title}}",
|
|
31193
|
+
"system.device-attention-resolved.body": "{{deviceName}} non richiede più intervento",
|
|
30950
31194
|
"system.storage-location-low.title": "Storage basso — {{locationName}}",
|
|
30951
31195
|
"system.storage-location-low.body": "La location {{locationName}} è al {{freePercent}}% libero ({{reason}})",
|
|
30952
31196
|
"system.storage-location-critical.title": "Storage critico — {{locationName}}",
|
|
@@ -30955,9 +31199,13 @@ var it_default = {
|
|
|
30955
31199
|
"system.storage-location-ok.body": "La location {{locationName}} ha recuperato spazio libero",
|
|
30956
31200
|
"system.detection-blind.title": "Senza detection — {{deviceName}}",
|
|
30957
31201
|
"system.detection-blind.body": {
|
|
30958
|
-
"one": "La camera {{deviceName}}
|
|
30959
|
-
"other": "La camera {{deviceName}}
|
|
31202
|
+
"one": "La camera {{deviceName}} ha avuto movimento per {{minutes}} minuto in {{sessions}} sessioni e il detector non ha prodotto nulla, nemmeno un candidato debole",
|
|
31203
|
+
"other": "La camera {{deviceName}} ha avuto movimento per {{minutes}} minuti in {{sessions}} sessioni e il detector non ha prodotto nulla, nemmeno un candidato debole"
|
|
30960
31204
|
},
|
|
31205
|
+
"system.motion-noisy.title": "Movimento continuo senza soggetto — {{deviceName}}",
|
|
31206
|
+
"system.motion-noisy.body": "La camera {{deviceName}} è stata in movimento il {{duty}}% degli ultimi {{minutes}} minuti ({{sessions}} sessioni) senza alcun rilevamento. Il detector funziona — regola le zone di movimento o la sensibilità di questa camera.",
|
|
31207
|
+
"system.motion-calm.title": "Movimento di nuovo calmo — {{deviceName}}",
|
|
31208
|
+
"system.motion-calm.body": "La camera {{deviceName}} non è più in movimento continuo ({{duty}}% degli ultimi {{minutes}} minuti, {{sessions}} sessioni)",
|
|
30961
31209
|
"system.addon-update-available.title": {
|
|
30962
31210
|
"one": "{{count}} aggiornamento addon disponibile",
|
|
30963
31211
|
"other": "{{count}} aggiornamenti addon disponibili"
|
|
@@ -31209,7 +31457,13 @@ var it_default = {
|
|
|
31209
31457
|
"calendar-events.line.timed": "• {{when}} {{title}}{{where}}",
|
|
31210
31458
|
"calendar-events.line.allDay": "• Tutto il giorno {{title}}{{where}}",
|
|
31211
31459
|
"calendar-events.where": " @ {{location}}",
|
|
31212
|
-
"calendar-events.more": "…e altri {{count}}"
|
|
31460
|
+
"calendar-events.more": "…e altri {{count}}",
|
|
31461
|
+
"system.rule-broken.title": "Regola di notifica rotta — {{ruleName}}",
|
|
31462
|
+
"system.rule-broken.body.unparseable": "La regola \"{{ruleName}}\" non si carica e non invia nulla. Aprila e salvala di nuovo.",
|
|
31463
|
+
"system.rule-broken.body.device-deleted": "La regola \"{{ruleName}}\" usa ancora un dispositivo eliminato ({{offendingIds}}) ed è stata disattivata. Scegli di nuovo il dispositivo.",
|
|
31464
|
+
"system.rule-broken.body.target-deleted": "La regola \"{{ruleName}}\" invia ancora a una destinazione eliminata ({{offendingIds}}) ed è stata disattivata. Scegli di nuovo una destinazione.",
|
|
31465
|
+
"system.rule-repaired.title": "Regola di notifica di nuovo funzionante — {{ruleName}}",
|
|
31466
|
+
"system.rule-repaired.body": "La regola \"{{ruleName}}\" si carica e non usa più nulla di eliminato."
|
|
31213
31467
|
};
|
|
31214
31468
|
//#endregion
|
|
31215
31469
|
//#region src/notification-center/text-catalog.ts
|
|
@@ -33480,6 +33734,10 @@ function systemEventKeys(kind) {
|
|
|
33480
33734
|
titleKey: `system.${kind}.title`,
|
|
33481
33735
|
bodyKey: `system.${kind}.body.cleanWater`
|
|
33482
33736
|
};
|
|
33737
|
+
if (kind === "rule-broken") return {
|
|
33738
|
+
titleKey: `system.${kind}.title`,
|
|
33739
|
+
bodyKey: `system.${kind}.body.device-deleted`
|
|
33740
|
+
};
|
|
33483
33741
|
return {
|
|
33484
33742
|
titleKey: `system.${kind}.title`,
|
|
33485
33743
|
bodyKey: `system.${kind}.body`
|
|
@@ -33507,6 +33765,7 @@ function systemEventSubject(kind, scope) {
|
|
|
33507
33765
|
if (kind === "node-online" || kind === "node-offline") return `node:${scope.nodeId ?? "hub"}`;
|
|
33508
33766
|
if (kind === "node-inference-unavailable") return `node-inference:${scope.nodeId ?? "hub"}`;
|
|
33509
33767
|
if (kind === "detection-blind") return `detection-blind:${scope.deviceId ?? 0}`;
|
|
33768
|
+
if (kind === "motion-noisy" || kind === "motion-calm") return `motion-noise:${scope.deviceId ?? 0}`;
|
|
33510
33769
|
if (kind === "addon-crash-loop") return `addon-crash:${scope.nodeId ?? "hub"}:${TEST_CRASHED_RUNNER}`;
|
|
33511
33770
|
if (kind === "addon-update-available" || kind === "server-update-available") return `package:${scope.packageName ?? "@camstack/server"}`;
|
|
33512
33771
|
if (kind === "wrapper-update-available") return `wrapper:${scope.nodeId ?? "hub"}`;
|
|
@@ -33652,6 +33911,8 @@ function planRuleTest(input) {
|
|
|
33652
33911
|
seconds: "30",
|
|
33653
33912
|
devices: "cuda:0",
|
|
33654
33913
|
minutes: "15",
|
|
33914
|
+
duty: "55",
|
|
33915
|
+
sessions: "96",
|
|
33655
33916
|
addons: TEST_CRASHED_RUNNER,
|
|
33656
33917
|
list: "@camstack/addon-pipeline 1.0.1 (current 1.0.0)",
|
|
33657
33918
|
nodes: "hub",
|
|
@@ -39735,6 +39996,99 @@ var NcRecordedCutMemo = class {
|
|
|
39735
39996
|
}
|
|
39736
39997
|
};
|
|
39737
39998
|
//#endregion
|
|
39999
|
+
//#region src/notification-center/notification-label-layout.ts
|
|
40000
|
+
/**
|
|
40001
|
+
* Where each box's caption goes on a notification picture — pure geometry,
|
|
40002
|
+
* rects in, placements out (D897 follow-up, 2026-10-22).
|
|
40003
|
+
*
|
|
40004
|
+
* A caption drawn INSIDE its box used to cover the subject whole on a small
|
|
40005
|
+
* box, and a caption next to a crowd landed on a neighbour's box. The rule:
|
|
40006
|
+
*
|
|
40007
|
+
* 1. Try, in order: OUTSIDE above the box (inside the picture), OUTSIDE below
|
|
40008
|
+
* it, then INSIDE its top edge — the last only when the box is big enough
|
|
40009
|
+
* that the plate is a thin band of it: plate height ≤
|
|
40010
|
+
* {@link NC_LABEL_INSIDE_MAX_HEIGHT_FRACTION} of the box height and plate
|
|
40011
|
+
* width ≤ the box width. A fifth of the height leaves the head and torso of
|
|
40012
|
+
* an upright person, or most of a vehicle, visible; anything taller is the
|
|
40013
|
+
* caption hiding what it names.
|
|
40014
|
+
* 2. A candidate is refused when its plate intersects ANY other box drawn on
|
|
40015
|
+
* the picture, or a plate already placed.
|
|
40016
|
+
* 3. The subject is placed first; the other tracks follow by score, highest
|
|
40017
|
+
* first, so a crowd never costs the subject its caption.
|
|
40018
|
+
* 4. The font never shrinks to make a plate fit: no fitting candidate means
|
|
40019
|
+
* no caption for that box. A box without its caption still carries its
|
|
40020
|
+
* outline and colour.
|
|
40021
|
+
*
|
|
40022
|
+
* Coordinates are the picture's own pixels, whatever it is (a scene or a crop).
|
|
40023
|
+
*/
|
|
40024
|
+
/** The inside placement is allowed only when the plate is at most this fraction of the box height. */
|
|
40025
|
+
var NC_LABEL_INSIDE_MAX_HEIGHT_FRACTION = .2;
|
|
40026
|
+
/** A caption is never drawn smaller than this, in output pixels. */
|
|
40027
|
+
var NC_LABEL_MIN_FONT_PX = 14;
|
|
40028
|
+
/** The dark plate a caption sits on, for this text at this font size. */
|
|
40029
|
+
function labelPlateSize(label, fontSize) {
|
|
40030
|
+
return {
|
|
40031
|
+
w: Math.round(label.length * fontSize * .62 + fontSize * .8),
|
|
40032
|
+
h: Math.round(fontSize * 1.35)
|
|
40033
|
+
};
|
|
40034
|
+
}
|
|
40035
|
+
/** Positive-area overlap; rectangles that only touch do not intersect. */
|
|
40036
|
+
function intersects(a, b) {
|
|
40037
|
+
return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
|
40038
|
+
}
|
|
40039
|
+
function captionOrder(boxes) {
|
|
40040
|
+
const rank = (b) => b.role === "subject" ? 0 : 1;
|
|
40041
|
+
return boxes.map((_, i) => i).sort((i, j) => {
|
|
40042
|
+
const a = boxes[i];
|
|
40043
|
+
const b = boxes[j];
|
|
40044
|
+
if (a === void 0 || b === void 0) return 0;
|
|
40045
|
+
return rank(a) - rank(b) || (b.score ?? 0) - (a.score ?? 0) || i - j;
|
|
40046
|
+
});
|
|
40047
|
+
}
|
|
40048
|
+
function candidates(box, plate, width, height) {
|
|
40049
|
+
if (plate.w > width) return [];
|
|
40050
|
+
const x = Math.min(box.x, Math.max(0, width - plate.w));
|
|
40051
|
+
const out = [];
|
|
40052
|
+
if (box.y - plate.h >= 0) out.push({
|
|
40053
|
+
x,
|
|
40054
|
+
y: box.y - plate.h,
|
|
40055
|
+
...plate,
|
|
40056
|
+
where: "above"
|
|
40057
|
+
});
|
|
40058
|
+
if (box.y + box.h + plate.h <= height) out.push({
|
|
40059
|
+
x,
|
|
40060
|
+
y: box.y + box.h,
|
|
40061
|
+
...plate,
|
|
40062
|
+
where: "below"
|
|
40063
|
+
});
|
|
40064
|
+
if (plate.h <= box.h * NC_LABEL_INSIDE_MAX_HEIGHT_FRACTION && plate.w <= box.w) out.push({
|
|
40065
|
+
x,
|
|
40066
|
+
y: box.y,
|
|
40067
|
+
...plate,
|
|
40068
|
+
where: "inside"
|
|
40069
|
+
});
|
|
40070
|
+
return out;
|
|
40071
|
+
}
|
|
40072
|
+
/** Place every caption by the rule above. */
|
|
40073
|
+
function layoutBoxLabels(input) {
|
|
40074
|
+
const { width, height, boxes } = input;
|
|
40075
|
+
const fontSize = Math.max(NC_LABEL_MIN_FONT_PX, input.fontSize);
|
|
40076
|
+
const placements = /* @__PURE__ */ new Map();
|
|
40077
|
+
let dropped = 0;
|
|
40078
|
+
for (const i of captionOrder(boxes)) {
|
|
40079
|
+
const box = boxes[i];
|
|
40080
|
+
if (box === void 0 || box.label.length === 0) continue;
|
|
40081
|
+
const chosen = candidates(box, labelPlateSize(box.label, fontSize), width, height).find((c) => !boxes.some((other, j) => j !== i && intersects(c, other)) && ![...placements.values()].some((p) => intersects(c, p)));
|
|
40082
|
+
if (chosen === void 0) dropped += 1;
|
|
40083
|
+
else placements.set(i, chosen);
|
|
40084
|
+
}
|
|
40085
|
+
return {
|
|
40086
|
+
placements: boxes.map((_, i) => placements.get(i) ?? null),
|
|
40087
|
+
placed: placements.size,
|
|
40088
|
+
dropped
|
|
40089
|
+
};
|
|
40090
|
+
}
|
|
40091
|
+
//#endregion
|
|
39738
40092
|
//#region src/notification-center/trigger-zone-still.ts
|
|
39739
40093
|
/**
|
|
39740
40094
|
* How far a candidate's instant may sit from the nearest trail point before the
|
|
@@ -39857,6 +40211,15 @@ var NC_IMAGE_OTHER_COLOR = "#f2f2f2";
|
|
|
39857
40211
|
var NC_IMAGE_ZONE_COLOR = "#29b6f6";
|
|
39858
40212
|
/** Two tracks whose boxes overlap this much at one instant are the same subject (a fused id). */
|
|
39859
40213
|
var SAME_SUBJECT_IOU = .8;
|
|
40214
|
+
/**
|
|
40215
|
+
* A box of the SUBJECT'S CLASS overlapping the subject this much at one
|
|
40216
|
+
* instant is the subject itself — an unfused piece L2 has not joined yet
|
|
40217
|
+
* (D683 fuses 10–20 s later; a push leaves ~6 s after the trigger). The
|
|
40218
|
+
* detector's per-class NMS (`NMS_IOU_DEFAULT` 0.45) keeps one of two
|
|
40219
|
+
* same-class boxes above 0.45 in a frame, so two REAL subjects of one class
|
|
40220
|
+
* cannot overlap past it; 0.5 leaves a margin over that.
|
|
40221
|
+
*/
|
|
40222
|
+
var NC_IMAGE_SAME_CLASS_SUBJECT_IOU = .5;
|
|
39860
40223
|
var JPEG_QUALITY = 85;
|
|
39861
40224
|
/** The caption of a box: class (or label) and confidence — "dog 87%". Language-free. */
|
|
39862
40225
|
function imageLabelText(name, confidence) {
|
|
@@ -39889,15 +40252,21 @@ function toPixels(box, width, height) {
|
|
|
39889
40252
|
h: Math.max(1, Math.min(Math.round(box.h * height), height - y))
|
|
39890
40253
|
};
|
|
39891
40254
|
}
|
|
40255
|
+
/** {@link notificationOverlay}'s SVG alone. */
|
|
40256
|
+
function notificationOverlaySvg(input) {
|
|
40257
|
+
return notificationOverlay(input).svg;
|
|
40258
|
+
}
|
|
39892
40259
|
/**
|
|
39893
|
-
* The SVG drawn over a
|
|
39894
|
-
*
|
|
39895
|
-
*
|
|
40260
|
+
* The SVG drawn over a picture of `width` × `height`. The subject is drawn
|
|
40261
|
+
* LAST so a crowd never hides it. Captions sit on a dark plate, white text,
|
|
40262
|
+
* where `layoutBoxLabels` puts them — outside above, outside below, inside a
|
|
40263
|
+
* box only when it covers a thin band of it, never on another box or caption,
|
|
40264
|
+
* and nowhere at all rather than over the subject.
|
|
39896
40265
|
*/
|
|
39897
|
-
function
|
|
40266
|
+
function notificationOverlay(input) {
|
|
39898
40267
|
const { width, height } = input;
|
|
39899
40268
|
const strokes = overlayStrokes(width, height);
|
|
39900
|
-
const fontSize = Math.max(
|
|
40269
|
+
const fontSize = Math.max(NC_LABEL_MIN_FONT_PX, Math.round(height / 36));
|
|
39901
40270
|
const parts = [];
|
|
39902
40271
|
for (const zone of input.zones) {
|
|
39903
40272
|
if (zone.length < 3) continue;
|
|
@@ -39909,19 +40278,29 @@ function notificationOverlaySvg(input) {
|
|
|
39909
40278
|
parts.push(`<polyline data-role="subject-path" points="${require_clip_model_registry.subjectPathSvgPoints(run, width, height)}" fill="none" stroke="${NC_IMAGE_SUBJECT_COLOR}" stroke-width="${String(strokes.subject)}" stroke-linejoin="round" stroke-linecap="round" stroke-opacity="0.9"/>`);
|
|
39910
40279
|
}
|
|
39911
40280
|
const ordered = [...input.boxes.filter((b) => b.role === "other"), ...input.boxes.filter((b) => b.role === "subject")];
|
|
39912
|
-
|
|
40281
|
+
const layout = layoutBoxLabels({
|
|
40282
|
+
width,
|
|
40283
|
+
height,
|
|
40284
|
+
fontSize,
|
|
40285
|
+
boxes: ordered
|
|
40286
|
+
});
|
|
40287
|
+
const plates = [];
|
|
40288
|
+
ordered.forEach((box, i) => {
|
|
39913
40289
|
const subject = box.role === "subject";
|
|
39914
40290
|
const color = subject ? NC_IMAGE_SUBJECT_COLOR : NC_IMAGE_OTHER_COLOR;
|
|
39915
40291
|
const stroke = subject ? strokes.subject : strokes.other;
|
|
39916
40292
|
parts.push(`<rect data-role="${box.role}" x="${String(box.x)}" y="${String(box.y)}" width="${String(box.w)}" height="${String(box.h)}" fill="none" stroke="${color}" stroke-width="${String(stroke)}"/>`);
|
|
39917
|
-
|
|
39918
|
-
|
|
39919
|
-
|
|
39920
|
-
|
|
39921
|
-
|
|
39922
|
-
|
|
39923
|
-
|
|
39924
|
-
|
|
40293
|
+
const plate = layout.placements[i];
|
|
40294
|
+
if (plate === null || plate === void 0) return;
|
|
40295
|
+
plates.push(`<rect data-role="${box.role}-plate" x="${String(plate.x)}" y="${String(plate.y)}" width="${String(plate.w)}" height="${String(plate.h)}" fill="#000000" fill-opacity="0.72" stroke="${color}" stroke-width="${String(subject ? 2 : 1)}"/>`, `<text data-role="${box.role}-label" x="${String(plate.x + Math.round(fontSize * .4))}" y="${String(plate.y + Math.round(plate.h * .75))}" font-family="DejaVu Sans, Helvetica, Arial, sans-serif" font-size="${String(fontSize)}" font-weight="${subject ? "700" : "400"}" fill="#ffffff">${escapeXml$2(box.label)}</text>`);
|
|
40296
|
+
});
|
|
40297
|
+
return {
|
|
40298
|
+
svg: `<svg xmlns="http://www.w3.org/2000/svg" width="${String(width)}" height="${String(height)}">${[...parts, ...plates].join("")}</svg>`,
|
|
40299
|
+
labels: {
|
|
40300
|
+
placed: layout.placed,
|
|
40301
|
+
dropped: layout.dropped
|
|
40302
|
+
}
|
|
40303
|
+
};
|
|
39925
40304
|
}
|
|
39926
40305
|
/** The pixel rectangle a `crop` cuts from a `width` × `height` frame, or null when too small. */
|
|
39927
40306
|
function subjectCropRect(box, width, height) {
|
|
@@ -39951,11 +40330,18 @@ function iou$4(a, b) {
|
|
|
39951
40330
|
}
|
|
39952
40331
|
/**
|
|
39953
40332
|
* The tracks to draw thin: every track at the instant except the subject —
|
|
39954
|
-
* the same id,
|
|
39955
|
-
* survivor's name)
|
|
39956
|
-
|
|
39957
|
-
|
|
39958
|
-
|
|
40333
|
+
* the same id, a box that is the subject's (a fused id reads under its
|
|
40334
|
+
* survivor's name), or a box of the subject's class over it past what NMS
|
|
40335
|
+
* lets two real subjects share (an unfused piece of the same subject).
|
|
40336
|
+
*/
|
|
40337
|
+
function otherTracks(tracks, subjectTrackId, subjectBox, subjectClassName) {
|
|
40338
|
+
return tracks.filter((t) => {
|
|
40339
|
+
if (t.trackId === subjectTrackId) return false;
|
|
40340
|
+
if (subjectBox === null) return true;
|
|
40341
|
+
const overlap = iou$4(t.bbox, subjectBox);
|
|
40342
|
+
if (overlap >= SAME_SUBJECT_IOU) return false;
|
|
40343
|
+
return !(t.className === subjectClassName && overlap >= NC_IMAGE_SAME_CLASS_SUBJECT_IOU);
|
|
40344
|
+
});
|
|
39959
40345
|
}
|
|
39960
40346
|
/**
|
|
39961
40347
|
* What picture a ROW asks for. A row written since D897 carries `mediaImage`;
|
|
@@ -39972,9 +40358,15 @@ function payloadImageMode(payload) {
|
|
|
39972
40358
|
if (payload.media === "keyFrame") return "clean";
|
|
39973
40359
|
return "crop";
|
|
39974
40360
|
}
|
|
39975
|
-
/**
|
|
40361
|
+
/**
|
|
40362
|
+
* The zone ids a row's picture outlines: the zone(s) THIS event triggered,
|
|
40363
|
+
* when it asked — never every zone the rule lists (a dog in Uscio drew Aiuola
|
|
40364
|
+
* too). A row that asked but froze no `matchedZoneIds` (written before the
|
|
40365
|
+
* field) draws none: an outline of a zone the subject never entered is a
|
|
40366
|
+
* picture that lies about its own reason.
|
|
40367
|
+
*/
|
|
39976
40368
|
function payloadZoneOverlayIds(payload) {
|
|
39977
|
-
if (payload.mediaShowTriggerZone === true) return payload.
|
|
40369
|
+
if (payload.mediaShowTriggerZone === true) return payload.matchedZoneIds ?? [];
|
|
39978
40370
|
if (payload.mediaImage === void 0) return payload.mediaZoneIds ?? [];
|
|
39979
40371
|
return [];
|
|
39980
40372
|
}
|
|
@@ -40063,7 +40455,8 @@ async function renderNotificationImage(req) {
|
|
|
40063
40455
|
const boxes = req.mode === "scene-boxes" ? [...req.others.map((o) => ({
|
|
40064
40456
|
...toPixels(o, outW, outH),
|
|
40065
40457
|
role: "other",
|
|
40066
|
-
label: o.label
|
|
40458
|
+
label: o.label,
|
|
40459
|
+
...o.confidence !== void 0 ? { score: o.confidence } : {}
|
|
40067
40460
|
})), ...req.subject !== null ? [{
|
|
40068
40461
|
...toPixels(req.subject, outW, outH),
|
|
40069
40462
|
role: "subject",
|
|
@@ -40081,16 +40474,18 @@ async function renderNotificationImage(req) {
|
|
|
40081
40474
|
height: outH,
|
|
40082
40475
|
fit: "fill"
|
|
40083
40476
|
});
|
|
40477
|
+
let labels;
|
|
40084
40478
|
if (boxes.length > 0 || zones.length > 0 || paths.length > 0) {
|
|
40085
|
-
const
|
|
40479
|
+
const overlay = notificationOverlay({
|
|
40086
40480
|
width: outW,
|
|
40087
40481
|
height: outH,
|
|
40088
40482
|
boxes,
|
|
40089
40483
|
zones,
|
|
40090
40484
|
paths
|
|
40091
40485
|
});
|
|
40486
|
+
if (boxes.length > 0) labels = overlay.labels;
|
|
40092
40487
|
pipeline = pipeline.composite([{
|
|
40093
|
-
input: Buffer.from(svg),
|
|
40488
|
+
input: Buffer.from(overlay.svg),
|
|
40094
40489
|
top: 0,
|
|
40095
40490
|
left: 0
|
|
40096
40491
|
}]);
|
|
@@ -40100,7 +40495,8 @@ async function renderNotificationImage(req) {
|
|
|
40100
40495
|
return {
|
|
40101
40496
|
bytes: toArrayBufferBytes(out),
|
|
40102
40497
|
name: `${rendered}.jpg`,
|
|
40103
|
-
rendered
|
|
40498
|
+
rendered,
|
|
40499
|
+
...labels !== void 0 ? { labels } : {}
|
|
40104
40500
|
};
|
|
40105
40501
|
}
|
|
40106
40502
|
var NcNotificationImageStage = class {
|
|
@@ -40174,10 +40570,29 @@ var NcNotificationImageStage = class {
|
|
|
40174
40570
|
...subjectBox,
|
|
40175
40571
|
label: imageLabelText(payload.subject.label ?? payload.subject.className, payload.subject.confidence)
|
|
40176
40572
|
} : null;
|
|
40177
|
-
const
|
|
40573
|
+
const othersDrawn = mode === "scene-boxes" ? otherTracks(await this.tracksAt(camera, source.timestamp), subjectTrackId, subjectBox, payload.subject.className) : [];
|
|
40574
|
+
if (mode === "scene-boxes") this.deps.logger.info("scene-boxes: other boxes drawn", {
|
|
40575
|
+
tags: { deviceId: camera },
|
|
40576
|
+
meta: {
|
|
40577
|
+
ruleId: entry.ruleId,
|
|
40578
|
+
recordId: entry.recordId,
|
|
40579
|
+
instant: source.timestamp,
|
|
40580
|
+
drawn: othersDrawn.map((t) => `${t.trackId}/${t.className}/dt=${t.dtMs ?? "?"}ms/${t.interpolated === true ? "interpolated" : "exact"}`).join(",")
|
|
40581
|
+
}
|
|
40582
|
+
});
|
|
40583
|
+
const others = mode === "scene-boxes" ? othersDrawn.map((t) => ({
|
|
40178
40584
|
...t.bbox,
|
|
40179
|
-
label: imageLabelText(t.label ?? t.className, t.confidence)
|
|
40585
|
+
label: imageLabelText(t.label ?? t.className, t.confidence),
|
|
40586
|
+
...t.confidence !== void 0 ? { confidence: t.confidence } : {}
|
|
40180
40587
|
})) : [];
|
|
40588
|
+
if (mode !== "crop" && input.withSubject && payload.mediaShowTriggerZone === true && payload.matchedZoneIds === void 0) this.deps.logger.info("trigger zone not drawn — the row never froze which zone fired", {
|
|
40589
|
+
tags: { deviceId: camera },
|
|
40590
|
+
meta: {
|
|
40591
|
+
ruleId: entry.ruleId,
|
|
40592
|
+
recordId: entry.recordId,
|
|
40593
|
+
ruleZoneIds: (payload.triggerZoneIds ?? []).join(",")
|
|
40594
|
+
}
|
|
40595
|
+
});
|
|
40181
40596
|
const zones = mode === "crop" || zoneIds.length === 0 ? [] : await this.zonePolygons(camera, zoneIds);
|
|
40182
40597
|
try {
|
|
40183
40598
|
const image = await this.render({
|
|
@@ -40190,6 +40605,7 @@ var NcNotificationImageStage = class {
|
|
|
40190
40605
|
});
|
|
40191
40606
|
const reason = image.rendered === mode ? void 0 : mode === "crop" ? "no-subject-box" : "no-tracks-at-instant";
|
|
40192
40607
|
if (reason !== void 0) this.reportFallback(input, mode, reason);
|
|
40608
|
+
if (image.labels !== void 0) this.reportLabels(input, image.labels);
|
|
40193
40609
|
return reason !== void 0 ? {
|
|
40194
40610
|
...image,
|
|
40195
40611
|
reason
|
|
@@ -40238,6 +40654,19 @@ var NcNotificationImageStage = class {
|
|
|
40238
40654
|
}
|
|
40239
40655
|
});
|
|
40240
40656
|
}
|
|
40657
|
+
/** ONE line per rendered picture: how many captions it carries and how many had no clear spot. */
|
|
40658
|
+
reportLabels(input, labels) {
|
|
40659
|
+
const camera = input.camera === "composite" ? input.entry.payload.subject.deviceId : input.camera;
|
|
40660
|
+
this.deps.logger.debug("notification picture captions laid out", {
|
|
40661
|
+
tags: { deviceId: camera },
|
|
40662
|
+
meta: {
|
|
40663
|
+
ruleId: input.entry.ruleId,
|
|
40664
|
+
recordId: input.entry.recordId,
|
|
40665
|
+
placed: labels.placed,
|
|
40666
|
+
dropped: labels.dropped
|
|
40667
|
+
}
|
|
40668
|
+
});
|
|
40669
|
+
}
|
|
40241
40670
|
/** The subject's trail, read once per render for its box AND its path. */
|
|
40242
40671
|
async subjectTrail(camera, trackId) {
|
|
40243
40672
|
const read = this.deps.getSubjectTrail;
|
|
@@ -46931,6 +47360,27 @@ function isRecord(value) {
|
|
|
46931
47360
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
46932
47361
|
}
|
|
46933
47362
|
/**
|
|
47363
|
+
* Incident 2026-10-22: a row stored with `groupingSec: 0` failed `NcRuleSchema` (min
|
|
47364
|
+
* {@link NC_GROUPING_MIN_SEC}) and the WHOLE rule was skipped as malformed on every load — a
|
|
47365
|
+
* system-event rule on which a window cannot even apply vanished from the engine and from
|
|
47366
|
+
* `listRules`. A non-positive (or non-numeric) window only ever meant "no window": dropped. A
|
|
47367
|
+
* positive one outside the range is clamped. One bad optional field never costs the rule.
|
|
47368
|
+
*/
|
|
47369
|
+
function repairStoredGroupingSec(value) {
|
|
47370
|
+
if (value === void 0) return { kind: "unchanged" };
|
|
47371
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return {
|
|
47372
|
+
kind: "repaired",
|
|
47373
|
+
from: value,
|
|
47374
|
+
to: null
|
|
47375
|
+
};
|
|
47376
|
+
const clamped = clampGroupingSec(value);
|
|
47377
|
+
return clamped === value ? { kind: "unchanged" } : {
|
|
47378
|
+
kind: "repaired",
|
|
47379
|
+
from: value,
|
|
47380
|
+
to: clamped
|
|
47381
|
+
};
|
|
47382
|
+
}
|
|
47383
|
+
/**
|
|
46934
47384
|
* Project a RAW stored rule value (`data['rule']`, before any Zod parse). A value that is not
|
|
46935
47385
|
* an object, or has no string `delivery`, is returned untouched: the schema parse that follows
|
|
46936
47386
|
* rejects it exactly as before, and a projection must never be what decides a row is valid.
|
|
@@ -46938,9 +47388,12 @@ function isRecord(value) {
|
|
|
46938
47388
|
function projectRawStoredRule(raw) {
|
|
46939
47389
|
if (!isRecord(raw) || typeof raw["delivery"] !== "string") return {
|
|
46940
47390
|
candidate: raw,
|
|
46941
|
-
projection: { kind: "unchanged" }
|
|
47391
|
+
projection: { kind: "unchanged" },
|
|
47392
|
+
groupingRepair: { kind: "unchanged" }
|
|
46942
47393
|
};
|
|
46943
|
-
const
|
|
47394
|
+
const groupingRepair = repairStoredGroupingSec(raw["groupingSec"]);
|
|
47395
|
+
const repaired = withRepairedGrouping(raw, groupingRepair);
|
|
47396
|
+
const groupingSec = repaired["groupingSec"];
|
|
46944
47397
|
const projection = projectRetiredRuleFields({
|
|
46945
47398
|
delivery: raw["delivery"],
|
|
46946
47399
|
conditions: isRecord(raw["conditions"]) ? raw["conditions"] : {},
|
|
@@ -46949,15 +47402,25 @@ function projectRawStoredRule(raw) {
|
|
|
46949
47402
|
...typeof groupingSec === "number" ? { groupingSec } : {}
|
|
46950
47403
|
});
|
|
46951
47404
|
if (projection.kind === "unchanged" || projection.groupingSec === void 0) return {
|
|
46952
|
-
candidate:
|
|
46953
|
-
projection
|
|
47405
|
+
candidate: repaired,
|
|
47406
|
+
projection,
|
|
47407
|
+
groupingRepair
|
|
46954
47408
|
};
|
|
46955
47409
|
return {
|
|
46956
47410
|
candidate: {
|
|
46957
|
-
...
|
|
47411
|
+
...repaired,
|
|
46958
47412
|
groupingSec: projection.groupingSec
|
|
46959
47413
|
},
|
|
46960
|
-
projection
|
|
47414
|
+
projection,
|
|
47415
|
+
groupingRepair
|
|
47416
|
+
};
|
|
47417
|
+
}
|
|
47418
|
+
function withRepairedGrouping(raw, repair) {
|
|
47419
|
+
if (repair.kind === "unchanged") return raw;
|
|
47420
|
+
const { groupingSec: _dropped, ...rest } = raw;
|
|
47421
|
+
return repair.to === null ? rest : {
|
|
47422
|
+
...rest,
|
|
47423
|
+
groupingSec: repair.to
|
|
46961
47424
|
};
|
|
46962
47425
|
}
|
|
46963
47426
|
/**
|
|
@@ -47010,16 +47473,44 @@ var LOAD_LIMIT = 1e4;
|
|
|
47010
47473
|
* Parse a raw body object (as returned from storage — may contain retired keys)
|
|
47011
47474
|
* into an `NcRule` applying the legacy field projection.
|
|
47012
47475
|
*/
|
|
47013
|
-
function bodyToRule(id, raw, onProjection) {
|
|
47014
|
-
const
|
|
47476
|
+
function bodyToRule(id, raw, onProjection, onGroupingRepair) {
|
|
47477
|
+
const parsed = parseStoredRule(id, raw, onProjection, onGroupingRepair);
|
|
47478
|
+
return parsed.ok ? parsed.rule : null;
|
|
47479
|
+
}
|
|
47480
|
+
function parseStoredRule(id, raw, onProjection, onGroupingRepair, onClassWidening) {
|
|
47481
|
+
const { candidate, projection, groupingRepair } = projectRawStoredRule(raw);
|
|
47015
47482
|
const parsed = require_clip_model_registry.NcRuleSchema.safeParse({
|
|
47016
47483
|
...candidate,
|
|
47017
47484
|
id
|
|
47018
47485
|
});
|
|
47019
|
-
if (!parsed.success)
|
|
47486
|
+
if (!parsed.success) {
|
|
47487
|
+
const issue = parsed.error.issues[0];
|
|
47488
|
+
return {
|
|
47489
|
+
ok: false,
|
|
47490
|
+
reason: issue === void 0 ? "invalid" : `${issue.path.join(".") || "(root)"}: ${issue.message}`
|
|
47491
|
+
};
|
|
47492
|
+
}
|
|
47020
47493
|
if (projection.kind === "projected") onProjection?.(id, projection);
|
|
47021
|
-
|
|
47494
|
+
if (groupingRepair.kind === "repaired") onGroupingRepair?.(id, groupingRepair);
|
|
47495
|
+
const media = withNormalizedMedia(parsed.data);
|
|
47496
|
+
const rule = require_clip_model_registry.withSystemEventClasses(media);
|
|
47497
|
+
if (rule !== media) onClassWidening?.(id, media, rule);
|
|
47498
|
+
return {
|
|
47499
|
+
ok: true,
|
|
47500
|
+
rule
|
|
47501
|
+
};
|
|
47502
|
+
}
|
|
47503
|
+
/**
|
|
47504
|
+
* D906: what a rule is stored as — its media policy folded (D897) and, on an
|
|
47505
|
+
* INSTANT system rule, every paired class it touches completed with both
|
|
47506
|
+
* edges. Applied on every WRITE; the load applies the same through
|
|
47507
|
+
* {@link parseStoredRule}, which also says once that it widened a rule.
|
|
47508
|
+
*/
|
|
47509
|
+
function normalizeStoredRule(rule) {
|
|
47510
|
+
return require_clip_model_registry.withSystemEventClasses(withNormalizedMedia(rule));
|
|
47022
47511
|
}
|
|
47512
|
+
/** At most this many skipped rows are NAMED in the WARN line; the count is always whole. */
|
|
47513
|
+
var SKIPPED_ROWS_NAMED_MAX = 10;
|
|
47023
47514
|
/**
|
|
47024
47515
|
* D897: the rule with its media policy in the stored shape — `image` set, the
|
|
47025
47516
|
* legacy `attach` / `frame` / `zoneCrop` folded into it. Applied on LOAD (a read
|
|
@@ -47096,6 +47587,8 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47096
47587
|
now;
|
|
47097
47588
|
newId;
|
|
47098
47589
|
identityIdsByName;
|
|
47590
|
+
/** Rows the last successful {@link load} skipped; `null` before one succeeded. */
|
|
47591
|
+
skipped = null;
|
|
47099
47592
|
/** Has a {@link load} ever completed? See the note there. */
|
|
47100
47593
|
loaded = false;
|
|
47101
47594
|
/** Legacy detection rules naming sounds already reported (D690) — once each. */
|
|
@@ -47107,6 +47600,10 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47107
47600
|
pendingProjections = /* @__PURE__ */ new Map();
|
|
47108
47601
|
/** Rules whose projection was already logged in this process (D692 §9) — once each. */
|
|
47109
47602
|
projectionLogged = /* @__PURE__ */ new Set();
|
|
47603
|
+
/** Rules whose out-of-range `groupingSec` repair was already logged — once each. */
|
|
47604
|
+
groupingRepairLogged = /* @__PURE__ */ new Set();
|
|
47605
|
+
/** Rules whose class widening (D906) was already logged — once each. */
|
|
47606
|
+
classWideningLogged = /* @__PURE__ */ new Set();
|
|
47110
47607
|
constructor(deps) {
|
|
47111
47608
|
this.logger = deps.logger;
|
|
47112
47609
|
this.now = deps.now ?? (() => Date.now());
|
|
@@ -47141,20 +47638,29 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47141
47638
|
try {
|
|
47142
47639
|
const { records } = await this.kind.query({ limit: LOAD_LIMIT });
|
|
47143
47640
|
const next = /* @__PURE__ */ new Map();
|
|
47144
|
-
|
|
47641
|
+
const skippedRows = [];
|
|
47145
47642
|
for (const row of records) {
|
|
47146
|
-
const
|
|
47643
|
+
const parsed = parseStoredRule(row.id, row.data, (ruleId, projection) => {
|
|
47147
47644
|
this.pendingProjections.set(ruleId, projection);
|
|
47148
|
-
});
|
|
47149
|
-
if (
|
|
47150
|
-
|
|
47645
|
+
}, (ruleId, repair) => this.logGroupingRepair(ruleId, repair), (ruleId, before, after) => this.logClassWidening(ruleId, before, after));
|
|
47646
|
+
if (!parsed.ok) {
|
|
47647
|
+
const name = row.data["name"];
|
|
47648
|
+
skippedRows.push({
|
|
47649
|
+
ruleId: row.id,
|
|
47650
|
+
name: typeof name === "string" ? name : null,
|
|
47651
|
+
reason: parsed.reason
|
|
47652
|
+
});
|
|
47151
47653
|
continue;
|
|
47152
47654
|
}
|
|
47153
|
-
next.set(row.id, rule);
|
|
47655
|
+
next.set(row.id, parsed.rule);
|
|
47154
47656
|
}
|
|
47155
47657
|
this.byId.clear();
|
|
47156
47658
|
for (const [id, rule] of next) this.byId.set(id, rule);
|
|
47157
|
-
|
|
47659
|
+
this.skipped = skippedRows;
|
|
47660
|
+
if (skippedRows.length > 0) this.logger.warn("notification rule rows skipped as malformed", { meta: {
|
|
47661
|
+
skipped: skippedRows.length,
|
|
47662
|
+
rows: skippedRows.slice(0, SKIPPED_ROWS_NAMED_MAX)
|
|
47663
|
+
} });
|
|
47158
47664
|
this.loaded = true;
|
|
47159
47665
|
} catch (err) {
|
|
47160
47666
|
this.logger.warn("notification rules load failed — keeping rules already in memory", { meta: {
|
|
@@ -47166,6 +47672,27 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47166
47672
|
for (const id of warnLegacyAudioClassRules([...this.byId.values()], this.audioClassWarned, this.logger)) this.audioClassWarned.add(id);
|
|
47167
47673
|
this.logger.debug("notification rules loaded", { meta: { rules: this.byId.size } });
|
|
47168
47674
|
}
|
|
47675
|
+
/** Say, once per process per rule, that an instant rule was read as its whole class (D906). */
|
|
47676
|
+
logClassWidening(ruleId, before, after) {
|
|
47677
|
+
if (this.classWideningLogged.has(ruleId)) return;
|
|
47678
|
+
this.classWideningLogged.add(ruleId);
|
|
47679
|
+
const had = new Set(before.conditions.systemEvent?.kinds ?? []);
|
|
47680
|
+
this.logger.info("instant system rule read as its whole event class — both edges", { meta: {
|
|
47681
|
+
ruleId,
|
|
47682
|
+
rule: after.name,
|
|
47683
|
+
added: (after.conditions.systemEvent?.kinds ?? []).filter((kind) => !had.has(kind))
|
|
47684
|
+
} });
|
|
47685
|
+
}
|
|
47686
|
+
/** Say, once per process per rule, that an out-of-range stored `groupingSec` was repaired. */
|
|
47687
|
+
logGroupingRepair(ruleId, repair) {
|
|
47688
|
+
if (this.groupingRepairLogged.has(ruleId)) return;
|
|
47689
|
+
this.groupingRepairLogged.add(ruleId);
|
|
47690
|
+
this.logger.warn("rule carries an out-of-range groupingSec — repaired on read", { meta: {
|
|
47691
|
+
ruleId,
|
|
47692
|
+
from: repair.from,
|
|
47693
|
+
to: repair.to
|
|
47694
|
+
} });
|
|
47695
|
+
}
|
|
47169
47696
|
/** Log, once per process per rule, what `bodyToRule` projected from a retired field. */
|
|
47170
47697
|
logPendingProjections() {
|
|
47171
47698
|
for (const [ruleId, projection] of this.pendingProjections) {
|
|
@@ -47234,6 +47761,13 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47234
47761
|
* camera's ring) must ask this first: before the first load the empty list
|
|
47235
47762
|
* is not a fact, and a failed load leaves whatever was already mirrored.
|
|
47236
47763
|
*/
|
|
47764
|
+
/**
|
|
47765
|
+
* The rows the last SUCCESSFUL load could not parse — `null` until one has
|
|
47766
|
+
* succeeded, so "not read yet" never reads as "nothing broken" (D315).
|
|
47767
|
+
*/
|
|
47768
|
+
skippedRows() {
|
|
47769
|
+
return this.skipped;
|
|
47770
|
+
}
|
|
47237
47771
|
isLoaded() {
|
|
47238
47772
|
return this.loaded;
|
|
47239
47773
|
}
|
|
@@ -47263,7 +47797,7 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47263
47797
|
/** Create a new rule. `createdBy` is the SERVER-injected caller userId. */
|
|
47264
47798
|
async create(input, createdBy) {
|
|
47265
47799
|
const now = this.now();
|
|
47266
|
-
const rule =
|
|
47800
|
+
const rule = normalizeStoredRule({
|
|
47267
47801
|
...input,
|
|
47268
47802
|
id: this.newId(),
|
|
47269
47803
|
createdBy,
|
|
@@ -47308,7 +47842,7 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47308
47842
|
createdAt: existing.createdAt,
|
|
47309
47843
|
updatedAt: this.now()
|
|
47310
47844
|
};
|
|
47311
|
-
const updated =
|
|
47845
|
+
const updated = normalizeStoredRule(require_clip_model_registry.NcRuleSchema.parse(candidate));
|
|
47312
47846
|
await this.write(updated);
|
|
47313
47847
|
return updated;
|
|
47314
47848
|
}
|
|
@@ -47348,7 +47882,7 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47348
47882
|
id: ruleId
|
|
47349
47883
|
} : candidate);
|
|
47350
47884
|
if (!parsed.success) return existing;
|
|
47351
|
-
const next =
|
|
47885
|
+
const next = normalizeStoredRule(parsed.data);
|
|
47352
47886
|
const { id: _omit, ...data } = next;
|
|
47353
47887
|
await this.kind.put({
|
|
47354
47888
|
id: ruleId,
|
|
@@ -53377,6 +53911,104 @@ var NcZoneOwnershipCache = class {
|
|
|
53377
53911
|
}
|
|
53378
53912
|
};
|
|
53379
53913
|
//#endregion
|
|
53914
|
+
//#region src/notification-center/rule-health.ts
|
|
53915
|
+
var EMPTY_SNAPSHOT = {
|
|
53916
|
+
timelapse: [],
|
|
53917
|
+
summary: [],
|
|
53918
|
+
alarm: null,
|
|
53919
|
+
mutedDeviceIds: []
|
|
53920
|
+
};
|
|
53921
|
+
function fingerprintOf(reasons) {
|
|
53922
|
+
return reasons.map((r) => `${r.code}:${[...r.ids].sort().join(",")}`).join("|");
|
|
53923
|
+
}
|
|
53924
|
+
/** The deleted subjects a rule still names, from the prune's `kept*` records. */
|
|
53925
|
+
function deletedReasons(rule, prunes) {
|
|
53926
|
+
const snapshot = {
|
|
53927
|
+
...EMPTY_SNAPSHOT,
|
|
53928
|
+
rules: [rule]
|
|
53929
|
+
};
|
|
53930
|
+
const devices = referencedDeviceIds(snapshot);
|
|
53931
|
+
const targets = referencedTargetIds(snapshot);
|
|
53932
|
+
const deadDevices = /* @__PURE__ */ new Set();
|
|
53933
|
+
const deadTargets = /* @__PURE__ */ new Set();
|
|
53934
|
+
for (const p of prunes) {
|
|
53935
|
+
if (p.kind !== "rule" || p.ruleId !== rule.id) continue;
|
|
53936
|
+
if (p.outcome !== "kept" && p.outcome !== "kept-disabled") continue;
|
|
53937
|
+
if (p.subjectKind === "device" && devices.has(Number(p.subject))) deadDevices.add(p.subject);
|
|
53938
|
+
if (p.subjectKind === "target" && targets.has(p.subject)) deadTargets.add(p.subject);
|
|
53939
|
+
}
|
|
53940
|
+
const reasons = [];
|
|
53941
|
+
if (deadDevices.size > 0) reasons.push({
|
|
53942
|
+
code: "device-deleted",
|
|
53943
|
+
ids: [...deadDevices].sort()
|
|
53944
|
+
});
|
|
53945
|
+
if (deadTargets.size > 0) reasons.push({
|
|
53946
|
+
code: "target-deleted",
|
|
53947
|
+
ids: [...deadTargets].sort()
|
|
53948
|
+
});
|
|
53949
|
+
return reasons;
|
|
53950
|
+
}
|
|
53951
|
+
var NcRuleHealthMonitor = class {
|
|
53952
|
+
verdicts = /* @__PURE__ */ new Map();
|
|
53953
|
+
/** Judge every rule once; return the edges to announce. */
|
|
53954
|
+
evaluate(inputs) {
|
|
53955
|
+
const next = /* @__PURE__ */ new Map();
|
|
53956
|
+
const seen = /* @__PURE__ */ new Set();
|
|
53957
|
+
if (inputs.skipped !== null) for (const row of inputs.skipped) {
|
|
53958
|
+
seen.add(row.ruleId);
|
|
53959
|
+
const unparseable = {
|
|
53960
|
+
code: "unparseable",
|
|
53961
|
+
ids: [],
|
|
53962
|
+
detail: row.reason
|
|
53963
|
+
};
|
|
53964
|
+
next.set(row.ruleId, this.verdict(row.name, unparseable, []));
|
|
53965
|
+
}
|
|
53966
|
+
if (inputs.rules !== null) for (const rule of inputs.rules) {
|
|
53967
|
+
seen.add(rule.id);
|
|
53968
|
+
const previous = this.verdicts.get(rule.id);
|
|
53969
|
+
const deleted = inputs.prunes !== null ? deletedReasons(rule, inputs.prunes) : previous?.deleted ?? [];
|
|
53970
|
+
const unparseable = inputs.skipped !== null ? null : previous?.unparseable ?? null;
|
|
53971
|
+
next.set(rule.id, this.verdict(rule.name, unparseable, deleted));
|
|
53972
|
+
}
|
|
53973
|
+
const edges = [];
|
|
53974
|
+
const answeredAll = inputs.rules !== null && inputs.skipped !== null;
|
|
53975
|
+
for (const [ruleId, previous] of this.verdicts) {
|
|
53976
|
+
if (seen.has(ruleId)) continue;
|
|
53977
|
+
if (!answeredAll) next.set(ruleId, previous);
|
|
53978
|
+
}
|
|
53979
|
+
for (const [ruleId, verdict] of next) {
|
|
53980
|
+
const previous = this.verdicts.get(ruleId);
|
|
53981
|
+
const reasons = reasonsOf(verdict);
|
|
53982
|
+
if (reasons.length > 0) {
|
|
53983
|
+
if (previous?.fingerprint !== verdict.fingerprint) edges.push({
|
|
53984
|
+
kind: "broken",
|
|
53985
|
+
ruleId,
|
|
53986
|
+
ruleName: verdict.name,
|
|
53987
|
+
reasons
|
|
53988
|
+
});
|
|
53989
|
+
} else if (previous !== void 0 && reasonsOf(previous).length > 0) edges.push({
|
|
53990
|
+
kind: "repaired",
|
|
53991
|
+
ruleId,
|
|
53992
|
+
ruleName: verdict.name
|
|
53993
|
+
});
|
|
53994
|
+
}
|
|
53995
|
+
this.verdicts.clear();
|
|
53996
|
+
for (const [ruleId, verdict] of next) if (reasonsOf(verdict).length > 0) this.verdicts.set(ruleId, verdict);
|
|
53997
|
+
return edges;
|
|
53998
|
+
}
|
|
53999
|
+
verdict(name, unparseable, deleted) {
|
|
54000
|
+
return {
|
|
54001
|
+
name,
|
|
54002
|
+
unparseable,
|
|
54003
|
+
deleted,
|
|
54004
|
+
fingerprint: fingerprintOf([...unparseable !== null ? [unparseable] : [], ...deleted])
|
|
54005
|
+
};
|
|
54006
|
+
}
|
|
54007
|
+
};
|
|
54008
|
+
function reasonsOf(verdict) {
|
|
54009
|
+
return [...verdict.unparseable !== null ? [verdict.unparseable] : [], ...verdict.deleted];
|
|
54010
|
+
}
|
|
54011
|
+
//#endregion
|
|
53380
54012
|
//#region src/notification-center/index.ts
|
|
53381
54013
|
/**
|
|
53382
54014
|
* NotificationCenter — the P1 core of the Notification Center (spec
|
|
@@ -53443,7 +54075,7 @@ var NC_ALARM_ANNOUNCE_RULE_ID = "nc-alarm-announce";
|
|
|
53443
54075
|
* media (the SOURCE rule of a combined alarm row), and a zone overlay is
|
|
53444
54076
|
* frozen only when the rule asked for one AND fired on a zone.
|
|
53445
54077
|
*/
|
|
53446
|
-
function mediaPolicyFields(mediaRule, cropZoneIds) {
|
|
54078
|
+
function mediaPolicyFields(mediaRule, cropZoneIds, matchedZoneIds) {
|
|
53447
54079
|
const media = effectiveMediaPolicyForRule(mediaRule);
|
|
53448
54080
|
const image = media.image ?? "crop";
|
|
53449
54081
|
return {
|
|
@@ -53452,6 +54084,7 @@ function mediaPolicyFields(mediaRule, cropZoneIds) {
|
|
|
53452
54084
|
...media.showTriggerZone === true && cropZoneIds !== void 0 && cropZoneIds.length > 0 ? { mediaShowTriggerZone: true } : {},
|
|
53453
54085
|
...media.showSubjectPath === true && image !== "none" ? { mediaShowSubjectPath: true } : {},
|
|
53454
54086
|
...cropZoneIds !== void 0 ? { triggerZoneIds: [...cropZoneIds] } : {},
|
|
54087
|
+
...matchedZoneIds !== void 0 ? { matchedZoneIds: [...matchedZoneIds] } : {},
|
|
53455
54088
|
...media.profile !== void 0 ? { mediaProfile: media.profile } : {},
|
|
53456
54089
|
...media.gif === true ? { mediaGif: true } : {},
|
|
53457
54090
|
...media.clip === true ? { mediaClip: true } : {},
|
|
@@ -53957,6 +54590,21 @@ function groupOrderInput(fate, triggerZoneIds) {
|
|
|
53957
54590
|
* condition's ids, else an occupancy rule's one zone. A grouped member freezes the same
|
|
53958
54591
|
* ids a plain row of the rule would.
|
|
53959
54592
|
*/
|
|
54593
|
+
/**
|
|
54594
|
+
* The zones of `cropZoneIds` this subject actually triggered — what the zone
|
|
54595
|
+
* condition saw (membership plus the crossed zone, `zonesVisitedBy`), on the
|
|
54596
|
+
* rule whose media the row carries. On a combined alarm row the subject is the
|
|
54597
|
+
* alarm, so the source match's frozen zones answer. An occupancy rule's one
|
|
54598
|
+
* zone is the zone the count edge is ABOUT, so it is its own trigger.
|
|
54599
|
+
*/
|
|
54600
|
+
function matchedTriggerZoneIds(mediaRule, cropZoneIds, subject) {
|
|
54601
|
+
if (cropZoneIds === void 0) return void 0;
|
|
54602
|
+
if (mediaRule.conditions.zones === void 0) return [...cropZoneIds];
|
|
54603
|
+
const alarmSource = subject.systemEvent?.alarmSource;
|
|
54604
|
+
const visited = new Set(alarmSource !== void 0 ? alarmSource.zones ?? [] : subject.zones);
|
|
54605
|
+
if (alarmSource === void 0 && subject.crossing !== void 0) visited.add(subject.crossing.zoneId);
|
|
54606
|
+
return cropZoneIds.filter((id) => visited.has(id));
|
|
54607
|
+
}
|
|
53960
54608
|
function ruleCropZoneIds(rule) {
|
|
53961
54609
|
if (rule.conditions.zones !== void 0) return rule.conditions.zones.ids;
|
|
53962
54610
|
const zoneId = rule.conditions.occupancy?.zoneId;
|
|
@@ -54110,6 +54758,8 @@ var NotificationCenter = class {
|
|
|
54110
54758
|
* evaluating one.
|
|
54111
54759
|
*/
|
|
54112
54760
|
pruneLedger;
|
|
54761
|
+
/** D905: which rules cannot work, announced once per distinct reason. */
|
|
54762
|
+
ruleHealth = new NcRuleHealthMonitor();
|
|
54113
54763
|
/** The prune itself — see `device-prune-runner.ts`. */
|
|
54114
54764
|
pruner;
|
|
54115
54765
|
/**
|
|
@@ -57539,7 +58189,7 @@ var NotificationCenter = class {
|
|
|
57539
58189
|
const subject = incoming.subject;
|
|
57540
58190
|
const alarmSource = subject.systemEvent?.alarmSource;
|
|
57541
58191
|
const sourceRule = alarmSource !== void 0 ? this.rules.get(alarmSource.ruleId) : null;
|
|
57542
|
-
const media = sourceRule !== null ? mediaPolicyFields(sourceRule, ruleCropZoneIds(sourceRule)) : { media: "none" };
|
|
58192
|
+
const media = sourceRule !== null ? mediaPolicyFields(sourceRule, ruleCropZoneIds(sourceRule), matchedTriggerZoneIds(sourceRule, ruleCropZoneIds(sourceRule), subject)) : { media: "none" };
|
|
57543
58193
|
const mediaDeviceId = alarmSource?.deviceId ?? panel.deviceId;
|
|
57544
58194
|
const entries = targets.map((targetId) => ({
|
|
57545
58195
|
ruleId: NC_ALARM_ANNOUNCE_RULE_ID,
|
|
@@ -58745,6 +59395,7 @@ var NotificationCenter = class {
|
|
|
58745
59395
|
const renderedZones = notificationZones(rule, subject);
|
|
58746
59396
|
const confirm = frozenConfirmPolicy(rule, isSystemEvent);
|
|
58747
59397
|
const cropZoneIds = ruleCropZoneIds(mediaRule);
|
|
59398
|
+
const matchedZoneIds = matchedTriggerZoneIds(mediaRule, cropZoneIds, subject);
|
|
58748
59399
|
const allTargets = [...rule.targets, ...userTargets.filter((t) => !direct.has(t.targetId))];
|
|
58749
59400
|
const linkedOwners = !noMedia && subject.linkedStills !== void 0 ? linkedMediaOwners(subject.linkedStills, linkedCameraMode(mediaRule.media)) : void 0;
|
|
58750
59401
|
const groupV2 = extras?.groupV2 ?? (subject.systemEvent !== void 0 && !noMedia ? alarmIncidentGroup(subject.systemEvent, subject.timestamp) : void 0);
|
|
@@ -58755,7 +59406,7 @@ var NotificationCenter = class {
|
|
|
58755
59406
|
...origin === "synthetic" ? { synthetic: true } : {},
|
|
58756
59407
|
priority: rule.priority,
|
|
58757
59408
|
...rule.template !== void 0 ? { template: rule.template } : {},
|
|
58758
|
-
...noMedia ? { media: "none" } : mediaPolicyFields(mediaRule, cropZoneIds),
|
|
59409
|
+
...noMedia ? { media: "none" } : mediaPolicyFields(mediaRule, cropZoneIds, matchedZoneIds),
|
|
58759
59410
|
...confirm !== void 0 ? {
|
|
58760
59411
|
confirm,
|
|
58761
59412
|
...cooldown !== void 0 ? { cooldown } : {},
|
|
@@ -59400,6 +60051,51 @@ var NotificationCenter = class {
|
|
|
59400
60051
|
if (!this.pruneLedger.isLoaded()) await this.pruneLedger.load();
|
|
59401
60052
|
await this.pruner.reconcileDevices();
|
|
59402
60053
|
await this.pruner.reconcileTargets();
|
|
60054
|
+
this.announceRuleHealth();
|
|
60055
|
+
}
|
|
60056
|
+
/**
|
|
60057
|
+
* D905 — say which rules cannot work: a row the load skipped, or a rule still
|
|
60058
|
+
* naming a device / target the prune confirmed deleted. One edge per rule per
|
|
60059
|
+
* distinct set of reasons; a reading that did not answer changes nothing.
|
|
60060
|
+
*/
|
|
60061
|
+
announceRuleHealth() {
|
|
60062
|
+
const edges = this.ruleHealth.evaluate({
|
|
60063
|
+
rules: this.rules.isLoaded() ? this.rules.list() : null,
|
|
60064
|
+
skipped: this.rules.skippedRows(),
|
|
60065
|
+
prunes: this.pruneLedger.isLoaded() ? this.pruner.listPrunes() : null
|
|
60066
|
+
});
|
|
60067
|
+
for (const edge of edges) {
|
|
60068
|
+
const source = {
|
|
60069
|
+
type: "addon",
|
|
60070
|
+
id: "notification-center"
|
|
60071
|
+
};
|
|
60072
|
+
if (edge.kind === "broken") {
|
|
60073
|
+
const [first] = edge.reasons;
|
|
60074
|
+
if (first === void 0) continue;
|
|
60075
|
+
const offendingIds = [...new Set(edge.reasons.flatMap((r) => r.ids))];
|
|
60076
|
+
this.logger.warn("notification rule cannot work — announced", { meta: {
|
|
60077
|
+
ruleId: edge.ruleId,
|
|
60078
|
+
rule: edge.ruleName,
|
|
60079
|
+
reasons: edge.reasons
|
|
60080
|
+
} });
|
|
60081
|
+
this.deps.emitBusEvent?.(require_clip_model_registry.createEvent(require_clip_model_registry.EventCategory.NotificationRuleBroken, source, {
|
|
60082
|
+
ruleId: edge.ruleId,
|
|
60083
|
+
ruleName: edge.ruleName,
|
|
60084
|
+
reason: first.code,
|
|
60085
|
+
reasons: edge.reasons,
|
|
60086
|
+
offendingIds
|
|
60087
|
+
}));
|
|
60088
|
+
} else {
|
|
60089
|
+
this.logger.info("notification rule works again — announced", { meta: {
|
|
60090
|
+
ruleId: edge.ruleId,
|
|
60091
|
+
rule: edge.ruleName
|
|
60092
|
+
} });
|
|
60093
|
+
this.deps.emitBusEvent?.(require_clip_model_registry.createEvent(require_clip_model_registry.EventCategory.NotificationRuleRepaired, source, {
|
|
60094
|
+
ruleId: edge.ruleId,
|
|
60095
|
+
ruleName: edge.ruleName
|
|
60096
|
+
}));
|
|
60097
|
+
}
|
|
60098
|
+
}
|
|
59403
60099
|
}
|
|
59404
60100
|
/**
|
|
59405
60101
|
* `DeviceUnregistered` — a device was DELETED (only `removeDevice` emits it,
|
|
@@ -87251,7 +87947,12 @@ var RetrainCollectionService = class {
|
|
|
87251
87947
|
reason: "unknown-collection"
|
|
87252
87948
|
};
|
|
87253
87949
|
}
|
|
87254
|
-
const
|
|
87950
|
+
const membershipOp = await this.deps.collections.addFrameOp({
|
|
87951
|
+
collectionId: input.collectionId,
|
|
87952
|
+
frameId: recordingFrameId(input.deviceId, input.epochMs),
|
|
87953
|
+
deviceId: input.deviceId
|
|
87954
|
+
});
|
|
87955
|
+
const stored = await this.storeStill(input.deviceId, input.epochMs, membershipOp === null ? [] : [membershipOp]);
|
|
87255
87956
|
if (stored.kind === "refused") {
|
|
87256
87957
|
this.deps.logger.info("retrain collection add refused", {
|
|
87257
87958
|
tags,
|
|
@@ -87263,7 +87964,7 @@ var RetrainCollectionService = class {
|
|
|
87263
87964
|
});
|
|
87264
87965
|
return stored;
|
|
87265
87966
|
}
|
|
87266
|
-
await this.deps.collections.addFrame({
|
|
87967
|
+
if (stored.reused) await this.deps.collections.addFrame({
|
|
87267
87968
|
collectionId: input.collectionId,
|
|
87268
87969
|
frameId: stored.frame.frameId,
|
|
87269
87970
|
deviceId: input.deviceId
|
|
@@ -87328,7 +88029,7 @@ var RetrainCollectionService = class {
|
|
|
87328
88029
|
const ids = await this.deps.collections.listFrameIds(input.collectionId);
|
|
87329
88030
|
return this.deps.retrain.toWireFrames(await this.deps.frames.getMany(ids));
|
|
87330
88031
|
}
|
|
87331
|
-
async storeStill(deviceId, epochMs) {
|
|
88032
|
+
async storeStill(deviceId, epochMs, withRow) {
|
|
87332
88033
|
const existing = await this.deps.frames.get(recordingFrameId(deviceId, epochMs));
|
|
87333
88034
|
if (existing !== null) return {
|
|
87334
88035
|
kind: "stored",
|
|
@@ -87348,7 +88049,7 @@ var RetrainCollectionService = class {
|
|
|
87348
88049
|
deviceId,
|
|
87349
88050
|
epochMs,
|
|
87350
88051
|
bytes: still.jpeg
|
|
87351
|
-
});
|
|
88052
|
+
}, withRow);
|
|
87352
88053
|
return stored.kind === "copied" ? {
|
|
87353
88054
|
kind: "stored",
|
|
87354
88055
|
frame: stored.frame,
|
|
@@ -89671,9 +90372,9 @@ var RetrainService = class {
|
|
|
89671
90372
|
...this.frameUrls(updated.frameId)
|
|
89672
90373
|
};
|
|
89673
90374
|
}
|
|
89674
|
-
/** Un-select:
|
|
89675
|
-
* an annotation whose frame is gone is unopenable
|
|
89676
|
-
*
|
|
90375
|
+
/** Un-select: the frame row, its annotations and its collection memberships
|
|
90376
|
+
* leave in one transaction (an annotation whose frame is gone is unopenable
|
|
90377
|
+
* and unfixable, so they never part); the blob is deleted after the row. */
|
|
89677
90378
|
async deselectFrame(input) {
|
|
89678
90379
|
const frame = await this.deps.frames.get(input.frameId);
|
|
89679
90380
|
if (frame === null) {
|
|
@@ -89692,11 +90393,16 @@ var RetrainService = class {
|
|
|
89692
90393
|
removedAnnotations: await this.removeFrame(frame)
|
|
89693
90394
|
};
|
|
89694
90395
|
}
|
|
89695
|
-
/**
|
|
90396
|
+
/**
|
|
90397
|
+
* Remove a frame from the dataset. The frame row, its annotations and its
|
|
90398
|
+
* collection memberships leave in ONE transaction (a failure removes nothing
|
|
90399
|
+
* and throws); the blob is deleted after the row, never before.
|
|
90400
|
+
*/
|
|
89696
90401
|
async removeFrame(frame) {
|
|
89697
|
-
const
|
|
89698
|
-
await this.deps.memberships?.
|
|
89699
|
-
await this.deps.frames.deleteFrame(frame);
|
|
90402
|
+
const annotationOps = await this.deps.annotations.deleteOpsForFrame(frame.frameId);
|
|
90403
|
+
const membershipOps = await this.deps.memberships?.forgetFrameOps(frame.frameId) ?? [];
|
|
90404
|
+
await this.deps.frames.deleteFrame(frame, [...annotationOps, ...membershipOps]);
|
|
90405
|
+
const removedAnnotations = annotationOps.length;
|
|
89700
90406
|
this.deps.logger.info("retrain frame removed from the dataset", {
|
|
89701
90407
|
tags: { deviceId: frame.deviceId },
|
|
89702
90408
|
meta: {
|
|
@@ -89890,7 +90596,8 @@ var RetrainService = class {
|
|
|
89890
90596
|
* The guarded replace. A caller that names the revision it saw gets exactly
|
|
89891
90597
|
* one attempt: a different current revision is a conflict, never a retry —
|
|
89892
90598
|
* the point is that it must look again. A caller that names none asked for
|
|
89893
|
-
* last-write-wins, so a race lost between our read and the
|
|
90599
|
+
* last-write-wins, so a race lost between our revision read and the records
|
|
90600
|
+
* `transact` that commits the save (a per-frame lock serialises the two) is
|
|
89894
90601
|
* retried on the fresh revision, a bounded number of times.
|
|
89895
90602
|
*/
|
|
89896
90603
|
async replaceGuarded(frame, input, drafts, dismissed) {
|
|
@@ -103517,12 +104224,16 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
|
|
|
103517
104224
|
require_clip_model_registry.EventCategory.WorkerOffline,
|
|
103518
104225
|
require_clip_model_registry.EventCategory.PipelineNodeInferenceUnavailable,
|
|
103519
104226
|
require_clip_model_registry.EventCategory.PipelineDetectionBlind,
|
|
104227
|
+
require_clip_model_registry.EventCategory.PipelineMotionNoisy,
|
|
104228
|
+
require_clip_model_registry.EventCategory.PipelineMotionCalm,
|
|
103520
104229
|
require_clip_model_registry.EventCategory.RecordingHealthStalled,
|
|
103521
104230
|
require_clip_model_registry.EventCategory.RecordingHealthOk,
|
|
103522
104231
|
require_clip_model_registry.EventCategory.RecordingHealthDegraded,
|
|
103523
104232
|
require_clip_model_registry.EventCategory.StorageLocationLow,
|
|
103524
104233
|
require_clip_model_registry.EventCategory.StorageLocationCritical,
|
|
103525
104234
|
require_clip_model_registry.EventCategory.StorageLocationOk,
|
|
104235
|
+
require_clip_model_registry.EventCategory.NotificationRuleBroken,
|
|
104236
|
+
require_clip_model_registry.EventCategory.NotificationRuleRepaired,
|
|
103526
104237
|
require_clip_model_registry.EventCategory.AddonRunnerFailed,
|
|
103527
104238
|
require_clip_model_registry.EventCategory.AddonStarted,
|
|
103528
104239
|
require_clip_model_registry.EventCategory.AddonRestarted,
|
|
@@ -106533,6 +107244,18 @@ Object.freeze({
|
|
|
106533
107244
|
"empty"
|
|
106534
107245
|
]
|
|
106535
107246
|
},
|
|
107247
|
+
{
|
|
107248
|
+
"path": "filterCleaningDue",
|
|
107249
|
+
"valueType": "boolean"
|
|
107250
|
+
},
|
|
107251
|
+
{
|
|
107252
|
+
"path": "machineCareDue",
|
|
107253
|
+
"valueType": "boolean"
|
|
107254
|
+
},
|
|
107255
|
+
{
|
|
107256
|
+
"path": "machineCareRemainingRuns",
|
|
107257
|
+
"valueType": "number"
|
|
107258
|
+
},
|
|
106536
107259
|
{
|
|
106537
107260
|
"path": "halfLoad",
|
|
106538
107261
|
"valueType": "boolean"
|
|
@@ -115553,6 +116276,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
115553
116276
|
nodeAddons: (nodeId) => api.nodes.getNodeAddons.query({ nodeId })
|
|
115554
116277
|
},
|
|
115555
116278
|
readAlerts: () => api.alerts.list.query({ limit: 5e3 }),
|
|
116279
|
+
emitBusEvent: (event) => this.ctx.eventBus.emit(event),
|
|
115556
116280
|
getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }),
|
|
115557
116281
|
recognitionOutlook: ({ deviceId, trackId, className, labelKind, now }) => recognitionOutlook({
|
|
115558
116282
|
className,
|
|
@@ -115826,7 +116550,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
115826
116550
|
since: timestamp - toleranceMs,
|
|
115827
116551
|
limit: NC_TRACKS_AT_INSTANT_LIMIT
|
|
115828
116552
|
});
|
|
115829
|
-
return tracksAtInstant([...live, ...persisted.tracks], timestamp, toleranceMs, this.lastFrameDimsByDevice.get(deviceId))
|
|
116553
|
+
return tracksAtInstant([...live, ...persisted.tracks], timestamp, toleranceMs, this.lastFrameDimsByDevice.get(deviceId), (drop) => this.ctx.logger.info("scene-boxes: other track not drawn", {
|
|
116554
|
+
tags: { deviceId },
|
|
116555
|
+
meta: {
|
|
116556
|
+
trackId: drop.trackId,
|
|
116557
|
+
className: drop.className,
|
|
116558
|
+
reason: drop.reason,
|
|
116559
|
+
instant: timestamp,
|
|
116560
|
+
...drop.nearestDtMs !== void 0 ? { nearestDtMs: drop.nearestDtMs } : {}
|
|
116561
|
+
}
|
|
116562
|
+
}));
|
|
115830
116563
|
},
|
|
115831
116564
|
getDeviceName,
|
|
115832
116565
|
getDeviceLocation,
|