@camstack/addon-post-analysis 1.2.621 → 1.2.622
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{clip-model-registry-BBzoIoKO.mjs → clip-model-registry-EDTOCKN9.mjs} +234 -4
- package/dist/{clip-model-registry-CcgzpELQ.js → clip-model-registry-MHJ9BSUV.js} +239 -3
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CkNZAMox.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DAyVukQp.mjs} +3 -3
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CTvx3PDS.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-wlnNaaYX.mjs → hostInit-C4-dCKYR.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +559 -75
- package/dist/pipeline-analytics/index.mjs +559 -75
- 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-MHJ9BSUV.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,15 +5402,28 @@ 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
|
+
* Every source track ALIVE at `timestamp` (± {@link NC_TRACK_LIFE_SLACK_MS})
|
|
5418
|
+
* with a position within `toleranceMs` of it, its nearest box normalised,
|
|
5419
|
+
* deduplicated by track id (a live copy and a persisted copy of one track are
|
|
5420
|
+
* one track). Pure.
|
|
5408
5421
|
*/
|
|
5409
5422
|
function tracksAtInstant(sources, timestamp, toleranceMs, fallbackDims) {
|
|
5410
5423
|
const out = /* @__PURE__ */ new Map();
|
|
5411
5424
|
for (const track of sources) {
|
|
5412
5425
|
if (out.has(track.trackId)) continue;
|
|
5413
|
-
if (track.firstSeen > timestamp +
|
|
5426
|
+
if (track.firstSeen > timestamp + NC_TRACK_LIFE_SLACK_MS || track.lastSeen < timestamp - NC_TRACK_LIFE_SLACK_MS) continue;
|
|
5414
5427
|
const w = track.frameWidth ?? fallbackDims?.w ?? 0;
|
|
5415
5428
|
const h = track.frameHeight ?? fallbackDims?.h ?? 0;
|
|
5416
5429
|
if (w <= 0 || h <= 0) continue;
|
|
@@ -20451,8 +20464,8 @@ var FACE_REF_COLUMNS = ["deviceId"];
|
|
|
20451
20464
|
* D312) a predicate that silently matched nothing. The faces ledger therefore derives a
|
|
20452
20465
|
* boolean `recognized` from every write that touches `recognizedIdentityId`
|
|
20453
20466
|
* (`face-recognized.ts`), and the views are plain ranges over the `recognized_ts` /
|
|
20454
|
-
* `assigned_recognized_ts` indexes.
|
|
20455
|
-
*
|
|
20467
|
+
* `assigned_recognized_ts` indexes. Rows written before the field existed were
|
|
20468
|
+
* backfilled offline on the operator hubs on 2026-10-22 (the one-shot script is deleted).
|
|
20456
20469
|
*
|
|
20457
20470
|
* `assigned` is `BOOLEAN NOT NULL`; the backend serialises `false` to `0` at
|
|
20458
20471
|
* the bind (`serializeColumnValue`) and decodes `0` back to `false` on read, so
|
|
@@ -28515,6 +28528,33 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
28515
28528
|
...nodeId !== void 0 ? { nodeId } : {}
|
|
28516
28529
|
});
|
|
28517
28530
|
}
|
|
28531
|
+
case "notification.rule-broken":
|
|
28532
|
+
case "notification.rule-repaired": {
|
|
28533
|
+
const ruleId = stringField(data, "ruleId");
|
|
28534
|
+
if (ruleId === void 0) return null;
|
|
28535
|
+
const broken = event.category === "notification.rule-broken";
|
|
28536
|
+
const kind = broken ? "rule-broken" : "rule-repaired";
|
|
28537
|
+
const ruleName = stringField(data, "ruleName") ?? ruleId;
|
|
28538
|
+
const reason = stringField(data, "reason") ?? "unknown";
|
|
28539
|
+
const ids = data["offendingIds"];
|
|
28540
|
+
const offendingIds = Array.isArray(ids) ? ids.filter((id) => typeof id === "string").join(", ") : "";
|
|
28541
|
+
return systemSubject({
|
|
28542
|
+
facts,
|
|
28543
|
+
kind,
|
|
28544
|
+
subject: `rule-health:${ruleId}`,
|
|
28545
|
+
titleKey: `system.${kind}.title`,
|
|
28546
|
+
bodyKey: broken ? `system.rule-broken.body.${reason}` : "system.rule-repaired.body",
|
|
28547
|
+
textParams: broken ? {
|
|
28548
|
+
ruleId,
|
|
28549
|
+
ruleName,
|
|
28550
|
+
brokenReason: reason,
|
|
28551
|
+
offendingIds
|
|
28552
|
+
} : {
|
|
28553
|
+
ruleId,
|
|
28554
|
+
ruleName
|
|
28555
|
+
}
|
|
28556
|
+
});
|
|
28557
|
+
}
|
|
28518
28558
|
case "update.available": return incomingFromUpdateAvailable(facts, data);
|
|
28519
28559
|
case "addon.updated": {
|
|
28520
28560
|
const update = parseAddonUpdateComplete(data);
|
|
@@ -30520,8 +30560,8 @@ var en_default = {
|
|
|
30520
30560
|
"occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
|
|
30521
30561
|
"occupancy.op.occupied": "occupied",
|
|
30522
30562
|
"occupancy.op.free": "free",
|
|
30523
|
-
"system.device-online.title": "
|
|
30524
|
-
"system.device-online.body": "Device {{deviceName}} is online",
|
|
30563
|
+
"system.device-online.title": "Back online — {{deviceName}}",
|
|
30564
|
+
"system.device-online.body": "Device {{deviceName}} is back online",
|
|
30525
30565
|
"system.device-offline.title": "Offline — {{deviceName}}",
|
|
30526
30566
|
"system.device-offline.body": "Device {{deviceName}} is offline",
|
|
30527
30567
|
"system.device-disabled.title": "Switched off — {{deviceName}}",
|
|
@@ -30534,14 +30574,14 @@ var en_default = {
|
|
|
30534
30574
|
"system.device-battery-normal.title": "Battery back to normal — {{deviceName}}",
|
|
30535
30575
|
"system.device-battery-normal.body": "{{deviceName}} is back to {{percentage}}% battery",
|
|
30536
30576
|
"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",
|
|
30577
|
+
"system.stream-online.title": "Stream back online — {{deviceName}}",
|
|
30578
|
+
"system.stream-online.body": "Stream for camera {{deviceName}} is back online",
|
|
30579
|
+
"system.stream-online.body.profiled": "{{profile}} stream for camera {{deviceName}} is back online",
|
|
30540
30580
|
"system.stream-offline.title": "Stream offline — {{deviceName}}",
|
|
30541
30581
|
"system.stream-offline.body": "Stream for camera {{deviceName}} is offline",
|
|
30542
30582
|
"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",
|
|
30583
|
+
"system.node-online.title": "Node back online — {{nodeName}}",
|
|
30584
|
+
"system.node-online.body.agent": "Agent {{nodeName}} is back online",
|
|
30545
30585
|
"system.node-online.body.agent.after": "Agent {{nodeName}} is back online after {{offlineFor}} offline",
|
|
30546
30586
|
"system.node-offline.title": "Node offline — {{nodeName}}",
|
|
30547
30587
|
"system.node-offline.body.agent": "Agent {{nodeName}} is offline",
|
|
@@ -30561,7 +30601,7 @@ var en_default = {
|
|
|
30561
30601
|
"one": "Camera {{deviceName}} has not written a recording segment for {{minutes}} minute",
|
|
30562
30602
|
"other": "Camera {{deviceName}} has not written a recording segment for {{minutes}} minutes"
|
|
30563
30603
|
},
|
|
30564
|
-
"system.recording-healthy.title": "Recording
|
|
30604
|
+
"system.recording-healthy.title": "Recording again — {{deviceName}}",
|
|
30565
30605
|
"system.recording-healthy.body": "Camera {{deviceName}} is writing recording segments again",
|
|
30566
30606
|
"system.recording-degraded.title": "Recording degraded — {{deviceName}}",
|
|
30567
30607
|
"system.recording-degraded.body": "Camera {{deviceName}} exhausted its writer restart budget ({{message}})",
|
|
@@ -30822,7 +30862,13 @@ var en_default = {
|
|
|
30822
30862
|
"calendar-events.line.timed": "• {{when}} {{title}}{{where}}",
|
|
30823
30863
|
"calendar-events.line.allDay": "• All day {{title}}{{where}}",
|
|
30824
30864
|
"calendar-events.where": " @ {{location}}",
|
|
30825
|
-
"calendar-events.more": "…and {{count}} more"
|
|
30865
|
+
"calendar-events.more": "…and {{count}} more",
|
|
30866
|
+
"system.rule-broken.title": "Notification rule broken — {{ruleName}}",
|
|
30867
|
+
"system.rule-broken.body.unparseable": "The rule \"{{ruleName}}\" cannot be loaded and sends nothing. Open it and save it again.",
|
|
30868
|
+
"system.rule-broken.body.device-deleted": "The rule \"{{ruleName}}\" still names a deleted device ({{offendingIds}}) and was switched off. Pick the device again.",
|
|
30869
|
+
"system.rule-broken.body.target-deleted": "The rule \"{{ruleName}}\" still sends to a deleted destination ({{offendingIds}}) and was switched off. Pick a destination again.",
|
|
30870
|
+
"system.rule-repaired.title": "Notification rule works again — {{ruleName}}",
|
|
30871
|
+
"system.rule-repaired.body": "The rule \"{{ruleName}}\" loads and names nothing deleted any more."
|
|
30826
30872
|
};
|
|
30827
30873
|
//#endregion
|
|
30828
30874
|
//#region src/notification-center/texts/it.json
|
|
@@ -30907,8 +30953,8 @@ var it_default = {
|
|
|
30907
30953
|
"occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
|
|
30908
30954
|
"occupancy.op.occupied": "occupata",
|
|
30909
30955
|
"occupancy.op.free": "libera",
|
|
30910
|
-
"system.device-online.title": "
|
|
30911
|
-
"system.device-online.body": "Il dispositivo {{deviceName}} è online",
|
|
30956
|
+
"system.device-online.title": "Di nuovo online — {{deviceName}}",
|
|
30957
|
+
"system.device-online.body": "Il dispositivo {{deviceName}} è di nuovo online",
|
|
30912
30958
|
"system.device-offline.title": "Offline — {{deviceName}}",
|
|
30913
30959
|
"system.device-offline.body": "Il dispositivo {{deviceName}} è offline",
|
|
30914
30960
|
"system.device-disabled.title": "Spento — {{deviceName}}",
|
|
@@ -30921,14 +30967,14 @@ var it_default = {
|
|
|
30921
30967
|
"system.device-battery-normal.title": "Batteria rientrata — {{deviceName}}",
|
|
30922
30968
|
"system.device-battery-normal.body": "{{deviceName}} è risalito al {{percentage}}% di batteria",
|
|
30923
30969
|
"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",
|
|
30970
|
+
"system.stream-online.title": "Stream di nuovo online — {{deviceName}}",
|
|
30971
|
+
"system.stream-online.body": "Lo stream della camera {{deviceName}} è di nuovo online",
|
|
30972
|
+
"system.stream-online.body.profiled": "Lo stream {{profile}} della camera {{deviceName}} è di nuovo online",
|
|
30927
30973
|
"system.stream-offline.title": "Stream offline — {{deviceName}}",
|
|
30928
30974
|
"system.stream-offline.body": "Lo stream della camera {{deviceName}} è offline",
|
|
30929
30975
|
"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",
|
|
30976
|
+
"system.node-online.title": "Nodo di nuovo online — {{nodeName}}",
|
|
30977
|
+
"system.node-online.body.agent": "L'agent {{nodeName}} è di nuovo online",
|
|
30932
30978
|
"system.node-online.body.agent.after": "L'agent {{nodeName}} è di nuovo online dopo {{offlineFor}} offline",
|
|
30933
30979
|
"system.node-offline.title": "Nodo offline — {{nodeName}}",
|
|
30934
30980
|
"system.node-offline.body.agent": "L'agent {{nodeName}} è offline",
|
|
@@ -30943,7 +30989,7 @@ var it_default = {
|
|
|
30943
30989
|
"one": "La camera {{deviceName}} non scrive un segmento da {{minutes}} minuto",
|
|
30944
30990
|
"other": "La camera {{deviceName}} non scrive un segmento da {{minutes}} minuti"
|
|
30945
30991
|
},
|
|
30946
|
-
"system.recording-healthy.title": "
|
|
30992
|
+
"system.recording-healthy.title": "Registra di nuovo — {{deviceName}}",
|
|
30947
30993
|
"system.recording-healthy.body": "La camera {{deviceName}} scrive di nuovo i segmenti",
|
|
30948
30994
|
"system.recording-degraded.title": "Registrazione degradata — {{deviceName}}",
|
|
30949
30995
|
"system.recording-degraded.body": "La camera {{deviceName}} ha esaurito i riavvii del writer ({{message}})",
|
|
@@ -31209,7 +31255,13 @@ var it_default = {
|
|
|
31209
31255
|
"calendar-events.line.timed": "• {{when}} {{title}}{{where}}",
|
|
31210
31256
|
"calendar-events.line.allDay": "• Tutto il giorno {{title}}{{where}}",
|
|
31211
31257
|
"calendar-events.where": " @ {{location}}",
|
|
31212
|
-
"calendar-events.more": "…e altri {{count}}"
|
|
31258
|
+
"calendar-events.more": "…e altri {{count}}",
|
|
31259
|
+
"system.rule-broken.title": "Regola di notifica rotta — {{ruleName}}",
|
|
31260
|
+
"system.rule-broken.body.unparseable": "La regola \"{{ruleName}}\" non si carica e non invia nulla. Aprila e salvala di nuovo.",
|
|
31261
|
+
"system.rule-broken.body.device-deleted": "La regola \"{{ruleName}}\" usa ancora un dispositivo eliminato ({{offendingIds}}) ed è stata disattivata. Scegli di nuovo il dispositivo.",
|
|
31262
|
+
"system.rule-broken.body.target-deleted": "La regola \"{{ruleName}}\" invia ancora a una destinazione eliminata ({{offendingIds}}) ed è stata disattivata. Scegli di nuovo una destinazione.",
|
|
31263
|
+
"system.rule-repaired.title": "Regola di notifica di nuovo funzionante — {{ruleName}}",
|
|
31264
|
+
"system.rule-repaired.body": "La regola \"{{ruleName}}\" si carica e non usa più nulla di eliminato."
|
|
31213
31265
|
};
|
|
31214
31266
|
//#endregion
|
|
31215
31267
|
//#region src/notification-center/text-catalog.ts
|
|
@@ -33480,6 +33532,10 @@ function systemEventKeys(kind) {
|
|
|
33480
33532
|
titleKey: `system.${kind}.title`,
|
|
33481
33533
|
bodyKey: `system.${kind}.body.cleanWater`
|
|
33482
33534
|
};
|
|
33535
|
+
if (kind === "rule-broken") return {
|
|
33536
|
+
titleKey: `system.${kind}.title`,
|
|
33537
|
+
bodyKey: `system.${kind}.body.device-deleted`
|
|
33538
|
+
};
|
|
33483
33539
|
return {
|
|
33484
33540
|
titleKey: `system.${kind}.title`,
|
|
33485
33541
|
bodyKey: `system.${kind}.body`
|
|
@@ -39735,6 +39791,99 @@ var NcRecordedCutMemo = class {
|
|
|
39735
39791
|
}
|
|
39736
39792
|
};
|
|
39737
39793
|
//#endregion
|
|
39794
|
+
//#region src/notification-center/notification-label-layout.ts
|
|
39795
|
+
/**
|
|
39796
|
+
* Where each box's caption goes on a notification picture — pure geometry,
|
|
39797
|
+
* rects in, placements out (D897 follow-up, 2026-10-22).
|
|
39798
|
+
*
|
|
39799
|
+
* A caption drawn INSIDE its box used to cover the subject whole on a small
|
|
39800
|
+
* box, and a caption next to a crowd landed on a neighbour's box. The rule:
|
|
39801
|
+
*
|
|
39802
|
+
* 1. Try, in order: OUTSIDE above the box (inside the picture), OUTSIDE below
|
|
39803
|
+
* it, then INSIDE its top edge — the last only when the box is big enough
|
|
39804
|
+
* that the plate is a thin band of it: plate height ≤
|
|
39805
|
+
* {@link NC_LABEL_INSIDE_MAX_HEIGHT_FRACTION} of the box height and plate
|
|
39806
|
+
* width ≤ the box width. A fifth of the height leaves the head and torso of
|
|
39807
|
+
* an upright person, or most of a vehicle, visible; anything taller is the
|
|
39808
|
+
* caption hiding what it names.
|
|
39809
|
+
* 2. A candidate is refused when its plate intersects ANY other box drawn on
|
|
39810
|
+
* the picture, or a plate already placed.
|
|
39811
|
+
* 3. The subject is placed first; the other tracks follow by score, highest
|
|
39812
|
+
* first, so a crowd never costs the subject its caption.
|
|
39813
|
+
* 4. The font never shrinks to make a plate fit: no fitting candidate means
|
|
39814
|
+
* no caption for that box. A box without its caption still carries its
|
|
39815
|
+
* outline and colour.
|
|
39816
|
+
*
|
|
39817
|
+
* Coordinates are the picture's own pixels, whatever it is (a scene or a crop).
|
|
39818
|
+
*/
|
|
39819
|
+
/** The inside placement is allowed only when the plate is at most this fraction of the box height. */
|
|
39820
|
+
var NC_LABEL_INSIDE_MAX_HEIGHT_FRACTION = .2;
|
|
39821
|
+
/** A caption is never drawn smaller than this, in output pixels. */
|
|
39822
|
+
var NC_LABEL_MIN_FONT_PX = 14;
|
|
39823
|
+
/** The dark plate a caption sits on, for this text at this font size. */
|
|
39824
|
+
function labelPlateSize(label, fontSize) {
|
|
39825
|
+
return {
|
|
39826
|
+
w: Math.round(label.length * fontSize * .62 + fontSize * .8),
|
|
39827
|
+
h: Math.round(fontSize * 1.35)
|
|
39828
|
+
};
|
|
39829
|
+
}
|
|
39830
|
+
/** Positive-area overlap; rectangles that only touch do not intersect. */
|
|
39831
|
+
function intersects(a, b) {
|
|
39832
|
+
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;
|
|
39833
|
+
}
|
|
39834
|
+
function captionOrder(boxes) {
|
|
39835
|
+
const rank = (b) => b.role === "subject" ? 0 : 1;
|
|
39836
|
+
return boxes.map((_, i) => i).sort((i, j) => {
|
|
39837
|
+
const a = boxes[i];
|
|
39838
|
+
const b = boxes[j];
|
|
39839
|
+
if (a === void 0 || b === void 0) return 0;
|
|
39840
|
+
return rank(a) - rank(b) || (b.score ?? 0) - (a.score ?? 0) || i - j;
|
|
39841
|
+
});
|
|
39842
|
+
}
|
|
39843
|
+
function candidates(box, plate, width, height) {
|
|
39844
|
+
if (plate.w > width) return [];
|
|
39845
|
+
const x = Math.min(box.x, Math.max(0, width - plate.w));
|
|
39846
|
+
const out = [];
|
|
39847
|
+
if (box.y - plate.h >= 0) out.push({
|
|
39848
|
+
x,
|
|
39849
|
+
y: box.y - plate.h,
|
|
39850
|
+
...plate,
|
|
39851
|
+
where: "above"
|
|
39852
|
+
});
|
|
39853
|
+
if (box.y + box.h + plate.h <= height) out.push({
|
|
39854
|
+
x,
|
|
39855
|
+
y: box.y + box.h,
|
|
39856
|
+
...plate,
|
|
39857
|
+
where: "below"
|
|
39858
|
+
});
|
|
39859
|
+
if (plate.h <= box.h * NC_LABEL_INSIDE_MAX_HEIGHT_FRACTION && plate.w <= box.w) out.push({
|
|
39860
|
+
x,
|
|
39861
|
+
y: box.y,
|
|
39862
|
+
...plate,
|
|
39863
|
+
where: "inside"
|
|
39864
|
+
});
|
|
39865
|
+
return out;
|
|
39866
|
+
}
|
|
39867
|
+
/** Place every caption by the rule above. */
|
|
39868
|
+
function layoutBoxLabels(input) {
|
|
39869
|
+
const { width, height, boxes } = input;
|
|
39870
|
+
const fontSize = Math.max(NC_LABEL_MIN_FONT_PX, input.fontSize);
|
|
39871
|
+
const placements = /* @__PURE__ */ new Map();
|
|
39872
|
+
let dropped = 0;
|
|
39873
|
+
for (const i of captionOrder(boxes)) {
|
|
39874
|
+
const box = boxes[i];
|
|
39875
|
+
if (box === void 0 || box.label.length === 0) continue;
|
|
39876
|
+
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)));
|
|
39877
|
+
if (chosen === void 0) dropped += 1;
|
|
39878
|
+
else placements.set(i, chosen);
|
|
39879
|
+
}
|
|
39880
|
+
return {
|
|
39881
|
+
placements: boxes.map((_, i) => placements.get(i) ?? null),
|
|
39882
|
+
placed: placements.size,
|
|
39883
|
+
dropped
|
|
39884
|
+
};
|
|
39885
|
+
}
|
|
39886
|
+
//#endregion
|
|
39738
39887
|
//#region src/notification-center/trigger-zone-still.ts
|
|
39739
39888
|
/**
|
|
39740
39889
|
* How far a candidate's instant may sit from the nearest trail point before the
|
|
@@ -39857,6 +40006,15 @@ var NC_IMAGE_OTHER_COLOR = "#f2f2f2";
|
|
|
39857
40006
|
var NC_IMAGE_ZONE_COLOR = "#29b6f6";
|
|
39858
40007
|
/** Two tracks whose boxes overlap this much at one instant are the same subject (a fused id). */
|
|
39859
40008
|
var SAME_SUBJECT_IOU = .8;
|
|
40009
|
+
/**
|
|
40010
|
+
* A box of the SUBJECT'S CLASS overlapping the subject this much at one
|
|
40011
|
+
* instant is the subject itself — an unfused piece L2 has not joined yet
|
|
40012
|
+
* (D683 fuses 10–20 s later; a push leaves ~6 s after the trigger). The
|
|
40013
|
+
* detector's per-class NMS (`NMS_IOU_DEFAULT` 0.45) keeps one of two
|
|
40014
|
+
* same-class boxes above 0.45 in a frame, so two REAL subjects of one class
|
|
40015
|
+
* cannot overlap past it; 0.5 leaves a margin over that.
|
|
40016
|
+
*/
|
|
40017
|
+
var NC_IMAGE_SAME_CLASS_SUBJECT_IOU = .5;
|
|
39860
40018
|
var JPEG_QUALITY = 85;
|
|
39861
40019
|
/** The caption of a box: class (or label) and confidence — "dog 87%". Language-free. */
|
|
39862
40020
|
function imageLabelText(name, confidence) {
|
|
@@ -39889,15 +40047,21 @@ function toPixels(box, width, height) {
|
|
|
39889
40047
|
h: Math.max(1, Math.min(Math.round(box.h * height), height - y))
|
|
39890
40048
|
};
|
|
39891
40049
|
}
|
|
40050
|
+
/** {@link notificationOverlay}'s SVG alone. */
|
|
40051
|
+
function notificationOverlaySvg(input) {
|
|
40052
|
+
return notificationOverlay(input).svg;
|
|
40053
|
+
}
|
|
39892
40054
|
/**
|
|
39893
|
-
* The SVG drawn over a
|
|
39894
|
-
*
|
|
39895
|
-
*
|
|
40055
|
+
* The SVG drawn over a picture of `width` × `height`. The subject is drawn
|
|
40056
|
+
* LAST so a crowd never hides it. Captions sit on a dark plate, white text,
|
|
40057
|
+
* where `layoutBoxLabels` puts them — outside above, outside below, inside a
|
|
40058
|
+
* box only when it covers a thin band of it, never on another box or caption,
|
|
40059
|
+
* and nowhere at all rather than over the subject.
|
|
39896
40060
|
*/
|
|
39897
|
-
function
|
|
40061
|
+
function notificationOverlay(input) {
|
|
39898
40062
|
const { width, height } = input;
|
|
39899
40063
|
const strokes = overlayStrokes(width, height);
|
|
39900
|
-
const fontSize = Math.max(
|
|
40064
|
+
const fontSize = Math.max(NC_LABEL_MIN_FONT_PX, Math.round(height / 36));
|
|
39901
40065
|
const parts = [];
|
|
39902
40066
|
for (const zone of input.zones) {
|
|
39903
40067
|
if (zone.length < 3) continue;
|
|
@@ -39909,19 +40073,29 @@ function notificationOverlaySvg(input) {
|
|
|
39909
40073
|
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
40074
|
}
|
|
39911
40075
|
const ordered = [...input.boxes.filter((b) => b.role === "other"), ...input.boxes.filter((b) => b.role === "subject")];
|
|
39912
|
-
|
|
40076
|
+
const layout = layoutBoxLabels({
|
|
40077
|
+
width,
|
|
40078
|
+
height,
|
|
40079
|
+
fontSize,
|
|
40080
|
+
boxes: ordered
|
|
40081
|
+
});
|
|
40082
|
+
const plates = [];
|
|
40083
|
+
ordered.forEach((box, i) => {
|
|
39913
40084
|
const subject = box.role === "subject";
|
|
39914
40085
|
const color = subject ? NC_IMAGE_SUBJECT_COLOR : NC_IMAGE_OTHER_COLOR;
|
|
39915
40086
|
const stroke = subject ? strokes.subject : strokes.other;
|
|
39916
40087
|
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
|
-
|
|
40088
|
+
const plate = layout.placements[i];
|
|
40089
|
+
if (plate === null || plate === void 0) return;
|
|
40090
|
+
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>`);
|
|
40091
|
+
});
|
|
40092
|
+
return {
|
|
40093
|
+
svg: `<svg xmlns="http://www.w3.org/2000/svg" width="${String(width)}" height="${String(height)}">${[...parts, ...plates].join("")}</svg>`,
|
|
40094
|
+
labels: {
|
|
40095
|
+
placed: layout.placed,
|
|
40096
|
+
dropped: layout.dropped
|
|
40097
|
+
}
|
|
40098
|
+
};
|
|
39925
40099
|
}
|
|
39926
40100
|
/** The pixel rectangle a `crop` cuts from a `width` × `height` frame, or null when too small. */
|
|
39927
40101
|
function subjectCropRect(box, width, height) {
|
|
@@ -39951,11 +40125,18 @@ function iou$4(a, b) {
|
|
|
39951
40125
|
}
|
|
39952
40126
|
/**
|
|
39953
40127
|
* The tracks to draw thin: every track at the instant except the subject —
|
|
39954
|
-
* the same id,
|
|
39955
|
-
* survivor's name)
|
|
39956
|
-
|
|
39957
|
-
|
|
39958
|
-
|
|
40128
|
+
* the same id, a box that is the subject's (a fused id reads under its
|
|
40129
|
+
* survivor's name), or a box of the subject's class over it past what NMS
|
|
40130
|
+
* lets two real subjects share (an unfused piece of the same subject).
|
|
40131
|
+
*/
|
|
40132
|
+
function otherTracks(tracks, subjectTrackId, subjectBox, subjectClassName) {
|
|
40133
|
+
return tracks.filter((t) => {
|
|
40134
|
+
if (t.trackId === subjectTrackId) return false;
|
|
40135
|
+
if (subjectBox === null) return true;
|
|
40136
|
+
const overlap = iou$4(t.bbox, subjectBox);
|
|
40137
|
+
if (overlap >= SAME_SUBJECT_IOU) return false;
|
|
40138
|
+
return !(t.className === subjectClassName && overlap >= NC_IMAGE_SAME_CLASS_SUBJECT_IOU);
|
|
40139
|
+
});
|
|
39959
40140
|
}
|
|
39960
40141
|
/**
|
|
39961
40142
|
* What picture a ROW asks for. A row written since D897 carries `mediaImage`;
|
|
@@ -39972,9 +40153,15 @@ function payloadImageMode(payload) {
|
|
|
39972
40153
|
if (payload.media === "keyFrame") return "clean";
|
|
39973
40154
|
return "crop";
|
|
39974
40155
|
}
|
|
39975
|
-
/**
|
|
40156
|
+
/**
|
|
40157
|
+
* The zone ids a row's picture outlines: the zone(s) THIS event triggered,
|
|
40158
|
+
* when it asked — never every zone the rule lists (a dog in Uscio drew Aiuola
|
|
40159
|
+
* too). A row that asked but froze no `matchedZoneIds` (written before the
|
|
40160
|
+
* field) draws none: an outline of a zone the subject never entered is a
|
|
40161
|
+
* picture that lies about its own reason.
|
|
40162
|
+
*/
|
|
39976
40163
|
function payloadZoneOverlayIds(payload) {
|
|
39977
|
-
if (payload.mediaShowTriggerZone === true) return payload.
|
|
40164
|
+
if (payload.mediaShowTriggerZone === true) return payload.matchedZoneIds ?? [];
|
|
39978
40165
|
if (payload.mediaImage === void 0) return payload.mediaZoneIds ?? [];
|
|
39979
40166
|
return [];
|
|
39980
40167
|
}
|
|
@@ -40063,7 +40250,8 @@ async function renderNotificationImage(req) {
|
|
|
40063
40250
|
const boxes = req.mode === "scene-boxes" ? [...req.others.map((o) => ({
|
|
40064
40251
|
...toPixels(o, outW, outH),
|
|
40065
40252
|
role: "other",
|
|
40066
|
-
label: o.label
|
|
40253
|
+
label: o.label,
|
|
40254
|
+
...o.confidence !== void 0 ? { score: o.confidence } : {}
|
|
40067
40255
|
})), ...req.subject !== null ? [{
|
|
40068
40256
|
...toPixels(req.subject, outW, outH),
|
|
40069
40257
|
role: "subject",
|
|
@@ -40081,16 +40269,18 @@ async function renderNotificationImage(req) {
|
|
|
40081
40269
|
height: outH,
|
|
40082
40270
|
fit: "fill"
|
|
40083
40271
|
});
|
|
40272
|
+
let labels;
|
|
40084
40273
|
if (boxes.length > 0 || zones.length > 0 || paths.length > 0) {
|
|
40085
|
-
const
|
|
40274
|
+
const overlay = notificationOverlay({
|
|
40086
40275
|
width: outW,
|
|
40087
40276
|
height: outH,
|
|
40088
40277
|
boxes,
|
|
40089
40278
|
zones,
|
|
40090
40279
|
paths
|
|
40091
40280
|
});
|
|
40281
|
+
if (boxes.length > 0) labels = overlay.labels;
|
|
40092
40282
|
pipeline = pipeline.composite([{
|
|
40093
|
-
input: Buffer.from(svg),
|
|
40283
|
+
input: Buffer.from(overlay.svg),
|
|
40094
40284
|
top: 0,
|
|
40095
40285
|
left: 0
|
|
40096
40286
|
}]);
|
|
@@ -40100,7 +40290,8 @@ async function renderNotificationImage(req) {
|
|
|
40100
40290
|
return {
|
|
40101
40291
|
bytes: toArrayBufferBytes(out),
|
|
40102
40292
|
name: `${rendered}.jpg`,
|
|
40103
|
-
rendered
|
|
40293
|
+
rendered,
|
|
40294
|
+
...labels !== void 0 ? { labels } : {}
|
|
40104
40295
|
};
|
|
40105
40296
|
}
|
|
40106
40297
|
var NcNotificationImageStage = class {
|
|
@@ -40174,10 +40365,19 @@ var NcNotificationImageStage = class {
|
|
|
40174
40365
|
...subjectBox,
|
|
40175
40366
|
label: imageLabelText(payload.subject.label ?? payload.subject.className, payload.subject.confidence)
|
|
40176
40367
|
} : null;
|
|
40177
|
-
const others = mode === "scene-boxes" ? otherTracks(await this.tracksAt(camera, source.timestamp), subjectTrackId, subjectBox).map((t) => ({
|
|
40368
|
+
const others = mode === "scene-boxes" ? otherTracks(await this.tracksAt(camera, source.timestamp), subjectTrackId, subjectBox, payload.subject.className).map((t) => ({
|
|
40178
40369
|
...t.bbox,
|
|
40179
|
-
label: imageLabelText(t.label ?? t.className, t.confidence)
|
|
40370
|
+
label: imageLabelText(t.label ?? t.className, t.confidence),
|
|
40371
|
+
...t.confidence !== void 0 ? { confidence: t.confidence } : {}
|
|
40180
40372
|
})) : [];
|
|
40373
|
+
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", {
|
|
40374
|
+
tags: { deviceId: camera },
|
|
40375
|
+
meta: {
|
|
40376
|
+
ruleId: entry.ruleId,
|
|
40377
|
+
recordId: entry.recordId,
|
|
40378
|
+
ruleZoneIds: (payload.triggerZoneIds ?? []).join(",")
|
|
40379
|
+
}
|
|
40380
|
+
});
|
|
40181
40381
|
const zones = mode === "crop" || zoneIds.length === 0 ? [] : await this.zonePolygons(camera, zoneIds);
|
|
40182
40382
|
try {
|
|
40183
40383
|
const image = await this.render({
|
|
@@ -40190,6 +40390,7 @@ var NcNotificationImageStage = class {
|
|
|
40190
40390
|
});
|
|
40191
40391
|
const reason = image.rendered === mode ? void 0 : mode === "crop" ? "no-subject-box" : "no-tracks-at-instant";
|
|
40192
40392
|
if (reason !== void 0) this.reportFallback(input, mode, reason);
|
|
40393
|
+
if (image.labels !== void 0) this.reportLabels(input, image.labels);
|
|
40193
40394
|
return reason !== void 0 ? {
|
|
40194
40395
|
...image,
|
|
40195
40396
|
reason
|
|
@@ -40238,6 +40439,19 @@ var NcNotificationImageStage = class {
|
|
|
40238
40439
|
}
|
|
40239
40440
|
});
|
|
40240
40441
|
}
|
|
40442
|
+
/** ONE line per rendered picture: how many captions it carries and how many had no clear spot. */
|
|
40443
|
+
reportLabels(input, labels) {
|
|
40444
|
+
const camera = input.camera === "composite" ? input.entry.payload.subject.deviceId : input.camera;
|
|
40445
|
+
this.deps.logger.debug("notification picture captions laid out", {
|
|
40446
|
+
tags: { deviceId: camera },
|
|
40447
|
+
meta: {
|
|
40448
|
+
ruleId: input.entry.ruleId,
|
|
40449
|
+
recordId: input.entry.recordId,
|
|
40450
|
+
placed: labels.placed,
|
|
40451
|
+
dropped: labels.dropped
|
|
40452
|
+
}
|
|
40453
|
+
});
|
|
40454
|
+
}
|
|
40241
40455
|
/** The subject's trail, read once per render for its box AND its path. */
|
|
40242
40456
|
async subjectTrail(camera, trackId) {
|
|
40243
40457
|
const read = this.deps.getSubjectTrail;
|
|
@@ -46931,6 +47145,27 @@ function isRecord(value) {
|
|
|
46931
47145
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
46932
47146
|
}
|
|
46933
47147
|
/**
|
|
47148
|
+
* Incident 2026-10-22: a row stored with `groupingSec: 0` failed `NcRuleSchema` (min
|
|
47149
|
+
* {@link NC_GROUPING_MIN_SEC}) and the WHOLE rule was skipped as malformed on every load — a
|
|
47150
|
+
* system-event rule on which a window cannot even apply vanished from the engine and from
|
|
47151
|
+
* `listRules`. A non-positive (or non-numeric) window only ever meant "no window": dropped. A
|
|
47152
|
+
* positive one outside the range is clamped. One bad optional field never costs the rule.
|
|
47153
|
+
*/
|
|
47154
|
+
function repairStoredGroupingSec(value) {
|
|
47155
|
+
if (value === void 0) return { kind: "unchanged" };
|
|
47156
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return {
|
|
47157
|
+
kind: "repaired",
|
|
47158
|
+
from: value,
|
|
47159
|
+
to: null
|
|
47160
|
+
};
|
|
47161
|
+
const clamped = clampGroupingSec(value);
|
|
47162
|
+
return clamped === value ? { kind: "unchanged" } : {
|
|
47163
|
+
kind: "repaired",
|
|
47164
|
+
from: value,
|
|
47165
|
+
to: clamped
|
|
47166
|
+
};
|
|
47167
|
+
}
|
|
47168
|
+
/**
|
|
46934
47169
|
* Project a RAW stored rule value (`data['rule']`, before any Zod parse). A value that is not
|
|
46935
47170
|
* an object, or has no string `delivery`, is returned untouched: the schema parse that follows
|
|
46936
47171
|
* rejects it exactly as before, and a projection must never be what decides a row is valid.
|
|
@@ -46938,9 +47173,12 @@ function isRecord(value) {
|
|
|
46938
47173
|
function projectRawStoredRule(raw) {
|
|
46939
47174
|
if (!isRecord(raw) || typeof raw["delivery"] !== "string") return {
|
|
46940
47175
|
candidate: raw,
|
|
46941
|
-
projection: { kind: "unchanged" }
|
|
47176
|
+
projection: { kind: "unchanged" },
|
|
47177
|
+
groupingRepair: { kind: "unchanged" }
|
|
46942
47178
|
};
|
|
46943
|
-
const
|
|
47179
|
+
const groupingRepair = repairStoredGroupingSec(raw["groupingSec"]);
|
|
47180
|
+
const repaired = withRepairedGrouping(raw, groupingRepair);
|
|
47181
|
+
const groupingSec = repaired["groupingSec"];
|
|
46944
47182
|
const projection = projectRetiredRuleFields({
|
|
46945
47183
|
delivery: raw["delivery"],
|
|
46946
47184
|
conditions: isRecord(raw["conditions"]) ? raw["conditions"] : {},
|
|
@@ -46949,15 +47187,25 @@ function projectRawStoredRule(raw) {
|
|
|
46949
47187
|
...typeof groupingSec === "number" ? { groupingSec } : {}
|
|
46950
47188
|
});
|
|
46951
47189
|
if (projection.kind === "unchanged" || projection.groupingSec === void 0) return {
|
|
46952
|
-
candidate:
|
|
46953
|
-
projection
|
|
47190
|
+
candidate: repaired,
|
|
47191
|
+
projection,
|
|
47192
|
+
groupingRepair
|
|
46954
47193
|
};
|
|
46955
47194
|
return {
|
|
46956
47195
|
candidate: {
|
|
46957
|
-
...
|
|
47196
|
+
...repaired,
|
|
46958
47197
|
groupingSec: projection.groupingSec
|
|
46959
47198
|
},
|
|
46960
|
-
projection
|
|
47199
|
+
projection,
|
|
47200
|
+
groupingRepair
|
|
47201
|
+
};
|
|
47202
|
+
}
|
|
47203
|
+
function withRepairedGrouping(raw, repair) {
|
|
47204
|
+
if (repair.kind === "unchanged") return raw;
|
|
47205
|
+
const { groupingSec: _dropped, ...rest } = raw;
|
|
47206
|
+
return repair.to === null ? rest : {
|
|
47207
|
+
...rest,
|
|
47208
|
+
groupingSec: repair.to
|
|
46961
47209
|
};
|
|
46962
47210
|
}
|
|
46963
47211
|
/**
|
|
@@ -47010,17 +47258,45 @@ var LOAD_LIMIT = 1e4;
|
|
|
47010
47258
|
* Parse a raw body object (as returned from storage — may contain retired keys)
|
|
47011
47259
|
* into an `NcRule` applying the legacy field projection.
|
|
47012
47260
|
*/
|
|
47013
|
-
function bodyToRule(id, raw, onProjection) {
|
|
47014
|
-
const
|
|
47261
|
+
function bodyToRule(id, raw, onProjection, onGroupingRepair) {
|
|
47262
|
+
const parsed = parseStoredRule(id, raw, onProjection, onGroupingRepair);
|
|
47263
|
+
return parsed.ok ? parsed.rule : null;
|
|
47264
|
+
}
|
|
47265
|
+
function parseStoredRule(id, raw, onProjection, onGroupingRepair, onClassWidening) {
|
|
47266
|
+
const { candidate, projection, groupingRepair } = projectRawStoredRule(raw);
|
|
47015
47267
|
const parsed = require_clip_model_registry.NcRuleSchema.safeParse({
|
|
47016
47268
|
...candidate,
|
|
47017
47269
|
id
|
|
47018
47270
|
});
|
|
47019
|
-
if (!parsed.success)
|
|
47271
|
+
if (!parsed.success) {
|
|
47272
|
+
const issue = parsed.error.issues[0];
|
|
47273
|
+
return {
|
|
47274
|
+
ok: false,
|
|
47275
|
+
reason: issue === void 0 ? "invalid" : `${issue.path.join(".") || "(root)"}: ${issue.message}`
|
|
47276
|
+
};
|
|
47277
|
+
}
|
|
47020
47278
|
if (projection.kind === "projected") onProjection?.(id, projection);
|
|
47021
|
-
|
|
47279
|
+
if (groupingRepair.kind === "repaired") onGroupingRepair?.(id, groupingRepair);
|
|
47280
|
+
const media = withNormalizedMedia(parsed.data);
|
|
47281
|
+
const rule = require_clip_model_registry.withSystemEventClasses(media);
|
|
47282
|
+
if (rule !== media) onClassWidening?.(id, media, rule);
|
|
47283
|
+
return {
|
|
47284
|
+
ok: true,
|
|
47285
|
+
rule
|
|
47286
|
+
};
|
|
47022
47287
|
}
|
|
47023
47288
|
/**
|
|
47289
|
+
* D906: what a rule is stored as — its media policy folded (D897) and, on an
|
|
47290
|
+
* INSTANT system rule, every paired class it touches completed with both
|
|
47291
|
+
* edges. Applied on every WRITE; the load applies the same through
|
|
47292
|
+
* {@link parseStoredRule}, which also says once that it widened a rule.
|
|
47293
|
+
*/
|
|
47294
|
+
function normalizeStoredRule(rule) {
|
|
47295
|
+
return require_clip_model_registry.withSystemEventClasses(withNormalizedMedia(rule));
|
|
47296
|
+
}
|
|
47297
|
+
/** At most this many skipped rows are NAMED in the WARN line; the count is always whole. */
|
|
47298
|
+
var SKIPPED_ROWS_NAMED_MAX = 10;
|
|
47299
|
+
/**
|
|
47024
47300
|
* D897: the rule with its media policy in the stored shape — `image` set, the
|
|
47025
47301
|
* legacy `attach` / `frame` / `zoneCrop` folded into it. Applied on LOAD (a read
|
|
47026
47302
|
* projection: a rule nobody saved since keeps its row as written) and on every
|
|
@@ -47096,6 +47372,8 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47096
47372
|
now;
|
|
47097
47373
|
newId;
|
|
47098
47374
|
identityIdsByName;
|
|
47375
|
+
/** Rows the last successful {@link load} skipped; `null` before one succeeded. */
|
|
47376
|
+
skipped = null;
|
|
47099
47377
|
/** Has a {@link load} ever completed? See the note there. */
|
|
47100
47378
|
loaded = false;
|
|
47101
47379
|
/** Legacy detection rules naming sounds already reported (D690) — once each. */
|
|
@@ -47107,6 +47385,10 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47107
47385
|
pendingProjections = /* @__PURE__ */ new Map();
|
|
47108
47386
|
/** Rules whose projection was already logged in this process (D692 §9) — once each. */
|
|
47109
47387
|
projectionLogged = /* @__PURE__ */ new Set();
|
|
47388
|
+
/** Rules whose out-of-range `groupingSec` repair was already logged — once each. */
|
|
47389
|
+
groupingRepairLogged = /* @__PURE__ */ new Set();
|
|
47390
|
+
/** Rules whose class widening (D906) was already logged — once each. */
|
|
47391
|
+
classWideningLogged = /* @__PURE__ */ new Set();
|
|
47110
47392
|
constructor(deps) {
|
|
47111
47393
|
this.logger = deps.logger;
|
|
47112
47394
|
this.now = deps.now ?? (() => Date.now());
|
|
@@ -47141,20 +47423,29 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47141
47423
|
try {
|
|
47142
47424
|
const { records } = await this.kind.query({ limit: LOAD_LIMIT });
|
|
47143
47425
|
const next = /* @__PURE__ */ new Map();
|
|
47144
|
-
|
|
47426
|
+
const skippedRows = [];
|
|
47145
47427
|
for (const row of records) {
|
|
47146
|
-
const
|
|
47428
|
+
const parsed = parseStoredRule(row.id, row.data, (ruleId, projection) => {
|
|
47147
47429
|
this.pendingProjections.set(ruleId, projection);
|
|
47148
|
-
});
|
|
47149
|
-
if (
|
|
47150
|
-
|
|
47430
|
+
}, (ruleId, repair) => this.logGroupingRepair(ruleId, repair), (ruleId, before, after) => this.logClassWidening(ruleId, before, after));
|
|
47431
|
+
if (!parsed.ok) {
|
|
47432
|
+
const name = row.data["name"];
|
|
47433
|
+
skippedRows.push({
|
|
47434
|
+
ruleId: row.id,
|
|
47435
|
+
name: typeof name === "string" ? name : null,
|
|
47436
|
+
reason: parsed.reason
|
|
47437
|
+
});
|
|
47151
47438
|
continue;
|
|
47152
47439
|
}
|
|
47153
|
-
next.set(row.id, rule);
|
|
47440
|
+
next.set(row.id, parsed.rule);
|
|
47154
47441
|
}
|
|
47155
47442
|
this.byId.clear();
|
|
47156
47443
|
for (const [id, rule] of next) this.byId.set(id, rule);
|
|
47157
|
-
|
|
47444
|
+
this.skipped = skippedRows;
|
|
47445
|
+
if (skippedRows.length > 0) this.logger.warn("notification rule rows skipped as malformed", { meta: {
|
|
47446
|
+
skipped: skippedRows.length,
|
|
47447
|
+
rows: skippedRows.slice(0, SKIPPED_ROWS_NAMED_MAX)
|
|
47448
|
+
} });
|
|
47158
47449
|
this.loaded = true;
|
|
47159
47450
|
} catch (err) {
|
|
47160
47451
|
this.logger.warn("notification rules load failed — keeping rules already in memory", { meta: {
|
|
@@ -47166,6 +47457,27 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47166
47457
|
for (const id of warnLegacyAudioClassRules([...this.byId.values()], this.audioClassWarned, this.logger)) this.audioClassWarned.add(id);
|
|
47167
47458
|
this.logger.debug("notification rules loaded", { meta: { rules: this.byId.size } });
|
|
47168
47459
|
}
|
|
47460
|
+
/** Say, once per process per rule, that an instant rule was read as its whole class (D906). */
|
|
47461
|
+
logClassWidening(ruleId, before, after) {
|
|
47462
|
+
if (this.classWideningLogged.has(ruleId)) return;
|
|
47463
|
+
this.classWideningLogged.add(ruleId);
|
|
47464
|
+
const had = new Set(before.conditions.systemEvent?.kinds ?? []);
|
|
47465
|
+
this.logger.info("instant system rule read as its whole event class — both edges", { meta: {
|
|
47466
|
+
ruleId,
|
|
47467
|
+
rule: after.name,
|
|
47468
|
+
added: (after.conditions.systemEvent?.kinds ?? []).filter((kind) => !had.has(kind))
|
|
47469
|
+
} });
|
|
47470
|
+
}
|
|
47471
|
+
/** Say, once per process per rule, that an out-of-range stored `groupingSec` was repaired. */
|
|
47472
|
+
logGroupingRepair(ruleId, repair) {
|
|
47473
|
+
if (this.groupingRepairLogged.has(ruleId)) return;
|
|
47474
|
+
this.groupingRepairLogged.add(ruleId);
|
|
47475
|
+
this.logger.warn("rule carries an out-of-range groupingSec — repaired on read", { meta: {
|
|
47476
|
+
ruleId,
|
|
47477
|
+
from: repair.from,
|
|
47478
|
+
to: repair.to
|
|
47479
|
+
} });
|
|
47480
|
+
}
|
|
47169
47481
|
/** Log, once per process per rule, what `bodyToRule` projected from a retired field. */
|
|
47170
47482
|
logPendingProjections() {
|
|
47171
47483
|
for (const [ruleId, projection] of this.pendingProjections) {
|
|
@@ -47234,6 +47546,13 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47234
47546
|
* camera's ring) must ask this first: before the first load the empty list
|
|
47235
47547
|
* is not a fact, and a failed load leaves whatever was already mirrored.
|
|
47236
47548
|
*/
|
|
47549
|
+
/**
|
|
47550
|
+
* The rows the last SUCCESSFUL load could not parse — `null` until one has
|
|
47551
|
+
* succeeded, so "not read yet" never reads as "nothing broken" (D315).
|
|
47552
|
+
*/
|
|
47553
|
+
skippedRows() {
|
|
47554
|
+
return this.skipped;
|
|
47555
|
+
}
|
|
47237
47556
|
isLoaded() {
|
|
47238
47557
|
return this.loaded;
|
|
47239
47558
|
}
|
|
@@ -47263,7 +47582,7 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47263
47582
|
/** Create a new rule. `createdBy` is the SERVER-injected caller userId. */
|
|
47264
47583
|
async create(input, createdBy) {
|
|
47265
47584
|
const now = this.now();
|
|
47266
|
-
const rule =
|
|
47585
|
+
const rule = normalizeStoredRule({
|
|
47267
47586
|
...input,
|
|
47268
47587
|
id: this.newId(),
|
|
47269
47588
|
createdBy,
|
|
@@ -47308,7 +47627,7 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47308
47627
|
createdAt: existing.createdAt,
|
|
47309
47628
|
updatedAt: this.now()
|
|
47310
47629
|
};
|
|
47311
|
-
const updated =
|
|
47630
|
+
const updated = normalizeStoredRule(require_clip_model_registry.NcRuleSchema.parse(candidate));
|
|
47312
47631
|
await this.write(updated);
|
|
47313
47632
|
return updated;
|
|
47314
47633
|
}
|
|
@@ -47348,7 +47667,7 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47348
47667
|
id: ruleId
|
|
47349
47668
|
} : candidate);
|
|
47350
47669
|
if (!parsed.success) return existing;
|
|
47351
|
-
const next =
|
|
47670
|
+
const next = normalizeStoredRule(parsed.data);
|
|
47352
47671
|
const { id: _omit, ...data } = next;
|
|
47353
47672
|
await this.kind.put({
|
|
47354
47673
|
id: ruleId,
|
|
@@ -53377,6 +53696,104 @@ var NcZoneOwnershipCache = class {
|
|
|
53377
53696
|
}
|
|
53378
53697
|
};
|
|
53379
53698
|
//#endregion
|
|
53699
|
+
//#region src/notification-center/rule-health.ts
|
|
53700
|
+
var EMPTY_SNAPSHOT = {
|
|
53701
|
+
timelapse: [],
|
|
53702
|
+
summary: [],
|
|
53703
|
+
alarm: null,
|
|
53704
|
+
mutedDeviceIds: []
|
|
53705
|
+
};
|
|
53706
|
+
function fingerprintOf(reasons) {
|
|
53707
|
+
return reasons.map((r) => `${r.code}:${[...r.ids].sort().join(",")}`).join("|");
|
|
53708
|
+
}
|
|
53709
|
+
/** The deleted subjects a rule still names, from the prune's `kept*` records. */
|
|
53710
|
+
function deletedReasons(rule, prunes) {
|
|
53711
|
+
const snapshot = {
|
|
53712
|
+
...EMPTY_SNAPSHOT,
|
|
53713
|
+
rules: [rule]
|
|
53714
|
+
};
|
|
53715
|
+
const devices = referencedDeviceIds(snapshot);
|
|
53716
|
+
const targets = referencedTargetIds(snapshot);
|
|
53717
|
+
const deadDevices = /* @__PURE__ */ new Set();
|
|
53718
|
+
const deadTargets = /* @__PURE__ */ new Set();
|
|
53719
|
+
for (const p of prunes) {
|
|
53720
|
+
if (p.kind !== "rule" || p.ruleId !== rule.id) continue;
|
|
53721
|
+
if (p.outcome !== "kept" && p.outcome !== "kept-disabled") continue;
|
|
53722
|
+
if (p.subjectKind === "device" && devices.has(Number(p.subject))) deadDevices.add(p.subject);
|
|
53723
|
+
if (p.subjectKind === "target" && targets.has(p.subject)) deadTargets.add(p.subject);
|
|
53724
|
+
}
|
|
53725
|
+
const reasons = [];
|
|
53726
|
+
if (deadDevices.size > 0) reasons.push({
|
|
53727
|
+
code: "device-deleted",
|
|
53728
|
+
ids: [...deadDevices].sort()
|
|
53729
|
+
});
|
|
53730
|
+
if (deadTargets.size > 0) reasons.push({
|
|
53731
|
+
code: "target-deleted",
|
|
53732
|
+
ids: [...deadTargets].sort()
|
|
53733
|
+
});
|
|
53734
|
+
return reasons;
|
|
53735
|
+
}
|
|
53736
|
+
var NcRuleHealthMonitor = class {
|
|
53737
|
+
verdicts = /* @__PURE__ */ new Map();
|
|
53738
|
+
/** Judge every rule once; return the edges to announce. */
|
|
53739
|
+
evaluate(inputs) {
|
|
53740
|
+
const next = /* @__PURE__ */ new Map();
|
|
53741
|
+
const seen = /* @__PURE__ */ new Set();
|
|
53742
|
+
if (inputs.skipped !== null) for (const row of inputs.skipped) {
|
|
53743
|
+
seen.add(row.ruleId);
|
|
53744
|
+
const unparseable = {
|
|
53745
|
+
code: "unparseable",
|
|
53746
|
+
ids: [],
|
|
53747
|
+
detail: row.reason
|
|
53748
|
+
};
|
|
53749
|
+
next.set(row.ruleId, this.verdict(row.name, unparseable, []));
|
|
53750
|
+
}
|
|
53751
|
+
if (inputs.rules !== null) for (const rule of inputs.rules) {
|
|
53752
|
+
seen.add(rule.id);
|
|
53753
|
+
const previous = this.verdicts.get(rule.id);
|
|
53754
|
+
const deleted = inputs.prunes !== null ? deletedReasons(rule, inputs.prunes) : previous?.deleted ?? [];
|
|
53755
|
+
const unparseable = inputs.skipped !== null ? null : previous?.unparseable ?? null;
|
|
53756
|
+
next.set(rule.id, this.verdict(rule.name, unparseable, deleted));
|
|
53757
|
+
}
|
|
53758
|
+
const edges = [];
|
|
53759
|
+
const answeredAll = inputs.rules !== null && inputs.skipped !== null;
|
|
53760
|
+
for (const [ruleId, previous] of this.verdicts) {
|
|
53761
|
+
if (seen.has(ruleId)) continue;
|
|
53762
|
+
if (!answeredAll) next.set(ruleId, previous);
|
|
53763
|
+
}
|
|
53764
|
+
for (const [ruleId, verdict] of next) {
|
|
53765
|
+
const previous = this.verdicts.get(ruleId);
|
|
53766
|
+
const reasons = reasonsOf(verdict);
|
|
53767
|
+
if (reasons.length > 0) {
|
|
53768
|
+
if (previous?.fingerprint !== verdict.fingerprint) edges.push({
|
|
53769
|
+
kind: "broken",
|
|
53770
|
+
ruleId,
|
|
53771
|
+
ruleName: verdict.name,
|
|
53772
|
+
reasons
|
|
53773
|
+
});
|
|
53774
|
+
} else if (previous !== void 0 && reasonsOf(previous).length > 0) edges.push({
|
|
53775
|
+
kind: "repaired",
|
|
53776
|
+
ruleId,
|
|
53777
|
+
ruleName: verdict.name
|
|
53778
|
+
});
|
|
53779
|
+
}
|
|
53780
|
+
this.verdicts.clear();
|
|
53781
|
+
for (const [ruleId, verdict] of next) if (reasonsOf(verdict).length > 0) this.verdicts.set(ruleId, verdict);
|
|
53782
|
+
return edges;
|
|
53783
|
+
}
|
|
53784
|
+
verdict(name, unparseable, deleted) {
|
|
53785
|
+
return {
|
|
53786
|
+
name,
|
|
53787
|
+
unparseable,
|
|
53788
|
+
deleted,
|
|
53789
|
+
fingerprint: fingerprintOf([...unparseable !== null ? [unparseable] : [], ...deleted])
|
|
53790
|
+
};
|
|
53791
|
+
}
|
|
53792
|
+
};
|
|
53793
|
+
function reasonsOf(verdict) {
|
|
53794
|
+
return [...verdict.unparseable !== null ? [verdict.unparseable] : [], ...verdict.deleted];
|
|
53795
|
+
}
|
|
53796
|
+
//#endregion
|
|
53380
53797
|
//#region src/notification-center/index.ts
|
|
53381
53798
|
/**
|
|
53382
53799
|
* NotificationCenter — the P1 core of the Notification Center (spec
|
|
@@ -53443,7 +53860,7 @@ var NC_ALARM_ANNOUNCE_RULE_ID = "nc-alarm-announce";
|
|
|
53443
53860
|
* media (the SOURCE rule of a combined alarm row), and a zone overlay is
|
|
53444
53861
|
* frozen only when the rule asked for one AND fired on a zone.
|
|
53445
53862
|
*/
|
|
53446
|
-
function mediaPolicyFields(mediaRule, cropZoneIds) {
|
|
53863
|
+
function mediaPolicyFields(mediaRule, cropZoneIds, matchedZoneIds) {
|
|
53447
53864
|
const media = effectiveMediaPolicyForRule(mediaRule);
|
|
53448
53865
|
const image = media.image ?? "crop";
|
|
53449
53866
|
return {
|
|
@@ -53452,6 +53869,7 @@ function mediaPolicyFields(mediaRule, cropZoneIds) {
|
|
|
53452
53869
|
...media.showTriggerZone === true && cropZoneIds !== void 0 && cropZoneIds.length > 0 ? { mediaShowTriggerZone: true } : {},
|
|
53453
53870
|
...media.showSubjectPath === true && image !== "none" ? { mediaShowSubjectPath: true } : {},
|
|
53454
53871
|
...cropZoneIds !== void 0 ? { triggerZoneIds: [...cropZoneIds] } : {},
|
|
53872
|
+
...matchedZoneIds !== void 0 ? { matchedZoneIds: [...matchedZoneIds] } : {},
|
|
53455
53873
|
...media.profile !== void 0 ? { mediaProfile: media.profile } : {},
|
|
53456
53874
|
...media.gif === true ? { mediaGif: true } : {},
|
|
53457
53875
|
...media.clip === true ? { mediaClip: true } : {},
|
|
@@ -53957,6 +54375,21 @@ function groupOrderInput(fate, triggerZoneIds) {
|
|
|
53957
54375
|
* condition's ids, else an occupancy rule's one zone. A grouped member freezes the same
|
|
53958
54376
|
* ids a plain row of the rule would.
|
|
53959
54377
|
*/
|
|
54378
|
+
/**
|
|
54379
|
+
* The zones of `cropZoneIds` this subject actually triggered — what the zone
|
|
54380
|
+
* condition saw (membership plus the crossed zone, `zonesVisitedBy`), on the
|
|
54381
|
+
* rule whose media the row carries. On a combined alarm row the subject is the
|
|
54382
|
+
* alarm, so the source match's frozen zones answer. An occupancy rule's one
|
|
54383
|
+
* zone is the zone the count edge is ABOUT, so it is its own trigger.
|
|
54384
|
+
*/
|
|
54385
|
+
function matchedTriggerZoneIds(mediaRule, cropZoneIds, subject) {
|
|
54386
|
+
if (cropZoneIds === void 0) return void 0;
|
|
54387
|
+
if (mediaRule.conditions.zones === void 0) return [...cropZoneIds];
|
|
54388
|
+
const alarmSource = subject.systemEvent?.alarmSource;
|
|
54389
|
+
const visited = new Set(alarmSource !== void 0 ? alarmSource.zones ?? [] : subject.zones);
|
|
54390
|
+
if (alarmSource === void 0 && subject.crossing !== void 0) visited.add(subject.crossing.zoneId);
|
|
54391
|
+
return cropZoneIds.filter((id) => visited.has(id));
|
|
54392
|
+
}
|
|
53960
54393
|
function ruleCropZoneIds(rule) {
|
|
53961
54394
|
if (rule.conditions.zones !== void 0) return rule.conditions.zones.ids;
|
|
53962
54395
|
const zoneId = rule.conditions.occupancy?.zoneId;
|
|
@@ -54110,6 +54543,8 @@ var NotificationCenter = class {
|
|
|
54110
54543
|
* evaluating one.
|
|
54111
54544
|
*/
|
|
54112
54545
|
pruneLedger;
|
|
54546
|
+
/** D905: which rules cannot work, announced once per distinct reason. */
|
|
54547
|
+
ruleHealth = new NcRuleHealthMonitor();
|
|
54113
54548
|
/** The prune itself — see `device-prune-runner.ts`. */
|
|
54114
54549
|
pruner;
|
|
54115
54550
|
/**
|
|
@@ -57539,7 +57974,7 @@ var NotificationCenter = class {
|
|
|
57539
57974
|
const subject = incoming.subject;
|
|
57540
57975
|
const alarmSource = subject.systemEvent?.alarmSource;
|
|
57541
57976
|
const sourceRule = alarmSource !== void 0 ? this.rules.get(alarmSource.ruleId) : null;
|
|
57542
|
-
const media = sourceRule !== null ? mediaPolicyFields(sourceRule, ruleCropZoneIds(sourceRule)) : { media: "none" };
|
|
57977
|
+
const media = sourceRule !== null ? mediaPolicyFields(sourceRule, ruleCropZoneIds(sourceRule), matchedTriggerZoneIds(sourceRule, ruleCropZoneIds(sourceRule), subject)) : { media: "none" };
|
|
57543
57978
|
const mediaDeviceId = alarmSource?.deviceId ?? panel.deviceId;
|
|
57544
57979
|
const entries = targets.map((targetId) => ({
|
|
57545
57980
|
ruleId: NC_ALARM_ANNOUNCE_RULE_ID,
|
|
@@ -58745,6 +59180,7 @@ var NotificationCenter = class {
|
|
|
58745
59180
|
const renderedZones = notificationZones(rule, subject);
|
|
58746
59181
|
const confirm = frozenConfirmPolicy(rule, isSystemEvent);
|
|
58747
59182
|
const cropZoneIds = ruleCropZoneIds(mediaRule);
|
|
59183
|
+
const matchedZoneIds = matchedTriggerZoneIds(mediaRule, cropZoneIds, subject);
|
|
58748
59184
|
const allTargets = [...rule.targets, ...userTargets.filter((t) => !direct.has(t.targetId))];
|
|
58749
59185
|
const linkedOwners = !noMedia && subject.linkedStills !== void 0 ? linkedMediaOwners(subject.linkedStills, linkedCameraMode(mediaRule.media)) : void 0;
|
|
58750
59186
|
const groupV2 = extras?.groupV2 ?? (subject.systemEvent !== void 0 && !noMedia ? alarmIncidentGroup(subject.systemEvent, subject.timestamp) : void 0);
|
|
@@ -58755,7 +59191,7 @@ var NotificationCenter = class {
|
|
|
58755
59191
|
...origin === "synthetic" ? { synthetic: true } : {},
|
|
58756
59192
|
priority: rule.priority,
|
|
58757
59193
|
...rule.template !== void 0 ? { template: rule.template } : {},
|
|
58758
|
-
...noMedia ? { media: "none" } : mediaPolicyFields(mediaRule, cropZoneIds),
|
|
59194
|
+
...noMedia ? { media: "none" } : mediaPolicyFields(mediaRule, cropZoneIds, matchedZoneIds),
|
|
58759
59195
|
...confirm !== void 0 ? {
|
|
58760
59196
|
confirm,
|
|
58761
59197
|
...cooldown !== void 0 ? { cooldown } : {},
|
|
@@ -59400,6 +59836,51 @@ var NotificationCenter = class {
|
|
|
59400
59836
|
if (!this.pruneLedger.isLoaded()) await this.pruneLedger.load();
|
|
59401
59837
|
await this.pruner.reconcileDevices();
|
|
59402
59838
|
await this.pruner.reconcileTargets();
|
|
59839
|
+
this.announceRuleHealth();
|
|
59840
|
+
}
|
|
59841
|
+
/**
|
|
59842
|
+
* D905 — say which rules cannot work: a row the load skipped, or a rule still
|
|
59843
|
+
* naming a device / target the prune confirmed deleted. One edge per rule per
|
|
59844
|
+
* distinct set of reasons; a reading that did not answer changes nothing.
|
|
59845
|
+
*/
|
|
59846
|
+
announceRuleHealth() {
|
|
59847
|
+
const edges = this.ruleHealth.evaluate({
|
|
59848
|
+
rules: this.rules.isLoaded() ? this.rules.list() : null,
|
|
59849
|
+
skipped: this.rules.skippedRows(),
|
|
59850
|
+
prunes: this.pruneLedger.isLoaded() ? this.pruner.listPrunes() : null
|
|
59851
|
+
});
|
|
59852
|
+
for (const edge of edges) {
|
|
59853
|
+
const source = {
|
|
59854
|
+
type: "addon",
|
|
59855
|
+
id: "notification-center"
|
|
59856
|
+
};
|
|
59857
|
+
if (edge.kind === "broken") {
|
|
59858
|
+
const [first] = edge.reasons;
|
|
59859
|
+
if (first === void 0) continue;
|
|
59860
|
+
const offendingIds = [...new Set(edge.reasons.flatMap((r) => r.ids))];
|
|
59861
|
+
this.logger.warn("notification rule cannot work — announced", { meta: {
|
|
59862
|
+
ruleId: edge.ruleId,
|
|
59863
|
+
rule: edge.ruleName,
|
|
59864
|
+
reasons: edge.reasons
|
|
59865
|
+
} });
|
|
59866
|
+
this.deps.emitBusEvent?.(require_clip_model_registry.createEvent(require_clip_model_registry.EventCategory.NotificationRuleBroken, source, {
|
|
59867
|
+
ruleId: edge.ruleId,
|
|
59868
|
+
ruleName: edge.ruleName,
|
|
59869
|
+
reason: first.code,
|
|
59870
|
+
reasons: edge.reasons,
|
|
59871
|
+
offendingIds
|
|
59872
|
+
}));
|
|
59873
|
+
} else {
|
|
59874
|
+
this.logger.info("notification rule works again — announced", { meta: {
|
|
59875
|
+
ruleId: edge.ruleId,
|
|
59876
|
+
rule: edge.ruleName
|
|
59877
|
+
} });
|
|
59878
|
+
this.deps.emitBusEvent?.(require_clip_model_registry.createEvent(require_clip_model_registry.EventCategory.NotificationRuleRepaired, source, {
|
|
59879
|
+
ruleId: edge.ruleId,
|
|
59880
|
+
ruleName: edge.ruleName
|
|
59881
|
+
}));
|
|
59882
|
+
}
|
|
59883
|
+
}
|
|
59403
59884
|
}
|
|
59404
59885
|
/**
|
|
59405
59886
|
* `DeviceUnregistered` — a device was DELETED (only `removeDevice` emits it,
|
|
@@ -103523,6 +104004,8 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
|
|
|
103523
104004
|
require_clip_model_registry.EventCategory.StorageLocationLow,
|
|
103524
104005
|
require_clip_model_registry.EventCategory.StorageLocationCritical,
|
|
103525
104006
|
require_clip_model_registry.EventCategory.StorageLocationOk,
|
|
104007
|
+
require_clip_model_registry.EventCategory.NotificationRuleBroken,
|
|
104008
|
+
require_clip_model_registry.EventCategory.NotificationRuleRepaired,
|
|
103526
104009
|
require_clip_model_registry.EventCategory.AddonRunnerFailed,
|
|
103527
104010
|
require_clip_model_registry.EventCategory.AddonStarted,
|
|
103528
104011
|
require_clip_model_registry.EventCategory.AddonRestarted,
|
|
@@ -115553,6 +116036,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
115553
116036
|
nodeAddons: (nodeId) => api.nodes.getNodeAddons.query({ nodeId })
|
|
115554
116037
|
},
|
|
115555
116038
|
readAlerts: () => api.alerts.list.query({ limit: 5e3 }),
|
|
116039
|
+
emitBusEvent: (event) => this.ctx.eventBus.emit(event),
|
|
115556
116040
|
getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }),
|
|
115557
116041
|
recognitionOutlook: ({ deviceId, trackId, className, labelKind, now }) => recognitionOutlook({
|
|
115558
116042
|
className,
|