@camstack/addon-post-analysis 1.2.281 → 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-DkERzZfY.mjs → dist-C3zTKShG.mjs} +185 -2
- package/dist/{dist-BtqlSCQT.js → dist-D0zzdvSd.js} +202 -1
- 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-d-u88SNA.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-BA7N1yDM.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-Cq_8hgwY.mjs → hostInit-lUAYfFf-.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +440 -28
- package/dist/pipeline-analytics/index.mjs +440 -28
- 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-OkXjMLHG.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}}",
|
|
@@ -13262,7 +13330,7 @@ function assertPatchKeepsFamily(existing, patch) {
|
|
|
13262
13330
|
* the offending kind or filter). Then rethrow. A refused save is dropped work,
|
|
13263
13331
|
* and dropped work is never silent. Any other error passes through unlogged.
|
|
13264
13332
|
*/
|
|
13265
|
-
function logRefusal(check, ctx) {
|
|
13333
|
+
function logRefusal$2(check, ctx) {
|
|
13266
13334
|
try {
|
|
13267
13335
|
check();
|
|
13268
13336
|
} catch (err) {
|
|
@@ -13278,11 +13346,115 @@ function logRefusal(check, ctx) {
|
|
|
13278
13346
|
}
|
|
13279
13347
|
/** {@link assertRuleFamily}, logged. The create path of both routes. */
|
|
13280
13348
|
function guardRuleFamily(rule, ctx) {
|
|
13281
|
-
logRefusal(() => assertRuleFamily(rule), ctx);
|
|
13349
|
+
logRefusal$2(() => assertRuleFamily(rule), ctx);
|
|
13282
13350
|
}
|
|
13283
13351
|
/** {@link assertPatchKeepsFamily}, logged. The update path of both routes. */
|
|
13284
13352
|
function guardPatchFamily(existing, patch, ctx) {
|
|
13285
|
-
logRefusal(() => assertPatchKeepsFamily(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);
|
|
13286
13458
|
}
|
|
13287
13459
|
//#endregion
|
|
13288
13460
|
//#region src/notification-center/occupancy-watcher.ts
|
|
@@ -14053,6 +14225,19 @@ function labelFor(conditions) {
|
|
|
14053
14225
|
return conditions.identities?.[0] ?? conditions.labelEquals?.[0] ?? conditions.plates?.values?.[0] ?? void 0;
|
|
14054
14226
|
}
|
|
14055
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
|
+
/**
|
|
14056
14241
|
* Plan the injection that fires this rule.
|
|
14057
14242
|
*
|
|
14058
14243
|
* Every condition the engine reads off the SUBJECT is satisfied here; the two
|
|
@@ -14074,6 +14259,10 @@ function planRuleTest(input) {
|
|
|
14074
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."
|
|
14075
14260
|
});
|
|
14076
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
|
+
});
|
|
14077
14266
|
const scheduledAt = scheduleActiveAt(rule.schedule, now);
|
|
14078
14267
|
if (scheduledAt === null) notes.push({
|
|
14079
14268
|
conditionId: "schedule",
|
|
@@ -14123,6 +14312,7 @@ function planRuleTest(input) {
|
|
|
14123
14312
|
} } : {},
|
|
14124
14313
|
...c.sensorKinds?.[0] !== void 0 ? { sensorKind: c.sensorKinds[0] } : {},
|
|
14125
14314
|
...c.eventTypeTokens?.[0] !== void 0 ? { eventType: c.eventTypeTokens[0] } : {},
|
|
14315
|
+
...sensorEdgeFor(trigger, c),
|
|
14126
14316
|
...trigger === "package-event" ? { packagePhase: c.packagePhase === "picked-up" ? "picked-up" : "delivered" } : {},
|
|
14127
14317
|
...c.source !== void 0 && c.source !== "any" ? { source: c.source } : {},
|
|
14128
14318
|
...bbox !== void 0 ? { bbox } : {},
|
|
@@ -14656,6 +14846,10 @@ var NcTestEventInputSchema = require_dist.object({
|
|
|
14656
14846
|
/** DEVICE-EVENT: the raw device event-type token, when the source is an
|
|
14657
14847
|
* event-emitter slice. */
|
|
14658
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(),
|
|
14659
14853
|
/** PACKAGE-EVENT: which lifecycle phase. */
|
|
14660
14854
|
packagePhase: require_dist._enum(["delivered", "picked-up"]).optional(),
|
|
14661
14855
|
/** Detection provenance. Absent = `pipeline` (what a record with no stamped
|
|
@@ -14874,6 +15068,7 @@ function buildSyntheticEvent(input, recordId, now) {
|
|
|
14874
15068
|
...input.labelConfidence !== void 0 ? { labelConfidence: input.labelConfidence } : {},
|
|
14875
15069
|
...input.sensorKind !== void 0 ? { sensorKind: input.sensorKind } : {},
|
|
14876
15070
|
...input.eventType !== void 0 ? { eventType: input.eventType } : {},
|
|
15071
|
+
...input.sensorRaised !== void 0 ? { sensorRaised: input.sensorRaised } : {},
|
|
14877
15072
|
...input.packagePhase !== void 0 ? { packagePhase: input.packagePhase } : {},
|
|
14878
15073
|
...input.bbox !== void 0 ? { bbox: input.bbox } : {},
|
|
14879
15074
|
...input.crossing !== void 0 ? { crossing: input.crossing } : {},
|
|
@@ -16004,6 +16199,14 @@ function makeNcActionHandlers(deps) {
|
|
|
16004
16199
|
logger: deps.logger,
|
|
16005
16200
|
by: c.userId
|
|
16006
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
|
+
});
|
|
16007
16210
|
await assertTargetsOwned(parsed.targets.map((t) => t.targetId), c);
|
|
16008
16211
|
const rule = await deps.ruleStore.create({
|
|
16009
16212
|
...parsed,
|
|
@@ -16027,6 +16230,16 @@ function makeNcActionHandlers(deps) {
|
|
|
16027
16230
|
by: c.userId,
|
|
16028
16231
|
ruleId: input.ruleId
|
|
16029
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
|
+
});
|
|
16030
16243
|
if (patch.targets !== void 0) await assertTargetsOwned(patch.targets.map((t) => t.targetId), c);
|
|
16031
16244
|
const { ownerUserId: _owner, disabledTargetIds: _optOut, ...safe } = patch;
|
|
16032
16245
|
const rule = await deps.ruleStore.update(input.ruleId, safe, clear);
|
|
@@ -19170,6 +19383,19 @@ function pruneRuleConditions(conditions, isDead, notes) {
|
|
|
19170
19383
|
notes.removed("conditions.devices", deviceSubjects(verdict.dead));
|
|
19171
19384
|
}
|
|
19172
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
|
+
}
|
|
19173
19399
|
const systemEvent = conditions.systemEvent;
|
|
19174
19400
|
if (systemEvent?.deviceIds !== void 0) {
|
|
19175
19401
|
const verdict = judgeList(systemEvent.deviceIds, isDead);
|
|
@@ -19350,6 +19576,7 @@ function referencedDeviceIds(snapshot) {
|
|
|
19350
19576
|
for (const rule of snapshot.rules) {
|
|
19351
19577
|
const c = rule.conditions;
|
|
19352
19578
|
add(c.devices);
|
|
19579
|
+
add(c.devicesExclude);
|
|
19353
19580
|
add(c.systemEvent?.deviceIds);
|
|
19354
19581
|
if (c.deviceState !== void 0) ids.add(c.deviceState.deviceId);
|
|
19355
19582
|
if (c.scene?.deviceId !== void 0) ids.add(c.scene.deviceId);
|
|
@@ -22203,6 +22430,50 @@ var NcOutbox = class {
|
|
|
22203
22430
|
return [];
|
|
22204
22431
|
}
|
|
22205
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
|
+
}
|
|
22206
22477
|
async getWatermark() {
|
|
22207
22478
|
try {
|
|
22208
22479
|
const row = await this.store.get.query({
|
|
@@ -22490,6 +22761,27 @@ function rowToEntry$1(id, data) {
|
|
|
22490
22761
|
payload
|
|
22491
22762
|
};
|
|
22492
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
|
+
}
|
|
22493
22785
|
//#endregion
|
|
22494
22786
|
//#region src/notification-center/rule-actions.ts
|
|
22495
22787
|
var NcRuleActionRunner = class {
|
|
@@ -23181,6 +23473,58 @@ var NcRuleAiStore = class {
|
|
|
23181
23473
|
}
|
|
23182
23474
|
};
|
|
23183
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
|
|
23184
23528
|
//#region src/notification-center/durable-reread.ts
|
|
23185
23529
|
function isRecord$3(value) {
|
|
23186
23530
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -28145,9 +28489,14 @@ function outboxEntryToHistory(texts, entry) {
|
|
|
28145
28489
|
function renderAudioSpec(spec) {
|
|
28146
28490
|
return spec.mode === "label" ? `label:${spec.labels.join("+")}` : `level:${String(spec.dbThreshold)}dBFS|h${String(spec.hitPercent)}|s${String(spec.samplingSeconds)}`;
|
|
28147
28491
|
}
|
|
28148
|
-
/**
|
|
28149
|
-
|
|
28150
|
-
|
|
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("+");
|
|
28151
28500
|
}
|
|
28152
28501
|
var NotificationCenter = class NotificationCenter {
|
|
28153
28502
|
logger;
|
|
@@ -29889,7 +30238,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29889
30238
|
* scope the evaluation will match on (`matchesDeviceScope`).
|
|
29890
30239
|
*/
|
|
29891
30240
|
beginLinkedStills(request) {
|
|
29892
|
-
const { sensorId, sensorKind, cameraIds, atMs, rowCameraIds } = request;
|
|
30241
|
+
const { sensorId, sensorKind, cameraIds, atMs, rowCameraIds, sensorRaised } = request;
|
|
29893
30242
|
const shelf = this.linkedStills;
|
|
29894
30243
|
if (shelf === null || !this.evaluationActive || cameraIds.length < 2) return void 0;
|
|
29895
30244
|
if (this.deviceMutes.isMuted(sensorId)) return void 0;
|
|
@@ -29899,7 +30248,13 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29899
30248
|
const involved = [sensorId, ...rowShown];
|
|
29900
30249
|
if (!this.rules.listEnabled("device-event").some((rule) => {
|
|
29901
30250
|
if (rule.media.attach === "none") return false;
|
|
29902
|
-
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;
|
|
29903
30258
|
const devices = rule.conditions.devices;
|
|
29904
30259
|
return devices === void 0 || devices.length === 0 || devices.some((id) => involved.includes(id));
|
|
29905
30260
|
})) return void 0;
|
|
@@ -30021,6 +30376,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30021
30376
|
if (!(mode === "condition" ? rule.conditions.audio !== void 0 : rule.conditions.audio === void 0 && referencesAudioClass(rule.conditions.classes))) continue;
|
|
30022
30377
|
const devices = rule.conditions.devices;
|
|
30023
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;
|
|
30024
30381
|
return shelf.capture(deviceId, atMs, "audio");
|
|
30025
30382
|
}
|
|
30026
30383
|
}
|
|
@@ -30123,6 +30480,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30123
30480
|
if (occ === void 0 || !matchesOccupancy(occ, subject)) continue;
|
|
30124
30481
|
const devices = rule.conditions.devices;
|
|
30125
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;
|
|
30126
30485
|
return shelf.capture(edge.deviceId, edge.timestamp, "occupancy");
|
|
30127
30486
|
}
|
|
30128
30487
|
}
|
|
@@ -30258,6 +30617,14 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30258
30617
|
logger: this.logger,
|
|
30259
30618
|
by: caller.userId
|
|
30260
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
|
+
});
|
|
30261
30628
|
await this.validateTargetRefs(rule.targets.map((t) => t.targetId));
|
|
30262
30629
|
const created = await this.rules.create(rule, caller.userId);
|
|
30263
30630
|
this.logger.info("notification rule created", { meta: {
|
|
@@ -30276,6 +30643,16 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30276
30643
|
by: caller.userId,
|
|
30277
30644
|
ruleId
|
|
30278
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
|
+
});
|
|
30279
30656
|
const { disabledTargetIds: _optOut, ...safePatch } = patch;
|
|
30280
30657
|
const updated = await this.rules.update(ruleId, safePatch, clear ?? []);
|
|
30281
30658
|
this.logger.info("notification rule updated", { meta: {
|
|
@@ -30317,6 +30694,14 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30317
30694
|
}
|
|
30318
30695
|
return { entries: mapped };
|
|
30319
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
|
+
},
|
|
30320
30705
|
resolveArtifactUrl: async ({ artifactId }) => ({ url: await this.deps.resolveArtifactUrl?.(artifactId) ?? null }),
|
|
30321
30706
|
listDeviceMutes: async () => ({ mutedDeviceIds: this.deviceMutes.listMuted() }),
|
|
30322
30707
|
setDeviceMuted: async ({ deviceId, muted }) => {
|
|
@@ -30648,6 +31033,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30648
31033
|
coverage: this.coverageFor(panel).map((c) => ({
|
|
30649
31034
|
...c,
|
|
30650
31035
|
deviceIds: [...c.deviceIds],
|
|
31036
|
+
allDevicesExcept: [...c.allDevicesExcept],
|
|
30651
31037
|
skippedDevices: c.skippedDevices.map((s) => ({ ...s }))
|
|
30652
31038
|
}))
|
|
30653
31039
|
};
|
|
@@ -31157,6 +31543,9 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31157
31543
|
failed: evaluation.failedCondition,
|
|
31158
31544
|
classes: subject.classNames,
|
|
31159
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 } : {},
|
|
31160
31549
|
eventId: subject.recordId,
|
|
31161
31550
|
...subject.trackId !== void 0 ? { trackId: subject.trackId } : {}
|
|
31162
31551
|
}
|
|
@@ -31266,7 +31655,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31266
31655
|
eventId: subject.recordId,
|
|
31267
31656
|
...subject.trackId !== void 0 ? { trackId: subject.trackId } : {},
|
|
31268
31657
|
...subject.confidence !== void 0 ? { confidence: subject.confidence } : {},
|
|
31269
|
-
...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
|
+
} : {}
|
|
31270
31663
|
}
|
|
31271
31664
|
});
|
|
31272
31665
|
const unresolvable = unresolvableButtons(rule.actions);
|
|
@@ -31873,7 +32266,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31873
32266
|
ruleId: rule.id,
|
|
31874
32267
|
ruleName: rule.name,
|
|
31875
32268
|
spec,
|
|
31876
|
-
devices: rule.conditions.devices ?? []
|
|
32269
|
+
devices: rule.conditions.devices ?? [],
|
|
32270
|
+
excluded: rule.conditions.devicesExclude ?? []
|
|
31877
32271
|
});
|
|
31878
32272
|
}
|
|
31879
32273
|
this.audioWatcher.setWatchedSpecs(audioSpecs);
|
|
@@ -31965,10 +32359,12 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31965
32359
|
* for at all"; the per-camera line answers "why is 617 not notifying" — and
|
|
31966
32360
|
* that question is ALWAYS asked per camera, so the line carries
|
|
31967
32361
|
* `tags.deviceId` and a rule scoped to no camera is reported as watching
|
|
31968
|
-
* 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.
|
|
31969
32365
|
*/
|
|
31970
32366
|
reportAudioWatch(entries) {
|
|
31971
|
-
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(",");
|
|
31972
32368
|
if (rendered === this.lastAudioWatchReport) return;
|
|
31973
32369
|
this.lastAudioWatchReport = rendered;
|
|
31974
32370
|
this.logger.info("audio watched specs", { meta: {
|
|
@@ -31982,14 +32378,28 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31982
32378
|
mode: entry.spec.mode,
|
|
31983
32379
|
spec: renderAudioSpec(entry.spec)
|
|
31984
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
|
+
});
|
|
31985
32388
|
if (entry.devices.length === 0) {
|
|
31986
|
-
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
|
+
} });
|
|
31987
32394
|
continue;
|
|
31988
32395
|
}
|
|
31989
|
-
for (const deviceId of entry.devices)
|
|
31990
|
-
|
|
31991
|
-
|
|
31992
|
-
|
|
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
|
+
}
|
|
31993
32403
|
}
|
|
31994
32404
|
}
|
|
31995
32405
|
/** Boot reseed of confirmed occupancy edge-state (durability, constraint 4) —
|
|
@@ -89431,10 +89841,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
89431
89841
|
startLinkedStills(data, timestamp, cameras) {
|
|
89432
89842
|
const sensorKind = require_dist.EVENT_KIND_BY_CAP[data.capName]?.kind;
|
|
89433
89843
|
if (sensorKind === void 0) return void 0;
|
|
89844
|
+
const sensorRaised = require_dist.sensorRaisedOf(sensorKind, sensorSliceRecord(data.slice));
|
|
89434
89845
|
try {
|
|
89435
89846
|
return this.notificationCenter?.beginLinkedStills({
|
|
89436
89847
|
sensorId: data.deviceId,
|
|
89437
89848
|
sensorKind,
|
|
89849
|
+
...sensorRaised !== void 0 ? { sensorRaised } : {},
|
|
89438
89850
|
cameraIds: cameras.mediaCameraIds,
|
|
89439
89851
|
rowCameraIds: cameras.rowCameraIds,
|
|
89440
89852
|
atMs: timestamp
|