@camstack/addon-post-analysis 1.2.280 → 1.2.282
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{dist-Z6AO1MsY.mjs → dist-C3zTKShG.mjs} +552 -25
- package/dist/{dist-VeiAavYR.js → dist-D0zzdvSd.js} +599 -24
- 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-C_C1P6Bk.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-B9WQ8rry.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DT-cQNv-.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-B62fofgN.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DWPDYuJS.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-B5__n3rE.mjs → hostInit-lUAYfFf-.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +592 -26
- package/dist/pipeline-analytics/index.mjs +592 -26
- 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-DBnj0lb3.mjs +0 -26
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-D0zzdvSd.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -287,6 +287,7 @@ function coverageFor(rules, panelDeviceId, mode, skipProbe) {
|
|
|
287
287
|
const ids = /* @__PURE__ */ new Set();
|
|
288
288
|
let ruleCount = 0;
|
|
289
289
|
let allDevices = false;
|
|
290
|
+
let except = null;
|
|
290
291
|
for (const rule of rules) {
|
|
291
292
|
if (!rule.enabled) continue;
|
|
292
293
|
const gate = rule.conditions.deviceState;
|
|
@@ -294,21 +295,35 @@ function coverageFor(rules, panelDeviceId, mode, skipProbe) {
|
|
|
294
295
|
if (!gate.states.includes(wanted)) continue;
|
|
295
296
|
ruleCount += 1;
|
|
296
297
|
const scope = rule.conditions.devices;
|
|
298
|
+
const excluded = rule.conditions.devicesExclude ?? [];
|
|
297
299
|
if (scope === void 0 || scope.length === 0) {
|
|
298
300
|
allDevices = true;
|
|
301
|
+
except = narrowExcept(except, excluded);
|
|
299
302
|
continue;
|
|
300
303
|
}
|
|
301
|
-
for (const id of scope) ids.add(id);
|
|
304
|
+
for (const id of scope) if (!excluded.includes(id)) ids.add(id);
|
|
302
305
|
}
|
|
303
306
|
const deviceIds = [...ids].toSorted((a, b) => a - b);
|
|
307
|
+
const allDevicesExcept = except === null || except === "none" ? [] : [...except].filter((id) => !ids.has(id)).toSorted((a, b) => a - b);
|
|
304
308
|
return {
|
|
305
309
|
mode,
|
|
306
310
|
ruleCount,
|
|
307
311
|
allDevices,
|
|
312
|
+
allDevicesExcept,
|
|
308
313
|
deviceIds,
|
|
309
314
|
skippedDevices: skippedFor(deviceIds, skipProbe)
|
|
310
315
|
};
|
|
311
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Fold one UNSCOPED covering rule into the except-set: with no exclusion it
|
|
319
|
+
* covers everything (`'none'`, final); otherwise only what every unscoped rule
|
|
320
|
+
* excludes stays uncovered — the intersection.
|
|
321
|
+
*/
|
|
322
|
+
function narrowExcept(current, excluded) {
|
|
323
|
+
if (current === "none" || excluded.length === 0) return "none";
|
|
324
|
+
if (current === null) return new Set(excluded);
|
|
325
|
+
return new Set(excluded.filter((id) => current.has(id)));
|
|
326
|
+
}
|
|
312
327
|
function skippedFor(deviceIds, skipProbe) {
|
|
313
328
|
if (skipProbe === void 0) return [];
|
|
314
329
|
const out = [];
|
|
@@ -382,6 +397,19 @@ function buildArmAnnouncement(coverage, deviceName, bypassed = []) {
|
|
|
382
397
|
textCount: names.length
|
|
383
398
|
};
|
|
384
399
|
}
|
|
400
|
+
if (coverage.allDevices && coverage.allDevicesExcept.length > 0) {
|
|
401
|
+
const names = coverage.allDevicesExcept.map(deviceName);
|
|
402
|
+
return {
|
|
403
|
+
bodyKey: "alarm.arm.body.allDevicesExcept",
|
|
404
|
+
textParams: {
|
|
405
|
+
rules,
|
|
406
|
+
mode,
|
|
407
|
+
count: `${names.length}`,
|
|
408
|
+
names: names.join(", ")
|
|
409
|
+
},
|
|
410
|
+
textCount: names.length
|
|
411
|
+
};
|
|
412
|
+
}
|
|
385
413
|
if (coverage.allDevices) return {
|
|
386
414
|
bodyKey: "alarm.arm.body.allDevices",
|
|
387
415
|
textParams: {
|
|
@@ -600,7 +628,8 @@ function alarmCoveredDevices(rules, panelDeviceId, mode) {
|
|
|
600
628
|
if (!gate.states.includes(wanted)) continue;
|
|
601
629
|
const scope = rule.conditions.devices;
|
|
602
630
|
if (scope === void 0 || scope.length === 0) continue;
|
|
603
|
-
|
|
631
|
+
const excluded = rule.conditions.devicesExclude ?? [];
|
|
632
|
+
for (const id of scope) if (!excluded.includes(id)) ids.add(id);
|
|
604
633
|
}
|
|
605
634
|
return [...ids].toSorted((a, b) => a - b);
|
|
606
635
|
}
|
|
@@ -7795,10 +7824,10 @@ function replaceConditionReferences(monitor, condition, reference) {
|
|
|
7795
7824
|
//#endregion
|
|
7796
7825
|
//#region src/notification-center/snooze-exempt.ts
|
|
7797
7826
|
/** The cap whose presence in a sequence makes the rule the alarm's trigger. */
|
|
7798
|
-
var ALARM_PANEL_CAP$
|
|
7827
|
+
var ALARM_PANEL_CAP$1 = "alarm-panel";
|
|
7799
7828
|
var ALARM_KINDS = new Set(require_dist.NC_ALARM_SYSTEM_EVENT_KINDS);
|
|
7800
7829
|
function actuatesAlarmPanel(sequences) {
|
|
7801
|
-
for (const sequence of sequences) for (const step of sequence.actions) if (step.kind === "cap" && step.cap === ALARM_PANEL_CAP$
|
|
7830
|
+
for (const sequence of sequences) for (const step of sequence.actions) if (step.kind === "cap" && step.cap === ALARM_PANEL_CAP$1) return true;
|
|
7802
7831
|
return false;
|
|
7803
7832
|
}
|
|
7804
7833
|
/**
|
|
@@ -9563,6 +9592,7 @@ function isLinkedSetSubject(subject) {
|
|
|
9563
9592
|
function subjectFromSensorEvent(ev, markerTrackId, linked) {
|
|
9564
9593
|
const eventType = readSensorEventType(ev.value);
|
|
9565
9594
|
const sensorEdge = sensorEdgeOf(ev.kind, ev.value);
|
|
9595
|
+
const sensorRaised = require_dist.sensorRaisedOf(ev.kind, ev.value);
|
|
9566
9596
|
return {
|
|
9567
9597
|
kind: "device-event",
|
|
9568
9598
|
recordId: String(ev.id),
|
|
@@ -9576,6 +9606,7 @@ function subjectFromSensorEvent(ev, markerTrackId, linked) {
|
|
|
9576
9606
|
sensorKind: ev.kind,
|
|
9577
9607
|
...eventType !== void 0 ? { eventType } : {},
|
|
9578
9608
|
...sensorEdge !== void 0 ? { sensorEdge } : {},
|
|
9609
|
+
...sensorRaised !== void 0 ? { sensorRaised } : {},
|
|
9579
9610
|
...linked !== void 0 ? {
|
|
9580
9611
|
linkedSensorId: ev.sourceDeviceId,
|
|
9581
9612
|
linkedCameraIds: [...linked.linkedCameraIds],
|
|
@@ -9803,6 +9834,7 @@ function presentConditionIds(c) {
|
|
|
9803
9834
|
const ids = [];
|
|
9804
9835
|
if (c.systemEvent !== void 0) ids.push("systemEvent");
|
|
9805
9836
|
if (c.devices !== void 0 && c.devices.length > 0) ids.push("devices");
|
|
9837
|
+
if (c.devicesExclude !== void 0 && c.devicesExclude.length > 0) ids.push("devicesExclude");
|
|
9806
9838
|
if (c.source !== void 0 && c.source !== "any") ids.push("source");
|
|
9807
9839
|
if (c.classes !== void 0 && c.classes.length > 0) ids.push("classes");
|
|
9808
9840
|
if (c.classesExclude !== void 0 && c.classesExclude.length > 0) ids.push("classesExclude");
|
|
@@ -9819,6 +9851,7 @@ function presentConditionIds(c) {
|
|
|
9819
9851
|
if (c.plates !== void 0) ids.push("plates");
|
|
9820
9852
|
if (c.sensorKinds !== void 0 && c.sensorKinds.length > 0) ids.push("sensorKinds");
|
|
9821
9853
|
if (c.eventTypeTokens !== void 0 && c.eventTypeTokens.length > 0) ids.push("eventTypeTokens");
|
|
9854
|
+
if (c.sensorTransition !== void 0) ids.push("sensorTransition");
|
|
9822
9855
|
if (c.packagePhase !== void 0 && c.packagePhase !== "both") ids.push("packagePhase");
|
|
9823
9856
|
if (c.customZones !== void 0 && c.customZones.length > 0) ids.push("customZones");
|
|
9824
9857
|
if (c.occupancy !== void 0) ids.push("occupancy");
|
|
@@ -9913,12 +9946,23 @@ function zonesVisitedBy(subject) {
|
|
|
9913
9946
|
* before the subject exists (the linked-camera capture gate). Coarse in one
|
|
9914
9947
|
* direction only: `true` may still fail later; `false` never would have fired.
|
|
9915
9948
|
*/
|
|
9916
|
-
function deviceEventRuleMayFireOnKind(rule, sensorKind) {
|
|
9949
|
+
function deviceEventRuleMayFireOnKind(rule, sensorKind, sensorRaised) {
|
|
9917
9950
|
const c = rule.conditions;
|
|
9918
9951
|
if (c.occupancy !== void 0) return false;
|
|
9952
|
+
if (sensorTransitionRefuses(c.sensorTransition, sensorRaised)) return false;
|
|
9919
9953
|
if (c.sensorKinds === void 0 || c.sensorKinds.length === 0) return true;
|
|
9920
9954
|
return toLowerSet(c.sensorKinds).has(sensorKind.trim().toLowerCase());
|
|
9921
9955
|
}
|
|
9956
|
+
/**
|
|
9957
|
+
* Does a rule's `sensorTransition` refuse this edge? Only a KNOWN edge can be
|
|
9958
|
+
* refused: `sensorRaised === undefined` (a kind with no raise, an unreadable
|
|
9959
|
+
* slice) always passes — a flood rule never goes silent on a bad read (D640).
|
|
9960
|
+
* The ONE predicate {@link evaluateRule} and the capture gate both ask.
|
|
9961
|
+
*/
|
|
9962
|
+
function sensorTransitionRefuses(transition, sensorRaised) {
|
|
9963
|
+
if (transition === void 0 || sensorRaised === void 0) return false;
|
|
9964
|
+
return sensorRaised !== (transition === "raised");
|
|
9965
|
+
}
|
|
9922
9966
|
function toLowerSet(values) {
|
|
9923
9967
|
return new Set(values.map((v) => v.trim().toLowerCase()));
|
|
9924
9968
|
}
|
|
@@ -9955,9 +9999,6 @@ function matchesIdentityScope(subject, scope) {
|
|
|
9955
9999
|
function ruleAwaitsRecognition(rule) {
|
|
9956
10000
|
return rule.conditions.identities !== void 0 || rule.conditions.plates !== void 0;
|
|
9957
10001
|
}
|
|
9958
|
-
var ARMED_WORD_PREFIX = "armed_";
|
|
9959
|
-
var ALARM_PANEL_CAP$1 = "alarm-panel";
|
|
9960
|
-
var ALARM_PANEL_TRIGGER_METHOD = "trigger";
|
|
9961
10002
|
/**
|
|
9962
10003
|
* A rule "belongs to the alarm" when its `deviceState` gate names ANY device's
|
|
9963
10004
|
* `armed_*` word — CamStack's own panel or any other, e.g. an HA-mirrored
|
|
@@ -9975,8 +10016,10 @@ var ALARM_PANEL_TRIGGER_METHOD = "trigger";
|
|
|
9975
10016
|
* catalog on save, so a new key would be dropped by the first edit.
|
|
9976
10017
|
*/
|
|
9977
10018
|
function ruleBelongsToAlarm(rule) {
|
|
9978
|
-
|
|
9979
|
-
|
|
10019
|
+
return require_dist.ruleBelongsToAlarm({
|
|
10020
|
+
conditions: rule.conditions,
|
|
10021
|
+
...rule.actions?.onTrigger !== void 0 ? { onTrigger: rule.actions.onTrigger } : {}
|
|
10022
|
+
});
|
|
9980
10023
|
}
|
|
9981
10024
|
/**
|
|
9982
10025
|
* The edge an alarm rule refuses: a close, or a halt/fault (a cover stopped
|
|
@@ -10152,6 +10195,7 @@ function evaluateRule(rule, subject, deviceState, zoneOwners, sceneStates) {
|
|
|
10152
10195
|
if (!sceneConditionSatisfied(c.scene, observation)) return fail("scene");
|
|
10153
10196
|
}
|
|
10154
10197
|
if (c.devices !== void 0 && c.devices.length > 0 && !matchesDeviceScope(c.devices, subject)) return fail("devices");
|
|
10198
|
+
if (c.devicesExclude !== void 0 && deviceScopeExcludes(c.devicesExclude, subject)) return fail("devicesExclude");
|
|
10155
10199
|
if (c.source !== void 0 && c.source !== "any") {
|
|
10156
10200
|
if ((subject.source ?? "pipeline") !== c.source) return fail("source");
|
|
10157
10201
|
}
|
|
@@ -10228,6 +10272,7 @@ function evaluateRule(rule, subject, deviceState, zoneOwners, sceneStates) {
|
|
|
10228
10272
|
if (c.eventTypeTokens !== void 0 && c.eventTypeTokens.length > 0) {
|
|
10229
10273
|
if (subject.eventType === void 0 || !toLowerSet(c.eventTypeTokens).has(subject.eventType.trim().toLowerCase())) return fail("eventTypeTokens");
|
|
10230
10274
|
}
|
|
10275
|
+
if (sensorTransitionRefuses(c.sensorTransition, subject.sensorRaised)) return fail("sensorTransition");
|
|
10231
10276
|
if (c.packagePhase !== void 0 && c.packagePhase !== "both") {
|
|
10232
10277
|
if (subject.packagePhase !== c.packagePhase) return fail("packagePhase");
|
|
10233
10278
|
}
|
|
@@ -10333,6 +10378,27 @@ function matchesDeviceScope(scope, subject) {
|
|
|
10333
10378
|
if (scope.includes(subject.deviceId)) return true;
|
|
10334
10379
|
return subject.sourceDeviceId !== void 0 && scope.includes(subject.sourceDeviceId);
|
|
10335
10380
|
}
|
|
10381
|
+
/**
|
|
10382
|
+
* Does a rule's `devicesExclude` (D641) exclude this subject?
|
|
10383
|
+
*
|
|
10384
|
+
* The exact complement of {@link matchesDeviceScope}, row by row. A per-camera
|
|
10385
|
+
* row is excluded by its camera OR by the sensor that fired, the two ids an
|
|
10386
|
+
* include matches on. A linked set stands for the rows it replaced (D630 §6):
|
|
10387
|
+
* it is excluded when its SENSOR is, or when EVERY camera that took a row is —
|
|
10388
|
+
* one excluded row camera leaves the others firing, as their own rows would
|
|
10389
|
+
* have. A camera that only LINKS the sensor excludes nothing, for the reason it
|
|
10390
|
+
* includes nothing: ADR 0632 widens the media, never which rules fire.
|
|
10391
|
+
*/
|
|
10392
|
+
function deviceScopeExcludes(excluded, subject) {
|
|
10393
|
+
if (excluded.length === 0) return false;
|
|
10394
|
+
if (subject.linkedSensorId !== void 0) {
|
|
10395
|
+
if (excluded.includes(subject.linkedSensorId)) return true;
|
|
10396
|
+
const rows = subject.rowCameraIds ?? [];
|
|
10397
|
+
return rows.length > 0 && rows.every((cameraId) => excluded.includes(cameraId));
|
|
10398
|
+
}
|
|
10399
|
+
if (excluded.includes(subject.deviceId)) return true;
|
|
10400
|
+
return subject.sourceDeviceId !== void 0 && excluded.includes(subject.sourceDeviceId);
|
|
10401
|
+
}
|
|
10336
10402
|
function cooldownKey(rule, subject) {
|
|
10337
10403
|
const first = subject.classNames[0];
|
|
10338
10404
|
const classKey = keysPerClass(rule, subject) && first !== void 0 ? `:c:${first}` : "";
|
|
@@ -12460,6 +12526,7 @@ var en_default = {
|
|
|
12460
12526
|
"snooze.scope.anyRule": "A rule",
|
|
12461
12527
|
"snooze.scope.rule": "“{{rule}}”",
|
|
12462
12528
|
"alarm.arm.body.allDevices": "Active: all devices",
|
|
12529
|
+
"alarm.arm.body.allDevicesExcept": "Active: all devices except {{count}}: {{names}}",
|
|
12463
12530
|
"alarm.arm.body.noDevices": "Active: no device restriction",
|
|
12464
12531
|
"alarm.arm.body.devices": "Active: {{names}}",
|
|
12465
12532
|
"alarm.arm.body.devicesSkipped": "Active: {{names}}\nExcluded: {{skippedNames}}",
|
|
@@ -12748,6 +12815,7 @@ var it_default = {
|
|
|
12748
12815
|
"snooze.scope.anyRule": "Una regola",
|
|
12749
12816
|
"snooze.scope.rule": "“{{rule}}”",
|
|
12750
12817
|
"alarm.arm.body.allDevices": "Attivi: tutti i dispositivi",
|
|
12818
|
+
"alarm.arm.body.allDevicesExcept": "Attivi: tutti i dispositivi tranne {{count}}: {{names}}",
|
|
12751
12819
|
"alarm.arm.body.noDevices": "Attivi: nessuna restrizione",
|
|
12752
12820
|
"alarm.arm.body.devices": "Attivi: {{names}}",
|
|
12753
12821
|
"alarm.arm.body.devicesSkipped": "Attivi: {{names}}\nEsclusi: {{skippedNames}}",
|
|
@@ -13150,6 +13218,245 @@ function pickTrackNarrationFrames(rows, max) {
|
|
|
13150
13218
|
return picked;
|
|
13151
13219
|
}
|
|
13152
13220
|
//#endregion
|
|
13221
|
+
//#region src/notification-center/rule-family-guard.ts
|
|
13222
|
+
/**
|
|
13223
|
+
* The hub's half of D637: a NEW or UPDATED system-event rule answers ONE family
|
|
13224
|
+
* (device events, system, alarm) and carries only that family's filters.
|
|
13225
|
+
*
|
|
13226
|
+
* THE authority for the invariant. The cap-provider route
|
|
13227
|
+
* (`notificationRules.createRule/updateRule`, index.ts) and the rule bridge
|
|
13228
|
+
* (`nc.createRule/updateRule`, nc-actions.ts) both call it through
|
|
13229
|
+
* `guardRuleFamily` (create) and `guardPatchFamily` (update), so the two save
|
|
13230
|
+
* paths cannot drift. This is the same
|
|
13231
|
+
* arrangement as `assertRuleHasAddressee`.
|
|
13232
|
+
*
|
|
13233
|
+
* Not in Zod: a cross-field refine on `NcConditionsSchema` would also reject the
|
|
13234
|
+
* STORED mixed rules at load (NcRuleSchema.parse in the store), and those must
|
|
13235
|
+
* keep firing until the operator recreates them. The engine is untouched, and
|
|
13236
|
+
* so is the device-prune runner: removing a deleted device id from a stored
|
|
13237
|
+
* rule is not authoring.
|
|
13238
|
+
*/
|
|
13239
|
+
var NcRuleFamilyRefusal = class extends Error {
|
|
13240
|
+
problems;
|
|
13241
|
+
/** The rule that was judged: the input on a create, the MERGED rule on an update. */
|
|
13242
|
+
rule;
|
|
13243
|
+
constructor(problems, rule) {
|
|
13244
|
+
super(`system-event rule refused: ${problems.map(require_dist.describeSystemEventFamilyProblem).join("; ")}`);
|
|
13245
|
+
this.name = "NcRuleFamilyRefusal";
|
|
13246
|
+
this.problems = problems;
|
|
13247
|
+
this.rule = rule;
|
|
13248
|
+
}
|
|
13249
|
+
};
|
|
13250
|
+
function assertRuleFamily(rule) {
|
|
13251
|
+
if (!require_dist.isSystemDelivery(rule.delivery)) return;
|
|
13252
|
+
const problems = require_dist.systemEventFamilyProblems(rule.conditions.systemEvent);
|
|
13253
|
+
if (problems.length > 0) throw new NcRuleFamilyRefusal(problems, rule);
|
|
13254
|
+
}
|
|
13255
|
+
var byValue = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
13256
|
+
var byNumber = (a, b) => a - b;
|
|
13257
|
+
/**
|
|
13258
|
+
* A system-event condition with every list sorted. The engine reads `kinds` and
|
|
13259
|
+
* the id-list filters as SETS (membership only), so an editor that re-sorts
|
|
13260
|
+
* them — the admin's `toggleSystemEventKind` puts kinds in option order — has
|
|
13261
|
+
* not changed the rule. Copies; the input is never touched.
|
|
13262
|
+
*/
|
|
13263
|
+
function sortedSystemEvent(condition) {
|
|
13264
|
+
const { kinds, deviceIds, excludeDeviceIds, deviceTypes, nodeIds, packageNames } = condition;
|
|
13265
|
+
return {
|
|
13266
|
+
...condition,
|
|
13267
|
+
kinds: kinds.toSorted(byValue),
|
|
13268
|
+
...deviceIds !== void 0 ? { deviceIds: deviceIds.toSorted(byNumber) } : {},
|
|
13269
|
+
...excludeDeviceIds !== void 0 ? { excludeDeviceIds: excludeDeviceIds.toSorted(byNumber) } : {},
|
|
13270
|
+
...deviceTypes !== void 0 ? { deviceTypes: deviceTypes.toSorted(byValue) } : {},
|
|
13271
|
+
...nodeIds !== void 0 ? { nodeIds: nodeIds.toSorted(byValue) } : {},
|
|
13272
|
+
...packageNames !== void 0 ? { packageNames: packageNames.toSorted(byValue) } : {}
|
|
13273
|
+
};
|
|
13274
|
+
}
|
|
13275
|
+
/** The canonical form two conditions are compared in. */
|
|
13276
|
+
function conditionsFingerprint(conditions) {
|
|
13277
|
+
const { systemEvent } = conditions;
|
|
13278
|
+
return require_dist.canonicalHash(systemEvent === void 0 ? conditions : {
|
|
13279
|
+
...conditions,
|
|
13280
|
+
systemEvent: sortedSystemEvent(systemEvent)
|
|
13281
|
+
});
|
|
13282
|
+
}
|
|
13283
|
+
/**
|
|
13284
|
+
* Do two conditions say the same thing? Both go through `NcConditionsSchema`
|
|
13285
|
+
* (the normalisation the store applies on every write) and are compared
|
|
13286
|
+
* canonically: key order is irrelevant, an `undefined` key equals an absent
|
|
13287
|
+
* one, and the system-event `kinds` and id-list filters are compared as SETS.
|
|
13288
|
+
* Every other array keeps its order. A side that does not parse is treated as
|
|
13289
|
+
* a change, so it is judged rather than waved through.
|
|
13290
|
+
*/
|
|
13291
|
+
function sameConditions(a, b) {
|
|
13292
|
+
const left = require_dist.NcConditionsSchema.safeParse(a);
|
|
13293
|
+
const right = require_dist.NcConditionsSchema.safeParse(b);
|
|
13294
|
+
if (!left.success || !right.success) return false;
|
|
13295
|
+
return conditionsFingerprint(left.data) === conditionsFingerprint(right.data);
|
|
13296
|
+
}
|
|
13297
|
+
/**
|
|
13298
|
+
* The rule a patch WOULD produce, as far as the family is concerned — merged
|
|
13299
|
+
* the way the store merges it: `delivery` and `conditions` replace whole.
|
|
13300
|
+
*
|
|
13301
|
+
* `null` when the patch CHANGES neither. Both editors send the whole rule on
|
|
13302
|
+
* every save, so "the patch carries `conditions`" is not "the patch changes
|
|
13303
|
+
* them": an unchanged full-form save of a stored mixed rule (a rename, a
|
|
13304
|
+
* retarget) must pass, and only a save that alters the delivery or the
|
|
13305
|
+
* conditions is judged.
|
|
13306
|
+
*/
|
|
13307
|
+
function mergedFamilySubject(existing, patch) {
|
|
13308
|
+
const deliveryChanged = patch.delivery !== void 0 && patch.delivery !== existing.delivery;
|
|
13309
|
+
const conditionsChanged = patch.conditions !== void 0 && !sameConditions(patch.conditions, existing.conditions);
|
|
13310
|
+
if (!deliveryChanged && !conditionsChanged) return null;
|
|
13311
|
+
return {
|
|
13312
|
+
delivery: patch.delivery ?? existing.delivery,
|
|
13313
|
+
conditions: patch.conditions ?? existing.conditions
|
|
13314
|
+
};
|
|
13315
|
+
}
|
|
13316
|
+
/**
|
|
13317
|
+
* On update, the MERGED rule is judged. A patch that changes neither `delivery`
|
|
13318
|
+
* nor `conditions` (absent, or equal to what is stored) leaves the kinds as
|
|
13319
|
+
* they are, so a stored mixed rule can still be switched off, renamed or
|
|
13320
|
+
* retargeted before the operator recreates it.
|
|
13321
|
+
*/
|
|
13322
|
+
function assertPatchKeepsFamily(existing, patch) {
|
|
13323
|
+
if (existing === null) return;
|
|
13324
|
+
const merged = mergedFamilySubject(existing, patch);
|
|
13325
|
+
if (merged !== null) assertRuleFamily(merged);
|
|
13326
|
+
}
|
|
13327
|
+
/**
|
|
13328
|
+
* Run a family check and LOG a refusal: who tried to save, the rule that was
|
|
13329
|
+
* judged (its delivery and kinds) and every problem in words (the families,
|
|
13330
|
+
* the offending kind or filter). Then rethrow. A refused save is dropped work,
|
|
13331
|
+
* and dropped work is never silent. Any other error passes through unlogged.
|
|
13332
|
+
*/
|
|
13333
|
+
function logRefusal$2(check, ctx) {
|
|
13334
|
+
try {
|
|
13335
|
+
check();
|
|
13336
|
+
} catch (err) {
|
|
13337
|
+
if (err instanceof NcRuleFamilyRefusal) ctx.logger.warn("nc rule refused: outside one family", { meta: {
|
|
13338
|
+
...ctx.ruleId !== void 0 ? { ruleId: ctx.ruleId } : {},
|
|
13339
|
+
by: ctx.by,
|
|
13340
|
+
delivery: err.rule.delivery,
|
|
13341
|
+
kinds: err.rule.conditions.systemEvent?.kinds ?? [],
|
|
13342
|
+
problems: err.problems.map(require_dist.describeSystemEventFamilyProblem)
|
|
13343
|
+
} });
|
|
13344
|
+
throw err;
|
|
13345
|
+
}
|
|
13346
|
+
}
|
|
13347
|
+
/** {@link assertRuleFamily}, logged. The create path of both routes. */
|
|
13348
|
+
function guardRuleFamily(rule, ctx) {
|
|
13349
|
+
logRefusal$2(() => assertRuleFamily(rule), ctx);
|
|
13350
|
+
}
|
|
13351
|
+
/** {@link assertPatchKeepsFamily}, logged. The update path of both routes. */
|
|
13352
|
+
function guardPatchFamily(existing, patch, ctx) {
|
|
13353
|
+
logRefusal$2(() => assertPatchKeepsFamily(existing, patch), ctx);
|
|
13354
|
+
}
|
|
13355
|
+
//#endregion
|
|
13356
|
+
//#region src/notification-center/device-scope-guard.ts
|
|
13357
|
+
/**
|
|
13358
|
+
* The deliveries whose evaluation reads the device scope — the `devices`
|
|
13359
|
+
* descriptor's `appliesTo`, restated as the one delivery that lacks it.
|
|
13360
|
+
*/
|
|
13361
|
+
var NO_DEVICE_SCOPE_DELIVERY = "system-event";
|
|
13362
|
+
var NcDevicesExcludeRefusal = class extends Error {
|
|
13363
|
+
delivery;
|
|
13364
|
+
devicesExclude;
|
|
13365
|
+
constructor(delivery, devicesExclude) {
|
|
13366
|
+
super(`rule refused: devicesExclude does not apply to a '${delivery}' rule — a system rule excludes devices with systemEvent.excludeDeviceIds`);
|
|
13367
|
+
this.name = "NcDevicesExcludeRefusal";
|
|
13368
|
+
this.delivery = delivery;
|
|
13369
|
+
this.devicesExclude = devicesExclude;
|
|
13370
|
+
}
|
|
13371
|
+
};
|
|
13372
|
+
function assertDevicesExcludeDelivery(rule) {
|
|
13373
|
+
const excluded = rule.conditions.devicesExclude;
|
|
13374
|
+
if (excluded === void 0 || rule.delivery !== NO_DEVICE_SCOPE_DELIVERY) return;
|
|
13375
|
+
throw new NcDevicesExcludeRefusal(rule.delivery, excluded);
|
|
13376
|
+
}
|
|
13377
|
+
/** Run the check; a refusal is dropped work, so it is logged before it is rethrown. */
|
|
13378
|
+
function logRefusal$1(check, ctx) {
|
|
13379
|
+
try {
|
|
13380
|
+
check();
|
|
13381
|
+
} catch (err) {
|
|
13382
|
+
if (err instanceof NcDevicesExcludeRefusal) warnRefusal$1(ctx.logger, err, ctx);
|
|
13383
|
+
throw err;
|
|
13384
|
+
}
|
|
13385
|
+
}
|
|
13386
|
+
function warnRefusal$1(logger, err, ctx) {
|
|
13387
|
+
logger.warn("nc rule refused: devicesExclude on a system rule", { meta: {
|
|
13388
|
+
...ctx.ruleId !== void 0 ? { ruleId: ctx.ruleId } : {},
|
|
13389
|
+
by: ctx.by,
|
|
13390
|
+
delivery: err.delivery,
|
|
13391
|
+
devicesExclude: [...err.devicesExclude]
|
|
13392
|
+
} });
|
|
13393
|
+
}
|
|
13394
|
+
/** The create path of both routes. */
|
|
13395
|
+
function guardRuleDevicesExclude(rule, ctx) {
|
|
13396
|
+
logRefusal$1(() => assertDevicesExcludeDelivery(rule), ctx);
|
|
13397
|
+
}
|
|
13398
|
+
/**
|
|
13399
|
+
* The update path of both routes: the MERGED rule is judged, and only when the
|
|
13400
|
+
* patch changes `delivery` or `conditions`.
|
|
13401
|
+
*/
|
|
13402
|
+
function guardPatchDevicesExclude(existing, patch, ctx) {
|
|
13403
|
+
if (existing === null) return;
|
|
13404
|
+
const merged = mergedFamilySubject(existing, patch);
|
|
13405
|
+
if (merged === null) return;
|
|
13406
|
+
logRefusal$1(() => assertDevicesExcludeDelivery(merged), ctx);
|
|
13407
|
+
}
|
|
13408
|
+
//#endregion
|
|
13409
|
+
//#region src/notification-center/sensor-transition-guard.ts
|
|
13410
|
+
/** The one delivery whose subject carries a sensor edge. */
|
|
13411
|
+
var SENSOR_TRANSITION_DELIVERY = "device-event";
|
|
13412
|
+
var NcSensorTransitionRefusal = class extends Error {
|
|
13413
|
+
delivery;
|
|
13414
|
+
sensorTransition;
|
|
13415
|
+
constructor(delivery, sensorTransition) {
|
|
13416
|
+
super(`rule refused: sensorTransition '${sensorTransition}' applies only to a device-event rule, not '${delivery}'`);
|
|
13417
|
+
this.name = "NcSensorTransitionRefusal";
|
|
13418
|
+
this.delivery = delivery;
|
|
13419
|
+
this.sensorTransition = sensorTransition;
|
|
13420
|
+
}
|
|
13421
|
+
};
|
|
13422
|
+
function assertSensorTransitionDelivery(rule) {
|
|
13423
|
+
const transition = rule.conditions.sensorTransition;
|
|
13424
|
+
if (transition === void 0 || rule.delivery === SENSOR_TRANSITION_DELIVERY) return;
|
|
13425
|
+
throw new NcSensorTransitionRefusal(rule.delivery, transition);
|
|
13426
|
+
}
|
|
13427
|
+
/** Run the check; a refusal is dropped work, so it is logged before it is rethrown. */
|
|
13428
|
+
function logRefusal(check, ctx) {
|
|
13429
|
+
try {
|
|
13430
|
+
check();
|
|
13431
|
+
} catch (err) {
|
|
13432
|
+
if (err instanceof NcSensorTransitionRefusal) warnRefusal(ctx.logger, err, ctx);
|
|
13433
|
+
throw err;
|
|
13434
|
+
}
|
|
13435
|
+
}
|
|
13436
|
+
function warnRefusal(logger, err, ctx) {
|
|
13437
|
+
logger.warn("nc rule refused: sensorTransition off a device-event rule", { meta: {
|
|
13438
|
+
...ctx.ruleId !== void 0 ? { ruleId: ctx.ruleId } : {},
|
|
13439
|
+
by: ctx.by,
|
|
13440
|
+
delivery: err.delivery,
|
|
13441
|
+
sensorTransition: err.sensorTransition
|
|
13442
|
+
} });
|
|
13443
|
+
}
|
|
13444
|
+
/** The create path of both routes. */
|
|
13445
|
+
function guardRuleSensorTransition(rule, ctx) {
|
|
13446
|
+
logRefusal(() => assertSensorTransitionDelivery(rule), ctx);
|
|
13447
|
+
}
|
|
13448
|
+
/**
|
|
13449
|
+
* The update path of both routes: the MERGED rule is judged, and only when the
|
|
13450
|
+
* patch changes `delivery` or `conditions` — an unchanged full-form save
|
|
13451
|
+
* (a rename, a retarget) is not re-judged.
|
|
13452
|
+
*/
|
|
13453
|
+
function guardPatchSensorTransition(existing, patch, ctx) {
|
|
13454
|
+
if (existing === null) return;
|
|
13455
|
+
const merged = mergedFamilySubject(existing, patch);
|
|
13456
|
+
if (merged === null) return;
|
|
13457
|
+
logRefusal(() => assertSensorTransitionDelivery(merged), ctx);
|
|
13458
|
+
}
|
|
13459
|
+
//#endregion
|
|
13153
13460
|
//#region src/notification-center/occupancy-watcher.ts
|
|
13154
13461
|
/** Sentinel key segments for the "no zone" (whole-frame) and "no class" scopes. */
|
|
13155
13462
|
var FRAME_SCOPE = "@frame";
|
|
@@ -13918,6 +14225,19 @@ function labelFor(conditions) {
|
|
|
13918
14225
|
return conditions.identities?.[0] ?? conditions.labelEquals?.[0] ?? conditions.plates?.values?.[0] ?? void 0;
|
|
13919
14226
|
}
|
|
13920
14227
|
/**
|
|
14228
|
+
* The edge a planned sensor event carries (D640): RAISED by default — the
|
|
14229
|
+
* alert, which is what an operator pressing Test on a flood rule expects to
|
|
14230
|
+
* see — and cleared only for a rule that asks for the clear. Only for a kind
|
|
14231
|
+
* that HAS a raise: a doorbell press carries none in production, and a test
|
|
14232
|
+
* subject claiming one would be a subject no real record could be.
|
|
14233
|
+
*/
|
|
14234
|
+
function sensorEdgeFor(trigger, c) {
|
|
14235
|
+
const kind = c.sensorKinds?.[0];
|
|
14236
|
+
if (trigger !== "device-event" || kind === void 0) return {};
|
|
14237
|
+
if (!require_dist.NC_SENSOR_TRANSITION_KINDS.includes(kind.trim().toLowerCase())) return {};
|
|
14238
|
+
return { sensorRaised: c.sensorTransition !== "cleared" };
|
|
14239
|
+
}
|
|
14240
|
+
/**
|
|
13921
14241
|
* Plan the injection that fires this rule.
|
|
13922
14242
|
*
|
|
13923
14243
|
* Every condition the engine reads off the SUBJECT is satisfied here; the two
|
|
@@ -13939,6 +14259,10 @@ function planRuleTest(input) {
|
|
|
13939
14259
|
note: "This rule names no camera and none was supplied, so the event is attributed to no device and carries no picture. Scope the rule to a camera, or pick one, to see the notification it would really send."
|
|
13940
14260
|
});
|
|
13941
14261
|
const deviceId = scopedDeviceId ?? NC_SYSTEM_DEVICE_ID;
|
|
14262
|
+
if (trigger !== "system-event" && (c.devicesExclude ?? []).includes(deviceId)) notes.push({
|
|
14263
|
+
conditionId: "devicesExclude",
|
|
14264
|
+
note: "The camera this test uses is one the rule excludes, so the evaluation will refuse the event — exactly as it refuses every real one there. Pick another camera to see the notification."
|
|
14265
|
+
});
|
|
13942
14266
|
const scheduledAt = scheduleActiveAt(rule.schedule, now);
|
|
13943
14267
|
if (scheduledAt === null) notes.push({
|
|
13944
14268
|
conditionId: "schedule",
|
|
@@ -13988,6 +14312,7 @@ function planRuleTest(input) {
|
|
|
13988
14312
|
} } : {},
|
|
13989
14313
|
...c.sensorKinds?.[0] !== void 0 ? { sensorKind: c.sensorKinds[0] } : {},
|
|
13990
14314
|
...c.eventTypeTokens?.[0] !== void 0 ? { eventType: c.eventTypeTokens[0] } : {},
|
|
14315
|
+
...sensorEdgeFor(trigger, c),
|
|
13991
14316
|
...trigger === "package-event" ? { packagePhase: c.packagePhase === "picked-up" ? "picked-up" : "delivered" } : {},
|
|
13992
14317
|
...c.source !== void 0 && c.source !== "any" ? { source: c.source } : {},
|
|
13993
14318
|
...bbox !== void 0 ? { bbox } : {},
|
|
@@ -14521,6 +14846,10 @@ var NcTestEventInputSchema = require_dist.object({
|
|
|
14521
14846
|
/** DEVICE-EVENT: the raw device event-type token, when the source is an
|
|
14522
14847
|
* event-emitter slice. */
|
|
14523
14848
|
eventType: require_dist.string().optional(),
|
|
14849
|
+
/** DEVICE-EVENT: which edge a boolean sensor moved on — true raised, false
|
|
14850
|
+
* cleared (D640). Absent = the slice could not say, which production
|
|
14851
|
+
* answers for a doorbell press or an unreadable read. */
|
|
14852
|
+
sensorRaised: require_dist.boolean().optional(),
|
|
14524
14853
|
/** PACKAGE-EVENT: which lifecycle phase. */
|
|
14525
14854
|
packagePhase: require_dist._enum(["delivered", "picked-up"]).optional(),
|
|
14526
14855
|
/** Detection provenance. Absent = `pipeline` (what a record with no stamped
|
|
@@ -14739,6 +15068,7 @@ function buildSyntheticEvent(input, recordId, now) {
|
|
|
14739
15068
|
...input.labelConfidence !== void 0 ? { labelConfidence: input.labelConfidence } : {},
|
|
14740
15069
|
...input.sensorKind !== void 0 ? { sensorKind: input.sensorKind } : {},
|
|
14741
15070
|
...input.eventType !== void 0 ? { eventType: input.eventType } : {},
|
|
15071
|
+
...input.sensorRaised !== void 0 ? { sensorRaised: input.sensorRaised } : {},
|
|
14742
15072
|
...input.packagePhase !== void 0 ? { packagePhase: input.packagePhase } : {},
|
|
14743
15073
|
...input.bbox !== void 0 ? { bbox: input.bbox } : {},
|
|
14744
15074
|
...input.crossing !== void 0 ? { crossing: input.crossing } : {},
|
|
@@ -15865,6 +16195,18 @@ function makeNcActionHandlers(deps) {
|
|
|
15865
16195
|
const parsed = require_dist.NcRuleInputSchema.parse(input.rule);
|
|
15866
16196
|
assertAdminOnlyFields(parsed, [], c);
|
|
15867
16197
|
assertRuleHasAddressee(parsed.targets, parsed.targetUsers);
|
|
16198
|
+
guardRuleFamily(parsed, {
|
|
16199
|
+
logger: deps.logger,
|
|
16200
|
+
by: c.userId
|
|
16201
|
+
});
|
|
16202
|
+
guardRuleSensorTransition(parsed, {
|
|
16203
|
+
logger: deps.logger,
|
|
16204
|
+
by: c.userId
|
|
16205
|
+
});
|
|
16206
|
+
guardRuleDevicesExclude(parsed, {
|
|
16207
|
+
logger: deps.logger,
|
|
16208
|
+
by: c.userId
|
|
16209
|
+
});
|
|
15868
16210
|
await assertTargetsOwned(parsed.targets.map((t) => t.targetId), c);
|
|
15869
16211
|
const rule = await deps.ruleStore.create({
|
|
15870
16212
|
...parsed,
|
|
@@ -15883,6 +16225,21 @@ function makeNcActionHandlers(deps) {
|
|
|
15883
16225
|
const clear = input.clear ?? [];
|
|
15884
16226
|
assertAdminOnlyFields(patch, clear, c, existing);
|
|
15885
16227
|
assertPatchKeepsAddressee(existing, patch, clear);
|
|
16228
|
+
guardPatchFamily(existing, patch, {
|
|
16229
|
+
logger: deps.logger,
|
|
16230
|
+
by: c.userId,
|
|
16231
|
+
ruleId: input.ruleId
|
|
16232
|
+
});
|
|
16233
|
+
guardPatchSensorTransition(existing, patch, {
|
|
16234
|
+
logger: deps.logger,
|
|
16235
|
+
by: c.userId,
|
|
16236
|
+
ruleId: input.ruleId
|
|
16237
|
+
});
|
|
16238
|
+
guardPatchDevicesExclude(existing, patch, {
|
|
16239
|
+
logger: deps.logger,
|
|
16240
|
+
by: c.userId,
|
|
16241
|
+
ruleId: input.ruleId
|
|
16242
|
+
});
|
|
15886
16243
|
if (patch.targets !== void 0) await assertTargetsOwned(patch.targets.map((t) => t.targetId), c);
|
|
15887
16244
|
const { ownerUserId: _owner, disabledTargetIds: _optOut, ...safe } = patch;
|
|
15888
16245
|
const rule = await deps.ruleStore.update(input.ruleId, safe, clear);
|
|
@@ -19026,6 +19383,19 @@ function pruneRuleConditions(conditions, isDead, notes) {
|
|
|
19026
19383
|
notes.removed("conditions.devices", deviceSubjects(verdict.dead));
|
|
19027
19384
|
}
|
|
19028
19385
|
}
|
|
19386
|
+
const excluded = conditions.devicesExclude;
|
|
19387
|
+
if (excluded !== void 0) {
|
|
19388
|
+
const verdict = judgeList(excluded, isDead);
|
|
19389
|
+
if (!verdict.untouched) {
|
|
19390
|
+
const { devicesExclude: _dead, ...rest } = next;
|
|
19391
|
+
next = verdict.last ? rest : {
|
|
19392
|
+
...rest,
|
|
19393
|
+
devicesExclude: [...verdict.next]
|
|
19394
|
+
};
|
|
19395
|
+
changed = true;
|
|
19396
|
+
notes.removed("conditions.devicesExclude", deviceSubjects(verdict.dead));
|
|
19397
|
+
}
|
|
19398
|
+
}
|
|
19029
19399
|
const systemEvent = conditions.systemEvent;
|
|
19030
19400
|
if (systemEvent?.deviceIds !== void 0) {
|
|
19031
19401
|
const verdict = judgeList(systemEvent.deviceIds, isDead);
|
|
@@ -19206,6 +19576,7 @@ function referencedDeviceIds(snapshot) {
|
|
|
19206
19576
|
for (const rule of snapshot.rules) {
|
|
19207
19577
|
const c = rule.conditions;
|
|
19208
19578
|
add(c.devices);
|
|
19579
|
+
add(c.devicesExclude);
|
|
19209
19580
|
add(c.systemEvent?.deviceIds);
|
|
19210
19581
|
if (c.deviceState !== void 0) ids.add(c.deviceState.deviceId);
|
|
19211
19582
|
if (c.scene?.deviceId !== void 0) ids.add(c.scene.deviceId);
|
|
@@ -22059,6 +22430,50 @@ var NcOutbox = class {
|
|
|
22059
22430
|
return [];
|
|
22060
22431
|
}
|
|
22061
22432
|
}
|
|
22433
|
+
/**
|
|
22434
|
+
* The rows `getRuleLastRuns` reduces — ONE statement: fire time inside
|
|
22435
|
+
* `[sinceMs, ∞)`, newest-first on `idx_nc_outbox_created`, projected to the
|
|
22436
|
+
* four columns the reduction reads (never the `payload` JSON), at most
|
|
22437
|
+
* `limit` rows. `sinceMs: null` = no lower bound (the operator keeps the
|
|
22438
|
+
* outbox forever); `limit` still bounds the read.
|
|
22439
|
+
*
|
|
22440
|
+
* Unlike {@link queryHistory} this THROWS on a store error: its caller
|
|
22441
|
+
* answers "which rules did not run", and an empty page from a failed read
|
|
22442
|
+
* would answer "none of them did" (D393).
|
|
22443
|
+
*/
|
|
22444
|
+
async queryLastRunRows(sinceMs, limit) {
|
|
22445
|
+
const rows = await this.store.query.query({
|
|
22446
|
+
collection: NC_OUTBOX_COLLECTION,
|
|
22447
|
+
filter: {
|
|
22448
|
+
...sinceMs !== null ? { whereBetween: { createdAt: [sinceMs, Number.MAX_SAFE_INTEGER] } } : {},
|
|
22449
|
+
orderBy: {
|
|
22450
|
+
field: "createdAt",
|
|
22451
|
+
direction: "desc"
|
|
22452
|
+
},
|
|
22453
|
+
limit
|
|
22454
|
+
},
|
|
22455
|
+
columns: [...NC_LAST_RUN_COLUMNS]
|
|
22456
|
+
});
|
|
22457
|
+
const out = [];
|
|
22458
|
+
let malformed = 0;
|
|
22459
|
+
let oldestReadAt = null;
|
|
22460
|
+
for (const row of rows) {
|
|
22461
|
+
const at = Number(row.data["createdAt"]);
|
|
22462
|
+
if (Number.isFinite(at) && (oldestReadAt === null || at < oldestReadAt)) oldestReadAt = at;
|
|
22463
|
+
const parsed = lastRunRow(row.data);
|
|
22464
|
+
if (parsed === null) malformed += 1;
|
|
22465
|
+
else out.push(parsed);
|
|
22466
|
+
}
|
|
22467
|
+
if (malformed > 0) this.logger.warn("last-run read skipped malformed outbox rows", { meta: {
|
|
22468
|
+
malformed,
|
|
22469
|
+
read: rows.length
|
|
22470
|
+
} });
|
|
22471
|
+
return {
|
|
22472
|
+
rows: out,
|
|
22473
|
+
truncated: rows.length >= limit,
|
|
22474
|
+
oldestReadAt
|
|
22475
|
+
};
|
|
22476
|
+
}
|
|
22062
22477
|
async getWatermark() {
|
|
22063
22478
|
try {
|
|
22064
22479
|
const row = await this.store.get.query({
|
|
@@ -22346,6 +22761,27 @@ function rowToEntry$1(id, data) {
|
|
|
22346
22761
|
payload
|
|
22347
22762
|
};
|
|
22348
22763
|
}
|
|
22764
|
+
/** The columns {@link NcOutbox.queryLastRunRows} projects. */
|
|
22765
|
+
var NC_LAST_RUN_COLUMNS = [
|
|
22766
|
+
"ruleId",
|
|
22767
|
+
"createdAt",
|
|
22768
|
+
"status",
|
|
22769
|
+
"deviceId"
|
|
22770
|
+
];
|
|
22771
|
+
/** A projected row back to a {@link NcLastRunRow} — a guard, never a cast. */
|
|
22772
|
+
function lastRunRow(data) {
|
|
22773
|
+
const ruleId = data["ruleId"];
|
|
22774
|
+
const createdAt = Number(data["createdAt"]);
|
|
22775
|
+
const deviceId = Number(data["deviceId"]);
|
|
22776
|
+
const status = data["status"];
|
|
22777
|
+
if (typeof ruleId !== "string" || !Number.isFinite(createdAt) || !Number.isFinite(deviceId) || status !== "pending" && status !== "sent" && status !== "dead") return null;
|
|
22778
|
+
return {
|
|
22779
|
+
ruleId,
|
|
22780
|
+
createdAt,
|
|
22781
|
+
status,
|
|
22782
|
+
deviceId
|
|
22783
|
+
};
|
|
22784
|
+
}
|
|
22349
22785
|
//#endregion
|
|
22350
22786
|
//#region src/notification-center/rule-actions.ts
|
|
22351
22787
|
var NcRuleActionRunner = class {
|
|
@@ -23037,6 +23473,58 @@ var NcRuleAiStore = class {
|
|
|
23037
23473
|
}
|
|
23038
23474
|
};
|
|
23039
23475
|
//#endregion
|
|
23476
|
+
//#region src/notification-center/rule-last-runs.ts
|
|
23477
|
+
/**
|
|
23478
|
+
* Row bound of the one read. Projected rows are four scalars (~100 B), so the
|
|
23479
|
+
* bound costs ~2 MB at worst. The live outbox volume was NOT measured when
|
|
23480
|
+
* this was chosen. It exists so a runaway rule cannot turn an admin page load
|
|
23481
|
+
* into a table walk — and when it bites, `horizonMs` says so.
|
|
23482
|
+
*/
|
|
23483
|
+
var NC_RULE_LAST_RUNS_SCAN_LIMIT = 2e4;
|
|
23484
|
+
/** Least successful first: a fire that reached some targets and not others
|
|
23485
|
+
* did not "deliver". */
|
|
23486
|
+
var STATUS_SEVERITY = {
|
|
23487
|
+
sent: 0,
|
|
23488
|
+
pending: 1,
|
|
23489
|
+
dead: 2
|
|
23490
|
+
};
|
|
23491
|
+
function worse(a, b) {
|
|
23492
|
+
return STATUS_SEVERITY[b] > STATUS_SEVERITY[a] ? b : a;
|
|
23493
|
+
}
|
|
23494
|
+
/**
|
|
23495
|
+
* Reduce newest-first rows to one run per rule, in one pass.
|
|
23496
|
+
*
|
|
23497
|
+
* Rows sharing the newest `createdAt` of a rule are one fire to several
|
|
23498
|
+
* targets; they merge into the least successful status. `read.rows` MUST be
|
|
23499
|
+
* ordered newest-first — that is what the outbox read asks the store for.
|
|
23500
|
+
*/
|
|
23501
|
+
function summariseRuleLastRuns(read, scan) {
|
|
23502
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
23503
|
+
for (const row of read.rows) {
|
|
23504
|
+
const seen = byRule.get(row.ruleId);
|
|
23505
|
+
if (seen === void 0) byRule.set(row.ruleId, {
|
|
23506
|
+
ruleId: row.ruleId,
|
|
23507
|
+
lastAt: row.createdAt,
|
|
23508
|
+
status: row.status,
|
|
23509
|
+
deviceId: row.deviceId
|
|
23510
|
+
});
|
|
23511
|
+
else if (seen.lastAt === row.createdAt && worse(seen.status, row.status) !== seen.status) byRule.set(row.ruleId, {
|
|
23512
|
+
...seen,
|
|
23513
|
+
status: row.status,
|
|
23514
|
+
deviceId: row.deviceId
|
|
23515
|
+
});
|
|
23516
|
+
}
|
|
23517
|
+
return {
|
|
23518
|
+
runs: [...byRule.values()],
|
|
23519
|
+
horizonMs: horizonOf(read, scan)
|
|
23520
|
+
};
|
|
23521
|
+
}
|
|
23522
|
+
/** How far back the answer really looked — see the module doc. */
|
|
23523
|
+
function horizonOf(read, scan) {
|
|
23524
|
+
if (read.truncated && read.oldestReadAt !== null) return Math.max(0, scan.now - read.oldestReadAt);
|
|
23525
|
+
return scan.windowMs;
|
|
23526
|
+
}
|
|
23527
|
+
//#endregion
|
|
23040
23528
|
//#region src/notification-center/durable-reread.ts
|
|
23041
23529
|
function isRecord$3(value) {
|
|
23042
23530
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -28001,9 +28489,14 @@ function outboxEntryToHistory(texts, entry) {
|
|
|
28001
28489
|
function renderAudioSpec(spec) {
|
|
28002
28490
|
return spec.mode === "label" ? `label:${spec.labels.join("+")}` : `level:${String(spec.dbThreshold)}dBFS|h${String(spec.hitPercent)}|s${String(spec.samplingSeconds)}`;
|
|
28003
28491
|
}
|
|
28004
|
-
/**
|
|
28005
|
-
|
|
28006
|
-
|
|
28492
|
+
/**
|
|
28493
|
+
* The camera scope in one token — `@all` is a real answer, not a missing one,
|
|
28494
|
+
* and `@all-except:<ids>` is a different one (D641): an excluding rule logged
|
|
28495
|
+
* as `@all` would claim the camera it never watches.
|
|
28496
|
+
*/
|
|
28497
|
+
function renderAudioScope(devices, excluded) {
|
|
28498
|
+
if (devices.length === 0) return excluded.length === 0 ? "@all" : `@all-except:${excluded.join("+")}`;
|
|
28499
|
+
return devices.filter((id) => !excluded.includes(id)).join("+");
|
|
28007
28500
|
}
|
|
28008
28501
|
var NotificationCenter = class NotificationCenter {
|
|
28009
28502
|
logger;
|
|
@@ -29745,7 +30238,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29745
30238
|
* scope the evaluation will match on (`matchesDeviceScope`).
|
|
29746
30239
|
*/
|
|
29747
30240
|
beginLinkedStills(request) {
|
|
29748
|
-
const { sensorId, sensorKind, cameraIds, atMs, rowCameraIds } = request;
|
|
30241
|
+
const { sensorId, sensorKind, cameraIds, atMs, rowCameraIds, sensorRaised } = request;
|
|
29749
30242
|
const shelf = this.linkedStills;
|
|
29750
30243
|
if (shelf === null || !this.evaluationActive || cameraIds.length < 2) return void 0;
|
|
29751
30244
|
if (this.deviceMutes.isMuted(sensorId)) return void 0;
|
|
@@ -29755,7 +30248,13 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29755
30248
|
const involved = [sensorId, ...rowShown];
|
|
29756
30249
|
if (!this.rules.listEnabled("device-event").some((rule) => {
|
|
29757
30250
|
if (rule.media.attach === "none") return false;
|
|
29758
|
-
if (!deviceEventRuleMayFireOnKind(rule, sensorKind)) return false;
|
|
30251
|
+
if (!deviceEventRuleMayFireOnKind(rule, sensorKind, sensorRaised)) return false;
|
|
30252
|
+
const excluded = rule.conditions.devicesExclude;
|
|
30253
|
+
if (excluded !== void 0 && deviceScopeExcludes(excluded, {
|
|
30254
|
+
deviceId: sensorId,
|
|
30255
|
+
linkedSensorId: sensorId,
|
|
30256
|
+
rowCameraIds: rowShown
|
|
30257
|
+
})) return false;
|
|
29759
30258
|
const devices = rule.conditions.devices;
|
|
29760
30259
|
return devices === void 0 || devices.length === 0 || devices.some((id) => involved.includes(id));
|
|
29761
30260
|
})) return void 0;
|
|
@@ -29877,6 +30376,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29877
30376
|
if (!(mode === "condition" ? rule.conditions.audio !== void 0 : rule.conditions.audio === void 0 && referencesAudioClass(rule.conditions.classes))) continue;
|
|
29878
30377
|
const devices = rule.conditions.devices;
|
|
29879
30378
|
if (devices !== void 0 && devices.length > 0 && !devices.includes(deviceId)) continue;
|
|
30379
|
+
const excluded = rule.conditions.devicesExclude;
|
|
30380
|
+
if (excluded !== void 0 && deviceScopeExcludes(excluded, { deviceId })) continue;
|
|
29880
30381
|
return shelf.capture(deviceId, atMs, "audio");
|
|
29881
30382
|
}
|
|
29882
30383
|
}
|
|
@@ -29979,6 +30480,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29979
30480
|
if (occ === void 0 || !matchesOccupancy(occ, subject)) continue;
|
|
29980
30481
|
const devices = rule.conditions.devices;
|
|
29981
30482
|
if (devices !== void 0 && devices.length > 0 && !devices.includes(edge.deviceId)) continue;
|
|
30483
|
+
const excluded = rule.conditions.devicesExclude;
|
|
30484
|
+
if (excluded !== void 0 && deviceScopeExcludes(excluded, { deviceId: edge.deviceId })) continue;
|
|
29982
30485
|
return shelf.capture(edge.deviceId, edge.timestamp, "occupancy");
|
|
29983
30486
|
}
|
|
29984
30487
|
}
|
|
@@ -30110,6 +30613,18 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30110
30613
|
getRule: async ({ ruleId }) => ({ rule: this.rules.get(ruleId) }),
|
|
30111
30614
|
createRule: async ({ rule, caller }) => {
|
|
30112
30615
|
assertRuleHasAddressee(rule.targets, rule.targetUsers);
|
|
30616
|
+
guardRuleFamily(rule, {
|
|
30617
|
+
logger: this.logger,
|
|
30618
|
+
by: caller.userId
|
|
30619
|
+
});
|
|
30620
|
+
guardRuleSensorTransition(rule, {
|
|
30621
|
+
logger: this.logger,
|
|
30622
|
+
by: caller.userId
|
|
30623
|
+
});
|
|
30624
|
+
guardRuleDevicesExclude(rule, {
|
|
30625
|
+
logger: this.logger,
|
|
30626
|
+
by: caller.userId
|
|
30627
|
+
});
|
|
30113
30628
|
await this.validateTargetRefs(rule.targets.map((t) => t.targetId));
|
|
30114
30629
|
const created = await this.rules.create(rule, caller.userId);
|
|
30115
30630
|
this.logger.info("notification rule created", { meta: {
|
|
@@ -30121,7 +30636,23 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30121
30636
|
},
|
|
30122
30637
|
updateRule: async ({ ruleId, patch, clear, caller }) => {
|
|
30123
30638
|
if (patch.targets !== void 0) await this.validateTargetRefs(patch.targets.map((t) => t.targetId));
|
|
30124
|
-
|
|
30639
|
+
const existing = this.rules.get(ruleId);
|
|
30640
|
+
assertPatchKeepsAddressee(existing, patch, clear ?? []);
|
|
30641
|
+
guardPatchFamily(existing, patch, {
|
|
30642
|
+
logger: this.logger,
|
|
30643
|
+
by: caller.userId,
|
|
30644
|
+
ruleId
|
|
30645
|
+
});
|
|
30646
|
+
guardPatchSensorTransition(existing, patch, {
|
|
30647
|
+
logger: this.logger,
|
|
30648
|
+
by: caller.userId,
|
|
30649
|
+
ruleId
|
|
30650
|
+
});
|
|
30651
|
+
guardPatchDevicesExclude(existing, patch, {
|
|
30652
|
+
logger: this.logger,
|
|
30653
|
+
by: caller.userId,
|
|
30654
|
+
ruleId
|
|
30655
|
+
});
|
|
30125
30656
|
const { disabledTargetIds: _optOut, ...safePatch } = patch;
|
|
30126
30657
|
const updated = await this.rules.update(ruleId, safePatch, clear ?? []);
|
|
30127
30658
|
this.logger.info("notification rule updated", { meta: {
|
|
@@ -30163,6 +30694,14 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30163
30694
|
}
|
|
30164
30695
|
return { entries: mapped };
|
|
30165
30696
|
},
|
|
30697
|
+
getRuleLastRuns: async () => {
|
|
30698
|
+
const now = this.now();
|
|
30699
|
+
const windowMs = (await this.deps.pruneWindows()).outboxMs;
|
|
30700
|
+
return summariseRuleLastRuns(await this.outbox.queryLastRunRows(windowMs !== null ? now - windowMs : null, NC_RULE_LAST_RUNS_SCAN_LIMIT), {
|
|
30701
|
+
now,
|
|
30702
|
+
windowMs
|
|
30703
|
+
});
|
|
30704
|
+
},
|
|
30166
30705
|
resolveArtifactUrl: async ({ artifactId }) => ({ url: await this.deps.resolveArtifactUrl?.(artifactId) ?? null }),
|
|
30167
30706
|
listDeviceMutes: async () => ({ mutedDeviceIds: this.deviceMutes.listMuted() }),
|
|
30168
30707
|
setDeviceMuted: async ({ deviceId, muted }) => {
|
|
@@ -30494,6 +31033,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30494
31033
|
coverage: this.coverageFor(panel).map((c) => ({
|
|
30495
31034
|
...c,
|
|
30496
31035
|
deviceIds: [...c.deviceIds],
|
|
31036
|
+
allDevicesExcept: [...c.allDevicesExcept],
|
|
30497
31037
|
skippedDevices: c.skippedDevices.map((s) => ({ ...s }))
|
|
30498
31038
|
}))
|
|
30499
31039
|
};
|
|
@@ -31003,6 +31543,9 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31003
31543
|
failed: evaluation.failedCondition,
|
|
31004
31544
|
classes: subject.classNames,
|
|
31005
31545
|
...subject.sensorEdge !== void 0 ? { sensorEdge: subject.sensorEdge } : {},
|
|
31546
|
+
...subject.sensorRaised !== void 0 ? { sensorRaised: subject.sensorRaised } : {},
|
|
31547
|
+
...subject.sourceDeviceId !== void 0 ? { sourceDeviceId: subject.sourceDeviceId } : {},
|
|
31548
|
+
...subject.linkedSensorId !== void 0 ? { linkedSensorId: subject.linkedSensorId } : {},
|
|
31006
31549
|
eventId: subject.recordId,
|
|
31007
31550
|
...subject.trackId !== void 0 ? { trackId: subject.trackId } : {}
|
|
31008
31551
|
}
|
|
@@ -31112,7 +31655,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31112
31655
|
eventId: subject.recordId,
|
|
31113
31656
|
...subject.trackId !== void 0 ? { trackId: subject.trackId } : {},
|
|
31114
31657
|
...subject.confidence !== void 0 ? { confidence: subject.confidence } : {},
|
|
31115
|
-
...subject.zones.length > 0 ? { zones: subject.zones } : {}
|
|
31658
|
+
...subject.zones.length > 0 ? { zones: subject.zones } : {},
|
|
31659
|
+
...rule.conditions.sensorTransition !== void 0 ? {
|
|
31660
|
+
sensorTransition: rule.conditions.sensorTransition,
|
|
31661
|
+
sensorRaised: subject.sensorRaised ?? null
|
|
31662
|
+
} : {}
|
|
31116
31663
|
}
|
|
31117
31664
|
});
|
|
31118
31665
|
const unresolvable = unresolvableButtons(rule.actions);
|
|
@@ -31719,7 +32266,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31719
32266
|
ruleId: rule.id,
|
|
31720
32267
|
ruleName: rule.name,
|
|
31721
32268
|
spec,
|
|
31722
|
-
devices: rule.conditions.devices ?? []
|
|
32269
|
+
devices: rule.conditions.devices ?? [],
|
|
32270
|
+
excluded: rule.conditions.devicesExclude ?? []
|
|
31723
32271
|
});
|
|
31724
32272
|
}
|
|
31725
32273
|
this.audioWatcher.setWatchedSpecs(audioSpecs);
|
|
@@ -31811,10 +32359,12 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31811
32359
|
* for at all"; the per-camera line answers "why is 617 not notifying" — and
|
|
31812
32360
|
* that question is ALWAYS asked per camera, so the line carries
|
|
31813
32361
|
* `tags.deviceId` and a rule scoped to no camera is reported as watching
|
|
31814
|
-
* every one rather than silently omitted
|
|
32362
|
+
* every one rather than silently omitted — every one EXCEPT its
|
|
32363
|
+
* `devicesExclude` cameras (D641), each of which gets its own NOT-watching
|
|
32364
|
+
* line on that camera.
|
|
31815
32365
|
*/
|
|
31816
32366
|
reportAudioWatch(entries) {
|
|
31817
|
-
const rendered = entries.map((e) => `${e.ruleId}=${renderAudioSpec(e.spec)}@${renderAudioScope(e.devices)}`).toSorted().join(",");
|
|
32367
|
+
const rendered = entries.map((e) => `${e.ruleId}=${renderAudioSpec(e.spec)}@${renderAudioScope(e.devices, e.excluded)}`).toSorted().join(",");
|
|
31818
32368
|
if (rendered === this.lastAudioWatchReport) return;
|
|
31819
32369
|
this.lastAudioWatchReport = rendered;
|
|
31820
32370
|
this.logger.info("audio watched specs", { meta: {
|
|
@@ -31828,14 +32378,28 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31828
32378
|
mode: entry.spec.mode,
|
|
31829
32379
|
spec: renderAudioSpec(entry.spec)
|
|
31830
32380
|
};
|
|
32381
|
+
for (const deviceId of entry.excluded) this.logger.info("audio rule NOT watching camera", {
|
|
32382
|
+
tags: { deviceId },
|
|
32383
|
+
meta: {
|
|
32384
|
+
...meta,
|
|
32385
|
+
reason: "devicesExclude"
|
|
32386
|
+
}
|
|
32387
|
+
});
|
|
31831
32388
|
if (entry.devices.length === 0) {
|
|
31832
|
-
this.logger.info("audio rule watching every camera", { meta });
|
|
32389
|
+
if (entry.excluded.length === 0) this.logger.info("audio rule watching every camera", { meta });
|
|
32390
|
+
else this.logger.info("audio rule watching every camera except the excluded", { meta: {
|
|
32391
|
+
...meta,
|
|
32392
|
+
excluded: [...entry.excluded]
|
|
32393
|
+
} });
|
|
31833
32394
|
continue;
|
|
31834
32395
|
}
|
|
31835
|
-
for (const deviceId of entry.devices)
|
|
31836
|
-
|
|
31837
|
-
|
|
31838
|
-
|
|
32396
|
+
for (const deviceId of entry.devices) {
|
|
32397
|
+
if (entry.excluded.includes(deviceId)) continue;
|
|
32398
|
+
this.logger.info("audio rule watching camera", {
|
|
32399
|
+
tags: { deviceId },
|
|
32400
|
+
meta
|
|
32401
|
+
});
|
|
32402
|
+
}
|
|
31839
32403
|
}
|
|
31840
32404
|
}
|
|
31841
32405
|
/** Boot reseed of confirmed occupancy edge-state (durability, constraint 4) —
|
|
@@ -89277,10 +89841,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
89277
89841
|
startLinkedStills(data, timestamp, cameras) {
|
|
89278
89842
|
const sensorKind = require_dist.EVENT_KIND_BY_CAP[data.capName]?.kind;
|
|
89279
89843
|
if (sensorKind === void 0) return void 0;
|
|
89844
|
+
const sensorRaised = require_dist.sensorRaisedOf(sensorKind, sensorSliceRecord(data.slice));
|
|
89280
89845
|
try {
|
|
89281
89846
|
return this.notificationCenter?.beginLinkedStills({
|
|
89282
89847
|
sensorId: data.deviceId,
|
|
89283
89848
|
sensorKind,
|
|
89849
|
+
...sensorRaised !== void 0 ? { sensorRaised } : {},
|
|
89284
89850
|
cameraIds: cameras.mediaCameraIds,
|
|
89285
89851
|
rowCameraIds: cameras.rowCameraIds,
|
|
89286
89852
|
atMs: timestamp
|