@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $t as systemEventFamilyProblems, A as NC_SENSOR_TRANSITION_KINDS, At as isEventOwnerType, B as NcRuleSchema, C as MAX_BIRTH_DECISION_RECORDS, Cn as partialRecord, Ct as encodeVectorBase64, D as NC_ALARM_SYSTEM_EVENT_KINDS, Dn as EventCategory, Dt as failureContributionCapability, E as MediaFileKindEnum, En as unknown, Et as faceGalleryCapability, F as NcAlarmSettingsPatchSchema, Ft as isSystemDelivery, G as NcSnoozeSchema, Gt as readTimelapseGeneratedAt, H as NcRuleUpdateInputSchema, Ht as pipelineAnalyticsCapability, I as NcConditionDescriptorSchema, It as kebabToCamel, J as NcTaxonomySchema, Jt as ruleBelongsToAlarm$1, K as NcSnoozeSuppressedSchema, Kt as resolveLocationMode, L as NcConditionsSchema, Lt as mayWriteToLocation, M as NC_TAXONOMY, Mt as isMediaOwnerType, N as NC_TEMPLATE_VARS, Nt as isScheduleActive, O as NC_CONDITION_CATALOG, P as NcAlarmConfigSchema, Pt as isSourceCap, Q as OpsLogEntrySchema, Qt as subKindsOf, R as NcRuleInputSchema, Rt as notificationRulesCapability, S as MAX_ARCHIVED_DEBUG_NOTES, Sn as object, T as MOTION_CLOSE_AFTER_MS, Tn as string, Tt as evictionPolicyOfLocation, U as NcScheduleSchema, Ut as plateGalleryCapability, V as NcRuleTargetSchema, Vt as pickClusterStepModels, W as NcSnoozeInputSchema, Wt as readDeviceStateFrom, X as NcTemplatePreviewSchema, Xt as sensorRaisedOf, Y as NcTemplatePreviewInputSchema, Yt as sceneMonitorCapability, Z as NcTemplateVarDescriptorSchema, Zt as storageOccupancyCapability, _ as FIRST_LEVEL_MACRO_CLASSES, _n as array, _t as audioModeOf, a as BirthDecisionRecordSchema, an as canonicalHash, at as SCENE_DEFAULT_UNCOVERED_POLICY, b as LabelAttributionSchema, bn as literal, bt as deriveRecordingMode, c as CoverStateSchema, cn as DeviceType, ct as TIMELAPSE_DENSE_FLOOR_SEC, dn as defineCustomActions, dt as TimelapseRuleSchema, en as systemEventFilterApplies, et as RECORDING_EXPORT_MAX_READ_BYTES, f as DETECTION_MACRO_CLASSES, fn as hydrateSchema, ft as TrackSourceSchema, g as EVENT_OWNER_TYPES, gn as _enum, gt as audioMetricsCapability, h as EVENT_KIND_BY_CAP, hn as sleep$1, ht as assertTimelapseCadences, i as BaseDevice, in as errMsg$1, it as SCENE_DEFAULT_ANCHOR_THRESHOLD, jt as isKnownTemplateVar, k as NC_DEFAULT_SNOOZE_MINUTES, kt as isDetectionMacroClass, l as DEFAULT_EVENT_COLOR, ln as createEvent, lt as TimelapseRuleInputSchema, m as DeclaredDevices, mn as nodePin, mt as alarmPanelCapability, n as AlarmArmModeSchema, nn as vectorDimFromBase64, nt as RetrainStatusSchema, o as CLUSTER_MODEL_SCOPED_STEPS, on as BaseAddon, ot as SCENE_DIVERGED, pn as isDeviceScopedCap, pt as addonWidgetsSourceCapability, q as NcSystemEventKindSchema, r as ArchivedDebugNoteSchema, rn as zoneAnalyticsCapability, s as COCO_TO_MACRO, sn as CamProfileSchema, st as SceneMonitorSchema, t as AUDIO_MACRO_LABELS, tn as templateVarsFor, tt as RECORDING_SOURCE_CAMSTACK_ADDON, un as customAction, ut as TimelapseRulePatchSchema, v as FULL_IMAGE_BBOX, vn as boolean, vt as buildEventKindDescriptor, wn as record, wt as evaluateSensorEdge, x as LockStateSchema, xn as number, xt as describeSystemEventFamilyProblem, y as FailureCounters, yn as discriminatedUnion, yt as cosineSimilarity$1, z as NcRulePatchSchema, zt as occupancyScope } from "../dist-C3zTKShG.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -281,6 +281,7 @@ function coverageFor(rules, panelDeviceId, mode, skipProbe) {
|
|
|
281
281
|
const ids = /* @__PURE__ */ new Set();
|
|
282
282
|
let ruleCount = 0;
|
|
283
283
|
let allDevices = false;
|
|
284
|
+
let except = null;
|
|
284
285
|
for (const rule of rules) {
|
|
285
286
|
if (!rule.enabled) continue;
|
|
286
287
|
const gate = rule.conditions.deviceState;
|
|
@@ -288,21 +289,35 @@ function coverageFor(rules, panelDeviceId, mode, skipProbe) {
|
|
|
288
289
|
if (!gate.states.includes(wanted)) continue;
|
|
289
290
|
ruleCount += 1;
|
|
290
291
|
const scope = rule.conditions.devices;
|
|
292
|
+
const excluded = rule.conditions.devicesExclude ?? [];
|
|
291
293
|
if (scope === void 0 || scope.length === 0) {
|
|
292
294
|
allDevices = true;
|
|
295
|
+
except = narrowExcept(except, excluded);
|
|
293
296
|
continue;
|
|
294
297
|
}
|
|
295
|
-
for (const id of scope) ids.add(id);
|
|
298
|
+
for (const id of scope) if (!excluded.includes(id)) ids.add(id);
|
|
296
299
|
}
|
|
297
300
|
const deviceIds = [...ids].toSorted((a, b) => a - b);
|
|
301
|
+
const allDevicesExcept = except === null || except === "none" ? [] : [...except].filter((id) => !ids.has(id)).toSorted((a, b) => a - b);
|
|
298
302
|
return {
|
|
299
303
|
mode,
|
|
300
304
|
ruleCount,
|
|
301
305
|
allDevices,
|
|
306
|
+
allDevicesExcept,
|
|
302
307
|
deviceIds,
|
|
303
308
|
skippedDevices: skippedFor(deviceIds, skipProbe)
|
|
304
309
|
};
|
|
305
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* Fold one UNSCOPED covering rule into the except-set: with no exclusion it
|
|
313
|
+
* covers everything (`'none'`, final); otherwise only what every unscoped rule
|
|
314
|
+
* excludes stays uncovered — the intersection.
|
|
315
|
+
*/
|
|
316
|
+
function narrowExcept(current, excluded) {
|
|
317
|
+
if (current === "none" || excluded.length === 0) return "none";
|
|
318
|
+
if (current === null) return new Set(excluded);
|
|
319
|
+
return new Set(excluded.filter((id) => current.has(id)));
|
|
320
|
+
}
|
|
306
321
|
function skippedFor(deviceIds, skipProbe) {
|
|
307
322
|
if (skipProbe === void 0) return [];
|
|
308
323
|
const out = [];
|
|
@@ -376,6 +391,19 @@ function buildArmAnnouncement(coverage, deviceName, bypassed = []) {
|
|
|
376
391
|
textCount: names.length
|
|
377
392
|
};
|
|
378
393
|
}
|
|
394
|
+
if (coverage.allDevices && coverage.allDevicesExcept.length > 0) {
|
|
395
|
+
const names = coverage.allDevicesExcept.map(deviceName);
|
|
396
|
+
return {
|
|
397
|
+
bodyKey: "alarm.arm.body.allDevicesExcept",
|
|
398
|
+
textParams: {
|
|
399
|
+
rules,
|
|
400
|
+
mode,
|
|
401
|
+
count: `${names.length}`,
|
|
402
|
+
names: names.join(", ")
|
|
403
|
+
},
|
|
404
|
+
textCount: names.length
|
|
405
|
+
};
|
|
406
|
+
}
|
|
379
407
|
if (coverage.allDevices) return {
|
|
380
408
|
bodyKey: "alarm.arm.body.allDevices",
|
|
381
409
|
textParams: {
|
|
@@ -594,7 +622,8 @@ function alarmCoveredDevices(rules, panelDeviceId, mode) {
|
|
|
594
622
|
if (!gate.states.includes(wanted)) continue;
|
|
595
623
|
const scope = rule.conditions.devices;
|
|
596
624
|
if (scope === void 0 || scope.length === 0) continue;
|
|
597
|
-
|
|
625
|
+
const excluded = rule.conditions.devicesExclude ?? [];
|
|
626
|
+
for (const id of scope) if (!excluded.includes(id)) ids.add(id);
|
|
598
627
|
}
|
|
599
628
|
return [...ids].toSorted((a, b) => a - b);
|
|
600
629
|
}
|
|
@@ -7788,10 +7817,10 @@ function replaceConditionReferences(monitor, condition, reference) {
|
|
|
7788
7817
|
//#endregion
|
|
7789
7818
|
//#region src/notification-center/snooze-exempt.ts
|
|
7790
7819
|
/** The cap whose presence in a sequence makes the rule the alarm's trigger. */
|
|
7791
|
-
var ALARM_PANEL_CAP$
|
|
7820
|
+
var ALARM_PANEL_CAP$1 = "alarm-panel";
|
|
7792
7821
|
var ALARM_KINDS = new Set(NC_ALARM_SYSTEM_EVENT_KINDS);
|
|
7793
7822
|
function actuatesAlarmPanel(sequences) {
|
|
7794
|
-
for (const sequence of sequences) for (const step of sequence.actions) if (step.kind === "cap" && step.cap === ALARM_PANEL_CAP$
|
|
7823
|
+
for (const sequence of sequences) for (const step of sequence.actions) if (step.kind === "cap" && step.cap === ALARM_PANEL_CAP$1) return true;
|
|
7795
7824
|
return false;
|
|
7796
7825
|
}
|
|
7797
7826
|
/**
|
|
@@ -9556,6 +9585,7 @@ function isLinkedSetSubject(subject) {
|
|
|
9556
9585
|
function subjectFromSensorEvent(ev, markerTrackId, linked) {
|
|
9557
9586
|
const eventType = readSensorEventType(ev.value);
|
|
9558
9587
|
const sensorEdge = sensorEdgeOf(ev.kind, ev.value);
|
|
9588
|
+
const sensorRaised = sensorRaisedOf(ev.kind, ev.value);
|
|
9559
9589
|
return {
|
|
9560
9590
|
kind: "device-event",
|
|
9561
9591
|
recordId: String(ev.id),
|
|
@@ -9569,6 +9599,7 @@ function subjectFromSensorEvent(ev, markerTrackId, linked) {
|
|
|
9569
9599
|
sensorKind: ev.kind,
|
|
9570
9600
|
...eventType !== void 0 ? { eventType } : {},
|
|
9571
9601
|
...sensorEdge !== void 0 ? { sensorEdge } : {},
|
|
9602
|
+
...sensorRaised !== void 0 ? { sensorRaised } : {},
|
|
9572
9603
|
...linked !== void 0 ? {
|
|
9573
9604
|
linkedSensorId: ev.sourceDeviceId,
|
|
9574
9605
|
linkedCameraIds: [...linked.linkedCameraIds],
|
|
@@ -9796,6 +9827,7 @@ function presentConditionIds(c) {
|
|
|
9796
9827
|
const ids = [];
|
|
9797
9828
|
if (c.systemEvent !== void 0) ids.push("systemEvent");
|
|
9798
9829
|
if (c.devices !== void 0 && c.devices.length > 0) ids.push("devices");
|
|
9830
|
+
if (c.devicesExclude !== void 0 && c.devicesExclude.length > 0) ids.push("devicesExclude");
|
|
9799
9831
|
if (c.source !== void 0 && c.source !== "any") ids.push("source");
|
|
9800
9832
|
if (c.classes !== void 0 && c.classes.length > 0) ids.push("classes");
|
|
9801
9833
|
if (c.classesExclude !== void 0 && c.classesExclude.length > 0) ids.push("classesExclude");
|
|
@@ -9812,6 +9844,7 @@ function presentConditionIds(c) {
|
|
|
9812
9844
|
if (c.plates !== void 0) ids.push("plates");
|
|
9813
9845
|
if (c.sensorKinds !== void 0 && c.sensorKinds.length > 0) ids.push("sensorKinds");
|
|
9814
9846
|
if (c.eventTypeTokens !== void 0 && c.eventTypeTokens.length > 0) ids.push("eventTypeTokens");
|
|
9847
|
+
if (c.sensorTransition !== void 0) ids.push("sensorTransition");
|
|
9815
9848
|
if (c.packagePhase !== void 0 && c.packagePhase !== "both") ids.push("packagePhase");
|
|
9816
9849
|
if (c.customZones !== void 0 && c.customZones.length > 0) ids.push("customZones");
|
|
9817
9850
|
if (c.occupancy !== void 0) ids.push("occupancy");
|
|
@@ -9906,12 +9939,23 @@ function zonesVisitedBy(subject) {
|
|
|
9906
9939
|
* before the subject exists (the linked-camera capture gate). Coarse in one
|
|
9907
9940
|
* direction only: `true` may still fail later; `false` never would have fired.
|
|
9908
9941
|
*/
|
|
9909
|
-
function deviceEventRuleMayFireOnKind(rule, sensorKind) {
|
|
9942
|
+
function deviceEventRuleMayFireOnKind(rule, sensorKind, sensorRaised) {
|
|
9910
9943
|
const c = rule.conditions;
|
|
9911
9944
|
if (c.occupancy !== void 0) return false;
|
|
9945
|
+
if (sensorTransitionRefuses(c.sensorTransition, sensorRaised)) return false;
|
|
9912
9946
|
if (c.sensorKinds === void 0 || c.sensorKinds.length === 0) return true;
|
|
9913
9947
|
return toLowerSet(c.sensorKinds).has(sensorKind.trim().toLowerCase());
|
|
9914
9948
|
}
|
|
9949
|
+
/**
|
|
9950
|
+
* Does a rule's `sensorTransition` refuse this edge? Only a KNOWN edge can be
|
|
9951
|
+
* refused: `sensorRaised === undefined` (a kind with no raise, an unreadable
|
|
9952
|
+
* slice) always passes — a flood rule never goes silent on a bad read (D640).
|
|
9953
|
+
* The ONE predicate {@link evaluateRule} and the capture gate both ask.
|
|
9954
|
+
*/
|
|
9955
|
+
function sensorTransitionRefuses(transition, sensorRaised) {
|
|
9956
|
+
if (transition === void 0 || sensorRaised === void 0) return false;
|
|
9957
|
+
return sensorRaised !== (transition === "raised");
|
|
9958
|
+
}
|
|
9915
9959
|
function toLowerSet(values) {
|
|
9916
9960
|
return new Set(values.map((v) => v.trim().toLowerCase()));
|
|
9917
9961
|
}
|
|
@@ -9948,9 +9992,6 @@ function matchesIdentityScope(subject, scope) {
|
|
|
9948
9992
|
function ruleAwaitsRecognition(rule) {
|
|
9949
9993
|
return rule.conditions.identities !== void 0 || rule.conditions.plates !== void 0;
|
|
9950
9994
|
}
|
|
9951
|
-
var ARMED_WORD_PREFIX = "armed_";
|
|
9952
|
-
var ALARM_PANEL_CAP$1 = "alarm-panel";
|
|
9953
|
-
var ALARM_PANEL_TRIGGER_METHOD = "trigger";
|
|
9954
9995
|
/**
|
|
9955
9996
|
* A rule "belongs to the alarm" when its `deviceState` gate names ANY device's
|
|
9956
9997
|
* `armed_*` word — CamStack's own panel or any other, e.g. an HA-mirrored
|
|
@@ -9968,8 +10009,10 @@ var ALARM_PANEL_TRIGGER_METHOD = "trigger";
|
|
|
9968
10009
|
* catalog on save, so a new key would be dropped by the first edit.
|
|
9969
10010
|
*/
|
|
9970
10011
|
function ruleBelongsToAlarm(rule) {
|
|
9971
|
-
|
|
9972
|
-
|
|
10012
|
+
return ruleBelongsToAlarm$1({
|
|
10013
|
+
conditions: rule.conditions,
|
|
10014
|
+
...rule.actions?.onTrigger !== void 0 ? { onTrigger: rule.actions.onTrigger } : {}
|
|
10015
|
+
});
|
|
9973
10016
|
}
|
|
9974
10017
|
/**
|
|
9975
10018
|
* The edge an alarm rule refuses: a close, or a halt/fault (a cover stopped
|
|
@@ -10145,6 +10188,7 @@ function evaluateRule(rule, subject, deviceState, zoneOwners, sceneStates) {
|
|
|
10145
10188
|
if (!sceneConditionSatisfied(c.scene, observation)) return fail("scene");
|
|
10146
10189
|
}
|
|
10147
10190
|
if (c.devices !== void 0 && c.devices.length > 0 && !matchesDeviceScope(c.devices, subject)) return fail("devices");
|
|
10191
|
+
if (c.devicesExclude !== void 0 && deviceScopeExcludes(c.devicesExclude, subject)) return fail("devicesExclude");
|
|
10148
10192
|
if (c.source !== void 0 && c.source !== "any") {
|
|
10149
10193
|
if ((subject.source ?? "pipeline") !== c.source) return fail("source");
|
|
10150
10194
|
}
|
|
@@ -10221,6 +10265,7 @@ function evaluateRule(rule, subject, deviceState, zoneOwners, sceneStates) {
|
|
|
10221
10265
|
if (c.eventTypeTokens !== void 0 && c.eventTypeTokens.length > 0) {
|
|
10222
10266
|
if (subject.eventType === void 0 || !toLowerSet(c.eventTypeTokens).has(subject.eventType.trim().toLowerCase())) return fail("eventTypeTokens");
|
|
10223
10267
|
}
|
|
10268
|
+
if (sensorTransitionRefuses(c.sensorTransition, subject.sensorRaised)) return fail("sensorTransition");
|
|
10224
10269
|
if (c.packagePhase !== void 0 && c.packagePhase !== "both") {
|
|
10225
10270
|
if (subject.packagePhase !== c.packagePhase) return fail("packagePhase");
|
|
10226
10271
|
}
|
|
@@ -10326,6 +10371,27 @@ function matchesDeviceScope(scope, subject) {
|
|
|
10326
10371
|
if (scope.includes(subject.deviceId)) return true;
|
|
10327
10372
|
return subject.sourceDeviceId !== void 0 && scope.includes(subject.sourceDeviceId);
|
|
10328
10373
|
}
|
|
10374
|
+
/**
|
|
10375
|
+
* Does a rule's `devicesExclude` (D641) exclude this subject?
|
|
10376
|
+
*
|
|
10377
|
+
* The exact complement of {@link matchesDeviceScope}, row by row. A per-camera
|
|
10378
|
+
* row is excluded by its camera OR by the sensor that fired, the two ids an
|
|
10379
|
+
* include matches on. A linked set stands for the rows it replaced (D630 §6):
|
|
10380
|
+
* it is excluded when its SENSOR is, or when EVERY camera that took a row is —
|
|
10381
|
+
* one excluded row camera leaves the others firing, as their own rows would
|
|
10382
|
+
* have. A camera that only LINKS the sensor excludes nothing, for the reason it
|
|
10383
|
+
* includes nothing: ADR 0632 widens the media, never which rules fire.
|
|
10384
|
+
*/
|
|
10385
|
+
function deviceScopeExcludes(excluded, subject) {
|
|
10386
|
+
if (excluded.length === 0) return false;
|
|
10387
|
+
if (subject.linkedSensorId !== void 0) {
|
|
10388
|
+
if (excluded.includes(subject.linkedSensorId)) return true;
|
|
10389
|
+
const rows = subject.rowCameraIds ?? [];
|
|
10390
|
+
return rows.length > 0 && rows.every((cameraId) => excluded.includes(cameraId));
|
|
10391
|
+
}
|
|
10392
|
+
if (excluded.includes(subject.deviceId)) return true;
|
|
10393
|
+
return subject.sourceDeviceId !== void 0 && excluded.includes(subject.sourceDeviceId);
|
|
10394
|
+
}
|
|
10329
10395
|
function cooldownKey(rule, subject) {
|
|
10330
10396
|
const first = subject.classNames[0];
|
|
10331
10397
|
const classKey = keysPerClass(rule, subject) && first !== void 0 ? `:c:${first}` : "";
|
|
@@ -12453,6 +12519,7 @@ var en_default = {
|
|
|
12453
12519
|
"snooze.scope.anyRule": "A rule",
|
|
12454
12520
|
"snooze.scope.rule": "“{{rule}}”",
|
|
12455
12521
|
"alarm.arm.body.allDevices": "Active: all devices",
|
|
12522
|
+
"alarm.arm.body.allDevicesExcept": "Active: all devices except {{count}}: {{names}}",
|
|
12456
12523
|
"alarm.arm.body.noDevices": "Active: no device restriction",
|
|
12457
12524
|
"alarm.arm.body.devices": "Active: {{names}}",
|
|
12458
12525
|
"alarm.arm.body.devicesSkipped": "Active: {{names}}\nExcluded: {{skippedNames}}",
|
|
@@ -12741,6 +12808,7 @@ var it_default = {
|
|
|
12741
12808
|
"snooze.scope.anyRule": "Una regola",
|
|
12742
12809
|
"snooze.scope.rule": "“{{rule}}”",
|
|
12743
12810
|
"alarm.arm.body.allDevices": "Attivi: tutti i dispositivi",
|
|
12811
|
+
"alarm.arm.body.allDevicesExcept": "Attivi: tutti i dispositivi tranne {{count}}: {{names}}",
|
|
12744
12812
|
"alarm.arm.body.noDevices": "Attivi: nessuna restrizione",
|
|
12745
12813
|
"alarm.arm.body.devices": "Attivi: {{names}}",
|
|
12746
12814
|
"alarm.arm.body.devicesSkipped": "Attivi: {{names}}\nEsclusi: {{skippedNames}}",
|
|
@@ -13255,7 +13323,7 @@ function assertPatchKeepsFamily(existing, patch) {
|
|
|
13255
13323
|
* the offending kind or filter). Then rethrow. A refused save is dropped work,
|
|
13256
13324
|
* and dropped work is never silent. Any other error passes through unlogged.
|
|
13257
13325
|
*/
|
|
13258
|
-
function logRefusal(check, ctx) {
|
|
13326
|
+
function logRefusal$2(check, ctx) {
|
|
13259
13327
|
try {
|
|
13260
13328
|
check();
|
|
13261
13329
|
} catch (err) {
|
|
@@ -13271,11 +13339,115 @@ function logRefusal(check, ctx) {
|
|
|
13271
13339
|
}
|
|
13272
13340
|
/** {@link assertRuleFamily}, logged. The create path of both routes. */
|
|
13273
13341
|
function guardRuleFamily(rule, ctx) {
|
|
13274
|
-
logRefusal(() => assertRuleFamily(rule), ctx);
|
|
13342
|
+
logRefusal$2(() => assertRuleFamily(rule), ctx);
|
|
13275
13343
|
}
|
|
13276
13344
|
/** {@link assertPatchKeepsFamily}, logged. The update path of both routes. */
|
|
13277
13345
|
function guardPatchFamily(existing, patch, ctx) {
|
|
13278
|
-
logRefusal(() => assertPatchKeepsFamily(existing, patch), ctx);
|
|
13346
|
+
logRefusal$2(() => assertPatchKeepsFamily(existing, patch), ctx);
|
|
13347
|
+
}
|
|
13348
|
+
//#endregion
|
|
13349
|
+
//#region src/notification-center/device-scope-guard.ts
|
|
13350
|
+
/**
|
|
13351
|
+
* The deliveries whose evaluation reads the device scope — the `devices`
|
|
13352
|
+
* descriptor's `appliesTo`, restated as the one delivery that lacks it.
|
|
13353
|
+
*/
|
|
13354
|
+
var NO_DEVICE_SCOPE_DELIVERY = "system-event";
|
|
13355
|
+
var NcDevicesExcludeRefusal = class extends Error {
|
|
13356
|
+
delivery;
|
|
13357
|
+
devicesExclude;
|
|
13358
|
+
constructor(delivery, devicesExclude) {
|
|
13359
|
+
super(`rule refused: devicesExclude does not apply to a '${delivery}' rule — a system rule excludes devices with systemEvent.excludeDeviceIds`);
|
|
13360
|
+
this.name = "NcDevicesExcludeRefusal";
|
|
13361
|
+
this.delivery = delivery;
|
|
13362
|
+
this.devicesExclude = devicesExclude;
|
|
13363
|
+
}
|
|
13364
|
+
};
|
|
13365
|
+
function assertDevicesExcludeDelivery(rule) {
|
|
13366
|
+
const excluded = rule.conditions.devicesExclude;
|
|
13367
|
+
if (excluded === void 0 || rule.delivery !== NO_DEVICE_SCOPE_DELIVERY) return;
|
|
13368
|
+
throw new NcDevicesExcludeRefusal(rule.delivery, excluded);
|
|
13369
|
+
}
|
|
13370
|
+
/** Run the check; a refusal is dropped work, so it is logged before it is rethrown. */
|
|
13371
|
+
function logRefusal$1(check, ctx) {
|
|
13372
|
+
try {
|
|
13373
|
+
check();
|
|
13374
|
+
} catch (err) {
|
|
13375
|
+
if (err instanceof NcDevicesExcludeRefusal) warnRefusal$1(ctx.logger, err, ctx);
|
|
13376
|
+
throw err;
|
|
13377
|
+
}
|
|
13378
|
+
}
|
|
13379
|
+
function warnRefusal$1(logger, err, ctx) {
|
|
13380
|
+
logger.warn("nc rule refused: devicesExclude on a system rule", { meta: {
|
|
13381
|
+
...ctx.ruleId !== void 0 ? { ruleId: ctx.ruleId } : {},
|
|
13382
|
+
by: ctx.by,
|
|
13383
|
+
delivery: err.delivery,
|
|
13384
|
+
devicesExclude: [...err.devicesExclude]
|
|
13385
|
+
} });
|
|
13386
|
+
}
|
|
13387
|
+
/** The create path of both routes. */
|
|
13388
|
+
function guardRuleDevicesExclude(rule, ctx) {
|
|
13389
|
+
logRefusal$1(() => assertDevicesExcludeDelivery(rule), ctx);
|
|
13390
|
+
}
|
|
13391
|
+
/**
|
|
13392
|
+
* The update path of both routes: the MERGED rule is judged, and only when the
|
|
13393
|
+
* patch changes `delivery` or `conditions`.
|
|
13394
|
+
*/
|
|
13395
|
+
function guardPatchDevicesExclude(existing, patch, ctx) {
|
|
13396
|
+
if (existing === null) return;
|
|
13397
|
+
const merged = mergedFamilySubject(existing, patch);
|
|
13398
|
+
if (merged === null) return;
|
|
13399
|
+
logRefusal$1(() => assertDevicesExcludeDelivery(merged), ctx);
|
|
13400
|
+
}
|
|
13401
|
+
//#endregion
|
|
13402
|
+
//#region src/notification-center/sensor-transition-guard.ts
|
|
13403
|
+
/** The one delivery whose subject carries a sensor edge. */
|
|
13404
|
+
var SENSOR_TRANSITION_DELIVERY = "device-event";
|
|
13405
|
+
var NcSensorTransitionRefusal = class extends Error {
|
|
13406
|
+
delivery;
|
|
13407
|
+
sensorTransition;
|
|
13408
|
+
constructor(delivery, sensorTransition) {
|
|
13409
|
+
super(`rule refused: sensorTransition '${sensorTransition}' applies only to a device-event rule, not '${delivery}'`);
|
|
13410
|
+
this.name = "NcSensorTransitionRefusal";
|
|
13411
|
+
this.delivery = delivery;
|
|
13412
|
+
this.sensorTransition = sensorTransition;
|
|
13413
|
+
}
|
|
13414
|
+
};
|
|
13415
|
+
function assertSensorTransitionDelivery(rule) {
|
|
13416
|
+
const transition = rule.conditions.sensorTransition;
|
|
13417
|
+
if (transition === void 0 || rule.delivery === SENSOR_TRANSITION_DELIVERY) return;
|
|
13418
|
+
throw new NcSensorTransitionRefusal(rule.delivery, transition);
|
|
13419
|
+
}
|
|
13420
|
+
/** Run the check; a refusal is dropped work, so it is logged before it is rethrown. */
|
|
13421
|
+
function logRefusal(check, ctx) {
|
|
13422
|
+
try {
|
|
13423
|
+
check();
|
|
13424
|
+
} catch (err) {
|
|
13425
|
+
if (err instanceof NcSensorTransitionRefusal) warnRefusal(ctx.logger, err, ctx);
|
|
13426
|
+
throw err;
|
|
13427
|
+
}
|
|
13428
|
+
}
|
|
13429
|
+
function warnRefusal(logger, err, ctx) {
|
|
13430
|
+
logger.warn("nc rule refused: sensorTransition off a device-event rule", { meta: {
|
|
13431
|
+
...ctx.ruleId !== void 0 ? { ruleId: ctx.ruleId } : {},
|
|
13432
|
+
by: ctx.by,
|
|
13433
|
+
delivery: err.delivery,
|
|
13434
|
+
sensorTransition: err.sensorTransition
|
|
13435
|
+
} });
|
|
13436
|
+
}
|
|
13437
|
+
/** The create path of both routes. */
|
|
13438
|
+
function guardRuleSensorTransition(rule, ctx) {
|
|
13439
|
+
logRefusal(() => assertSensorTransitionDelivery(rule), ctx);
|
|
13440
|
+
}
|
|
13441
|
+
/**
|
|
13442
|
+
* The update path of both routes: the MERGED rule is judged, and only when the
|
|
13443
|
+
* patch changes `delivery` or `conditions` — an unchanged full-form save
|
|
13444
|
+
* (a rename, a retarget) is not re-judged.
|
|
13445
|
+
*/
|
|
13446
|
+
function guardPatchSensorTransition(existing, patch, ctx) {
|
|
13447
|
+
if (existing === null) return;
|
|
13448
|
+
const merged = mergedFamilySubject(existing, patch);
|
|
13449
|
+
if (merged === null) return;
|
|
13450
|
+
logRefusal(() => assertSensorTransitionDelivery(merged), ctx);
|
|
13279
13451
|
}
|
|
13280
13452
|
//#endregion
|
|
13281
13453
|
//#region src/notification-center/occupancy-watcher.ts
|
|
@@ -14046,6 +14218,19 @@ function labelFor(conditions) {
|
|
|
14046
14218
|
return conditions.identities?.[0] ?? conditions.labelEquals?.[0] ?? conditions.plates?.values?.[0] ?? void 0;
|
|
14047
14219
|
}
|
|
14048
14220
|
/**
|
|
14221
|
+
* The edge a planned sensor event carries (D640): RAISED by default — the
|
|
14222
|
+
* alert, which is what an operator pressing Test on a flood rule expects to
|
|
14223
|
+
* see — and cleared only for a rule that asks for the clear. Only for a kind
|
|
14224
|
+
* that HAS a raise: a doorbell press carries none in production, and a test
|
|
14225
|
+
* subject claiming one would be a subject no real record could be.
|
|
14226
|
+
*/
|
|
14227
|
+
function sensorEdgeFor(trigger, c) {
|
|
14228
|
+
const kind = c.sensorKinds?.[0];
|
|
14229
|
+
if (trigger !== "device-event" || kind === void 0) return {};
|
|
14230
|
+
if (!NC_SENSOR_TRANSITION_KINDS.includes(kind.trim().toLowerCase())) return {};
|
|
14231
|
+
return { sensorRaised: c.sensorTransition !== "cleared" };
|
|
14232
|
+
}
|
|
14233
|
+
/**
|
|
14049
14234
|
* Plan the injection that fires this rule.
|
|
14050
14235
|
*
|
|
14051
14236
|
* Every condition the engine reads off the SUBJECT is satisfied here; the two
|
|
@@ -14067,6 +14252,10 @@ function planRuleTest(input) {
|
|
|
14067
14252
|
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."
|
|
14068
14253
|
});
|
|
14069
14254
|
const deviceId = scopedDeviceId ?? NC_SYSTEM_DEVICE_ID;
|
|
14255
|
+
if (trigger !== "system-event" && (c.devicesExclude ?? []).includes(deviceId)) notes.push({
|
|
14256
|
+
conditionId: "devicesExclude",
|
|
14257
|
+
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."
|
|
14258
|
+
});
|
|
14070
14259
|
const scheduledAt = scheduleActiveAt(rule.schedule, now);
|
|
14071
14260
|
if (scheduledAt === null) notes.push({
|
|
14072
14261
|
conditionId: "schedule",
|
|
@@ -14116,6 +14305,7 @@ function planRuleTest(input) {
|
|
|
14116
14305
|
} } : {},
|
|
14117
14306
|
...c.sensorKinds?.[0] !== void 0 ? { sensorKind: c.sensorKinds[0] } : {},
|
|
14118
14307
|
...c.eventTypeTokens?.[0] !== void 0 ? { eventType: c.eventTypeTokens[0] } : {},
|
|
14308
|
+
...sensorEdgeFor(trigger, c),
|
|
14119
14309
|
...trigger === "package-event" ? { packagePhase: c.packagePhase === "picked-up" ? "picked-up" : "delivered" } : {},
|
|
14120
14310
|
...c.source !== void 0 && c.source !== "any" ? { source: c.source } : {},
|
|
14121
14311
|
...bbox !== void 0 ? { bbox } : {},
|
|
@@ -14634,6 +14824,10 @@ var NcTestEventInputSchema = object({
|
|
|
14634
14824
|
/** DEVICE-EVENT: the raw device event-type token, when the source is an
|
|
14635
14825
|
* event-emitter slice. */
|
|
14636
14826
|
eventType: string().optional(),
|
|
14827
|
+
/** DEVICE-EVENT: which edge a boolean sensor moved on — true raised, false
|
|
14828
|
+
* cleared (D640). Absent = the slice could not say, which production
|
|
14829
|
+
* answers for a doorbell press or an unreadable read. */
|
|
14830
|
+
sensorRaised: boolean().optional(),
|
|
14637
14831
|
/** PACKAGE-EVENT: which lifecycle phase. */
|
|
14638
14832
|
packagePhase: _enum(["delivered", "picked-up"]).optional(),
|
|
14639
14833
|
/** Detection provenance. Absent = `pipeline` (what a record with no stamped
|
|
@@ -14852,6 +15046,7 @@ function buildSyntheticEvent(input, recordId, now) {
|
|
|
14852
15046
|
...input.labelConfidence !== void 0 ? { labelConfidence: input.labelConfidence } : {},
|
|
14853
15047
|
...input.sensorKind !== void 0 ? { sensorKind: input.sensorKind } : {},
|
|
14854
15048
|
...input.eventType !== void 0 ? { eventType: input.eventType } : {},
|
|
15049
|
+
...input.sensorRaised !== void 0 ? { sensorRaised: input.sensorRaised } : {},
|
|
14855
15050
|
...input.packagePhase !== void 0 ? { packagePhase: input.packagePhase } : {},
|
|
14856
15051
|
...input.bbox !== void 0 ? { bbox: input.bbox } : {},
|
|
14857
15052
|
...input.crossing !== void 0 ? { crossing: input.crossing } : {},
|
|
@@ -15982,6 +16177,14 @@ function makeNcActionHandlers(deps) {
|
|
|
15982
16177
|
logger: deps.logger,
|
|
15983
16178
|
by: c.userId
|
|
15984
16179
|
});
|
|
16180
|
+
guardRuleSensorTransition(parsed, {
|
|
16181
|
+
logger: deps.logger,
|
|
16182
|
+
by: c.userId
|
|
16183
|
+
});
|
|
16184
|
+
guardRuleDevicesExclude(parsed, {
|
|
16185
|
+
logger: deps.logger,
|
|
16186
|
+
by: c.userId
|
|
16187
|
+
});
|
|
15985
16188
|
await assertTargetsOwned(parsed.targets.map((t) => t.targetId), c);
|
|
15986
16189
|
const rule = await deps.ruleStore.create({
|
|
15987
16190
|
...parsed,
|
|
@@ -16005,6 +16208,16 @@ function makeNcActionHandlers(deps) {
|
|
|
16005
16208
|
by: c.userId,
|
|
16006
16209
|
ruleId: input.ruleId
|
|
16007
16210
|
});
|
|
16211
|
+
guardPatchSensorTransition(existing, patch, {
|
|
16212
|
+
logger: deps.logger,
|
|
16213
|
+
by: c.userId,
|
|
16214
|
+
ruleId: input.ruleId
|
|
16215
|
+
});
|
|
16216
|
+
guardPatchDevicesExclude(existing, patch, {
|
|
16217
|
+
logger: deps.logger,
|
|
16218
|
+
by: c.userId,
|
|
16219
|
+
ruleId: input.ruleId
|
|
16220
|
+
});
|
|
16008
16221
|
if (patch.targets !== void 0) await assertTargetsOwned(patch.targets.map((t) => t.targetId), c);
|
|
16009
16222
|
const { ownerUserId: _owner, disabledTargetIds: _optOut, ...safe } = patch;
|
|
16010
16223
|
const rule = await deps.ruleStore.update(input.ruleId, safe, clear);
|
|
@@ -19148,6 +19361,19 @@ function pruneRuleConditions(conditions, isDead, notes) {
|
|
|
19148
19361
|
notes.removed("conditions.devices", deviceSubjects(verdict.dead));
|
|
19149
19362
|
}
|
|
19150
19363
|
}
|
|
19364
|
+
const excluded = conditions.devicesExclude;
|
|
19365
|
+
if (excluded !== void 0) {
|
|
19366
|
+
const verdict = judgeList(excluded, isDead);
|
|
19367
|
+
if (!verdict.untouched) {
|
|
19368
|
+
const { devicesExclude: _dead, ...rest } = next;
|
|
19369
|
+
next = verdict.last ? rest : {
|
|
19370
|
+
...rest,
|
|
19371
|
+
devicesExclude: [...verdict.next]
|
|
19372
|
+
};
|
|
19373
|
+
changed = true;
|
|
19374
|
+
notes.removed("conditions.devicesExclude", deviceSubjects(verdict.dead));
|
|
19375
|
+
}
|
|
19376
|
+
}
|
|
19151
19377
|
const systemEvent = conditions.systemEvent;
|
|
19152
19378
|
if (systemEvent?.deviceIds !== void 0) {
|
|
19153
19379
|
const verdict = judgeList(systemEvent.deviceIds, isDead);
|
|
@@ -19328,6 +19554,7 @@ function referencedDeviceIds(snapshot) {
|
|
|
19328
19554
|
for (const rule of snapshot.rules) {
|
|
19329
19555
|
const c = rule.conditions;
|
|
19330
19556
|
add(c.devices);
|
|
19557
|
+
add(c.devicesExclude);
|
|
19331
19558
|
add(c.systemEvent?.deviceIds);
|
|
19332
19559
|
if (c.deviceState !== void 0) ids.add(c.deviceState.deviceId);
|
|
19333
19560
|
if (c.scene?.deviceId !== void 0) ids.add(c.scene.deviceId);
|
|
@@ -22181,6 +22408,50 @@ var NcOutbox = class {
|
|
|
22181
22408
|
return [];
|
|
22182
22409
|
}
|
|
22183
22410
|
}
|
|
22411
|
+
/**
|
|
22412
|
+
* The rows `getRuleLastRuns` reduces — ONE statement: fire time inside
|
|
22413
|
+
* `[sinceMs, ∞)`, newest-first on `idx_nc_outbox_created`, projected to the
|
|
22414
|
+
* four columns the reduction reads (never the `payload` JSON), at most
|
|
22415
|
+
* `limit` rows. `sinceMs: null` = no lower bound (the operator keeps the
|
|
22416
|
+
* outbox forever); `limit` still bounds the read.
|
|
22417
|
+
*
|
|
22418
|
+
* Unlike {@link queryHistory} this THROWS on a store error: its caller
|
|
22419
|
+
* answers "which rules did not run", and an empty page from a failed read
|
|
22420
|
+
* would answer "none of them did" (D393).
|
|
22421
|
+
*/
|
|
22422
|
+
async queryLastRunRows(sinceMs, limit) {
|
|
22423
|
+
const rows = await this.store.query.query({
|
|
22424
|
+
collection: NC_OUTBOX_COLLECTION,
|
|
22425
|
+
filter: {
|
|
22426
|
+
...sinceMs !== null ? { whereBetween: { createdAt: [sinceMs, Number.MAX_SAFE_INTEGER] } } : {},
|
|
22427
|
+
orderBy: {
|
|
22428
|
+
field: "createdAt",
|
|
22429
|
+
direction: "desc"
|
|
22430
|
+
},
|
|
22431
|
+
limit
|
|
22432
|
+
},
|
|
22433
|
+
columns: [...NC_LAST_RUN_COLUMNS]
|
|
22434
|
+
});
|
|
22435
|
+
const out = [];
|
|
22436
|
+
let malformed = 0;
|
|
22437
|
+
let oldestReadAt = null;
|
|
22438
|
+
for (const row of rows) {
|
|
22439
|
+
const at = Number(row.data["createdAt"]);
|
|
22440
|
+
if (Number.isFinite(at) && (oldestReadAt === null || at < oldestReadAt)) oldestReadAt = at;
|
|
22441
|
+
const parsed = lastRunRow(row.data);
|
|
22442
|
+
if (parsed === null) malformed += 1;
|
|
22443
|
+
else out.push(parsed);
|
|
22444
|
+
}
|
|
22445
|
+
if (malformed > 0) this.logger.warn("last-run read skipped malformed outbox rows", { meta: {
|
|
22446
|
+
malformed,
|
|
22447
|
+
read: rows.length
|
|
22448
|
+
} });
|
|
22449
|
+
return {
|
|
22450
|
+
rows: out,
|
|
22451
|
+
truncated: rows.length >= limit,
|
|
22452
|
+
oldestReadAt
|
|
22453
|
+
};
|
|
22454
|
+
}
|
|
22184
22455
|
async getWatermark() {
|
|
22185
22456
|
try {
|
|
22186
22457
|
const row = await this.store.get.query({
|
|
@@ -22468,6 +22739,27 @@ function rowToEntry$1(id, data) {
|
|
|
22468
22739
|
payload
|
|
22469
22740
|
};
|
|
22470
22741
|
}
|
|
22742
|
+
/** The columns {@link NcOutbox.queryLastRunRows} projects. */
|
|
22743
|
+
var NC_LAST_RUN_COLUMNS = [
|
|
22744
|
+
"ruleId",
|
|
22745
|
+
"createdAt",
|
|
22746
|
+
"status",
|
|
22747
|
+
"deviceId"
|
|
22748
|
+
];
|
|
22749
|
+
/** A projected row back to a {@link NcLastRunRow} — a guard, never a cast. */
|
|
22750
|
+
function lastRunRow(data) {
|
|
22751
|
+
const ruleId = data["ruleId"];
|
|
22752
|
+
const createdAt = Number(data["createdAt"]);
|
|
22753
|
+
const deviceId = Number(data["deviceId"]);
|
|
22754
|
+
const status = data["status"];
|
|
22755
|
+
if (typeof ruleId !== "string" || !Number.isFinite(createdAt) || !Number.isFinite(deviceId) || status !== "pending" && status !== "sent" && status !== "dead") return null;
|
|
22756
|
+
return {
|
|
22757
|
+
ruleId,
|
|
22758
|
+
createdAt,
|
|
22759
|
+
status,
|
|
22760
|
+
deviceId
|
|
22761
|
+
};
|
|
22762
|
+
}
|
|
22471
22763
|
//#endregion
|
|
22472
22764
|
//#region src/notification-center/rule-actions.ts
|
|
22473
22765
|
var NcRuleActionRunner = class {
|
|
@@ -23159,6 +23451,58 @@ var NcRuleAiStore = class {
|
|
|
23159
23451
|
}
|
|
23160
23452
|
};
|
|
23161
23453
|
//#endregion
|
|
23454
|
+
//#region src/notification-center/rule-last-runs.ts
|
|
23455
|
+
/**
|
|
23456
|
+
* Row bound of the one read. Projected rows are four scalars (~100 B), so the
|
|
23457
|
+
* bound costs ~2 MB at worst. The live outbox volume was NOT measured when
|
|
23458
|
+
* this was chosen. It exists so a runaway rule cannot turn an admin page load
|
|
23459
|
+
* into a table walk — and when it bites, `horizonMs` says so.
|
|
23460
|
+
*/
|
|
23461
|
+
var NC_RULE_LAST_RUNS_SCAN_LIMIT = 2e4;
|
|
23462
|
+
/** Least successful first: a fire that reached some targets and not others
|
|
23463
|
+
* did not "deliver". */
|
|
23464
|
+
var STATUS_SEVERITY = {
|
|
23465
|
+
sent: 0,
|
|
23466
|
+
pending: 1,
|
|
23467
|
+
dead: 2
|
|
23468
|
+
};
|
|
23469
|
+
function worse(a, b) {
|
|
23470
|
+
return STATUS_SEVERITY[b] > STATUS_SEVERITY[a] ? b : a;
|
|
23471
|
+
}
|
|
23472
|
+
/**
|
|
23473
|
+
* Reduce newest-first rows to one run per rule, in one pass.
|
|
23474
|
+
*
|
|
23475
|
+
* Rows sharing the newest `createdAt` of a rule are one fire to several
|
|
23476
|
+
* targets; they merge into the least successful status. `read.rows` MUST be
|
|
23477
|
+
* ordered newest-first — that is what the outbox read asks the store for.
|
|
23478
|
+
*/
|
|
23479
|
+
function summariseRuleLastRuns(read, scan) {
|
|
23480
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
23481
|
+
for (const row of read.rows) {
|
|
23482
|
+
const seen = byRule.get(row.ruleId);
|
|
23483
|
+
if (seen === void 0) byRule.set(row.ruleId, {
|
|
23484
|
+
ruleId: row.ruleId,
|
|
23485
|
+
lastAt: row.createdAt,
|
|
23486
|
+
status: row.status,
|
|
23487
|
+
deviceId: row.deviceId
|
|
23488
|
+
});
|
|
23489
|
+
else if (seen.lastAt === row.createdAt && worse(seen.status, row.status) !== seen.status) byRule.set(row.ruleId, {
|
|
23490
|
+
...seen,
|
|
23491
|
+
status: row.status,
|
|
23492
|
+
deviceId: row.deviceId
|
|
23493
|
+
});
|
|
23494
|
+
}
|
|
23495
|
+
return {
|
|
23496
|
+
runs: [...byRule.values()],
|
|
23497
|
+
horizonMs: horizonOf(read, scan)
|
|
23498
|
+
};
|
|
23499
|
+
}
|
|
23500
|
+
/** How far back the answer really looked — see the module doc. */
|
|
23501
|
+
function horizonOf(read, scan) {
|
|
23502
|
+
if (read.truncated && read.oldestReadAt !== null) return Math.max(0, scan.now - read.oldestReadAt);
|
|
23503
|
+
return scan.windowMs;
|
|
23504
|
+
}
|
|
23505
|
+
//#endregion
|
|
23162
23506
|
//#region src/notification-center/durable-reread.ts
|
|
23163
23507
|
function isRecord$3(value) {
|
|
23164
23508
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -28123,9 +28467,14 @@ function outboxEntryToHistory(texts, entry) {
|
|
|
28123
28467
|
function renderAudioSpec(spec) {
|
|
28124
28468
|
return spec.mode === "label" ? `label:${spec.labels.join("+")}` : `level:${String(spec.dbThreshold)}dBFS|h${String(spec.hitPercent)}|s${String(spec.samplingSeconds)}`;
|
|
28125
28469
|
}
|
|
28126
|
-
/**
|
|
28127
|
-
|
|
28128
|
-
|
|
28470
|
+
/**
|
|
28471
|
+
* The camera scope in one token — `@all` is a real answer, not a missing one,
|
|
28472
|
+
* and `@all-except:<ids>` is a different one (D641): an excluding rule logged
|
|
28473
|
+
* as `@all` would claim the camera it never watches.
|
|
28474
|
+
*/
|
|
28475
|
+
function renderAudioScope(devices, excluded) {
|
|
28476
|
+
if (devices.length === 0) return excluded.length === 0 ? "@all" : `@all-except:${excluded.join("+")}`;
|
|
28477
|
+
return devices.filter((id) => !excluded.includes(id)).join("+");
|
|
28129
28478
|
}
|
|
28130
28479
|
var NotificationCenter = class NotificationCenter {
|
|
28131
28480
|
logger;
|
|
@@ -29867,7 +30216,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29867
30216
|
* scope the evaluation will match on (`matchesDeviceScope`).
|
|
29868
30217
|
*/
|
|
29869
30218
|
beginLinkedStills(request) {
|
|
29870
|
-
const { sensorId, sensorKind, cameraIds, atMs, rowCameraIds } = request;
|
|
30219
|
+
const { sensorId, sensorKind, cameraIds, atMs, rowCameraIds, sensorRaised } = request;
|
|
29871
30220
|
const shelf = this.linkedStills;
|
|
29872
30221
|
if (shelf === null || !this.evaluationActive || cameraIds.length < 2) return void 0;
|
|
29873
30222
|
if (this.deviceMutes.isMuted(sensorId)) return void 0;
|
|
@@ -29877,7 +30226,13 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29877
30226
|
const involved = [sensorId, ...rowShown];
|
|
29878
30227
|
if (!this.rules.listEnabled("device-event").some((rule) => {
|
|
29879
30228
|
if (rule.media.attach === "none") return false;
|
|
29880
|
-
if (!deviceEventRuleMayFireOnKind(rule, sensorKind)) return false;
|
|
30229
|
+
if (!deviceEventRuleMayFireOnKind(rule, sensorKind, sensorRaised)) return false;
|
|
30230
|
+
const excluded = rule.conditions.devicesExclude;
|
|
30231
|
+
if (excluded !== void 0 && deviceScopeExcludes(excluded, {
|
|
30232
|
+
deviceId: sensorId,
|
|
30233
|
+
linkedSensorId: sensorId,
|
|
30234
|
+
rowCameraIds: rowShown
|
|
30235
|
+
})) return false;
|
|
29881
30236
|
const devices = rule.conditions.devices;
|
|
29882
30237
|
return devices === void 0 || devices.length === 0 || devices.some((id) => involved.includes(id));
|
|
29883
30238
|
})) return void 0;
|
|
@@ -29999,6 +30354,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29999
30354
|
if (!(mode === "condition" ? rule.conditions.audio !== void 0 : rule.conditions.audio === void 0 && referencesAudioClass(rule.conditions.classes))) continue;
|
|
30000
30355
|
const devices = rule.conditions.devices;
|
|
30001
30356
|
if (devices !== void 0 && devices.length > 0 && !devices.includes(deviceId)) continue;
|
|
30357
|
+
const excluded = rule.conditions.devicesExclude;
|
|
30358
|
+
if (excluded !== void 0 && deviceScopeExcludes(excluded, { deviceId })) continue;
|
|
30002
30359
|
return shelf.capture(deviceId, atMs, "audio");
|
|
30003
30360
|
}
|
|
30004
30361
|
}
|
|
@@ -30101,6 +30458,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30101
30458
|
if (occ === void 0 || !matchesOccupancy(occ, subject)) continue;
|
|
30102
30459
|
const devices = rule.conditions.devices;
|
|
30103
30460
|
if (devices !== void 0 && devices.length > 0 && !devices.includes(edge.deviceId)) continue;
|
|
30461
|
+
const excluded = rule.conditions.devicesExclude;
|
|
30462
|
+
if (excluded !== void 0 && deviceScopeExcludes(excluded, { deviceId: edge.deviceId })) continue;
|
|
30104
30463
|
return shelf.capture(edge.deviceId, edge.timestamp, "occupancy");
|
|
30105
30464
|
}
|
|
30106
30465
|
}
|
|
@@ -30236,6 +30595,14 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30236
30595
|
logger: this.logger,
|
|
30237
30596
|
by: caller.userId
|
|
30238
30597
|
});
|
|
30598
|
+
guardRuleSensorTransition(rule, {
|
|
30599
|
+
logger: this.logger,
|
|
30600
|
+
by: caller.userId
|
|
30601
|
+
});
|
|
30602
|
+
guardRuleDevicesExclude(rule, {
|
|
30603
|
+
logger: this.logger,
|
|
30604
|
+
by: caller.userId
|
|
30605
|
+
});
|
|
30239
30606
|
await this.validateTargetRefs(rule.targets.map((t) => t.targetId));
|
|
30240
30607
|
const created = await this.rules.create(rule, caller.userId);
|
|
30241
30608
|
this.logger.info("notification rule created", { meta: {
|
|
@@ -30254,6 +30621,16 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30254
30621
|
by: caller.userId,
|
|
30255
30622
|
ruleId
|
|
30256
30623
|
});
|
|
30624
|
+
guardPatchSensorTransition(existing, patch, {
|
|
30625
|
+
logger: this.logger,
|
|
30626
|
+
by: caller.userId,
|
|
30627
|
+
ruleId
|
|
30628
|
+
});
|
|
30629
|
+
guardPatchDevicesExclude(existing, patch, {
|
|
30630
|
+
logger: this.logger,
|
|
30631
|
+
by: caller.userId,
|
|
30632
|
+
ruleId
|
|
30633
|
+
});
|
|
30257
30634
|
const { disabledTargetIds: _optOut, ...safePatch } = patch;
|
|
30258
30635
|
const updated = await this.rules.update(ruleId, safePatch, clear ?? []);
|
|
30259
30636
|
this.logger.info("notification rule updated", { meta: {
|
|
@@ -30295,6 +30672,14 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30295
30672
|
}
|
|
30296
30673
|
return { entries: mapped };
|
|
30297
30674
|
},
|
|
30675
|
+
getRuleLastRuns: async () => {
|
|
30676
|
+
const now = this.now();
|
|
30677
|
+
const windowMs = (await this.deps.pruneWindows()).outboxMs;
|
|
30678
|
+
return summariseRuleLastRuns(await this.outbox.queryLastRunRows(windowMs !== null ? now - windowMs : null, NC_RULE_LAST_RUNS_SCAN_LIMIT), {
|
|
30679
|
+
now,
|
|
30680
|
+
windowMs
|
|
30681
|
+
});
|
|
30682
|
+
},
|
|
30298
30683
|
resolveArtifactUrl: async ({ artifactId }) => ({ url: await this.deps.resolveArtifactUrl?.(artifactId) ?? null }),
|
|
30299
30684
|
listDeviceMutes: async () => ({ mutedDeviceIds: this.deviceMutes.listMuted() }),
|
|
30300
30685
|
setDeviceMuted: async ({ deviceId, muted }) => {
|
|
@@ -30626,6 +31011,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30626
31011
|
coverage: this.coverageFor(panel).map((c) => ({
|
|
30627
31012
|
...c,
|
|
30628
31013
|
deviceIds: [...c.deviceIds],
|
|
31014
|
+
allDevicesExcept: [...c.allDevicesExcept],
|
|
30629
31015
|
skippedDevices: c.skippedDevices.map((s) => ({ ...s }))
|
|
30630
31016
|
}))
|
|
30631
31017
|
};
|
|
@@ -31135,6 +31521,9 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31135
31521
|
failed: evaluation.failedCondition,
|
|
31136
31522
|
classes: subject.classNames,
|
|
31137
31523
|
...subject.sensorEdge !== void 0 ? { sensorEdge: subject.sensorEdge } : {},
|
|
31524
|
+
...subject.sensorRaised !== void 0 ? { sensorRaised: subject.sensorRaised } : {},
|
|
31525
|
+
...subject.sourceDeviceId !== void 0 ? { sourceDeviceId: subject.sourceDeviceId } : {},
|
|
31526
|
+
...subject.linkedSensorId !== void 0 ? { linkedSensorId: subject.linkedSensorId } : {},
|
|
31138
31527
|
eventId: subject.recordId,
|
|
31139
31528
|
...subject.trackId !== void 0 ? { trackId: subject.trackId } : {}
|
|
31140
31529
|
}
|
|
@@ -31244,7 +31633,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31244
31633
|
eventId: subject.recordId,
|
|
31245
31634
|
...subject.trackId !== void 0 ? { trackId: subject.trackId } : {},
|
|
31246
31635
|
...subject.confidence !== void 0 ? { confidence: subject.confidence } : {},
|
|
31247
|
-
...subject.zones.length > 0 ? { zones: subject.zones } : {}
|
|
31636
|
+
...subject.zones.length > 0 ? { zones: subject.zones } : {},
|
|
31637
|
+
...rule.conditions.sensorTransition !== void 0 ? {
|
|
31638
|
+
sensorTransition: rule.conditions.sensorTransition,
|
|
31639
|
+
sensorRaised: subject.sensorRaised ?? null
|
|
31640
|
+
} : {}
|
|
31248
31641
|
}
|
|
31249
31642
|
});
|
|
31250
31643
|
const unresolvable = unresolvableButtons(rule.actions);
|
|
@@ -31851,7 +32244,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31851
32244
|
ruleId: rule.id,
|
|
31852
32245
|
ruleName: rule.name,
|
|
31853
32246
|
spec,
|
|
31854
|
-
devices: rule.conditions.devices ?? []
|
|
32247
|
+
devices: rule.conditions.devices ?? [],
|
|
32248
|
+
excluded: rule.conditions.devicesExclude ?? []
|
|
31855
32249
|
});
|
|
31856
32250
|
}
|
|
31857
32251
|
this.audioWatcher.setWatchedSpecs(audioSpecs);
|
|
@@ -31943,10 +32337,12 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31943
32337
|
* for at all"; the per-camera line answers "why is 617 not notifying" — and
|
|
31944
32338
|
* that question is ALWAYS asked per camera, so the line carries
|
|
31945
32339
|
* `tags.deviceId` and a rule scoped to no camera is reported as watching
|
|
31946
|
-
* every one rather than silently omitted
|
|
32340
|
+
* every one rather than silently omitted — every one EXCEPT its
|
|
32341
|
+
* `devicesExclude` cameras (D641), each of which gets its own NOT-watching
|
|
32342
|
+
* line on that camera.
|
|
31947
32343
|
*/
|
|
31948
32344
|
reportAudioWatch(entries) {
|
|
31949
|
-
const rendered = entries.map((e) => `${e.ruleId}=${renderAudioSpec(e.spec)}@${renderAudioScope(e.devices)}`).toSorted().join(",");
|
|
32345
|
+
const rendered = entries.map((e) => `${e.ruleId}=${renderAudioSpec(e.spec)}@${renderAudioScope(e.devices, e.excluded)}`).toSorted().join(",");
|
|
31950
32346
|
if (rendered === this.lastAudioWatchReport) return;
|
|
31951
32347
|
this.lastAudioWatchReport = rendered;
|
|
31952
32348
|
this.logger.info("audio watched specs", { meta: {
|
|
@@ -31960,14 +32356,28 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31960
32356
|
mode: entry.spec.mode,
|
|
31961
32357
|
spec: renderAudioSpec(entry.spec)
|
|
31962
32358
|
};
|
|
32359
|
+
for (const deviceId of entry.excluded) this.logger.info("audio rule NOT watching camera", {
|
|
32360
|
+
tags: { deviceId },
|
|
32361
|
+
meta: {
|
|
32362
|
+
...meta,
|
|
32363
|
+
reason: "devicesExclude"
|
|
32364
|
+
}
|
|
32365
|
+
});
|
|
31963
32366
|
if (entry.devices.length === 0) {
|
|
31964
|
-
this.logger.info("audio rule watching every camera", { meta });
|
|
32367
|
+
if (entry.excluded.length === 0) this.logger.info("audio rule watching every camera", { meta });
|
|
32368
|
+
else this.logger.info("audio rule watching every camera except the excluded", { meta: {
|
|
32369
|
+
...meta,
|
|
32370
|
+
excluded: [...entry.excluded]
|
|
32371
|
+
} });
|
|
31965
32372
|
continue;
|
|
31966
32373
|
}
|
|
31967
|
-
for (const deviceId of entry.devices)
|
|
31968
|
-
|
|
31969
|
-
|
|
31970
|
-
|
|
32374
|
+
for (const deviceId of entry.devices) {
|
|
32375
|
+
if (entry.excluded.includes(deviceId)) continue;
|
|
32376
|
+
this.logger.info("audio rule watching camera", {
|
|
32377
|
+
tags: { deviceId },
|
|
32378
|
+
meta
|
|
32379
|
+
});
|
|
32380
|
+
}
|
|
31971
32381
|
}
|
|
31972
32382
|
}
|
|
31973
32383
|
/** Boot reseed of confirmed occupancy edge-state (durability, constraint 4) —
|
|
@@ -89355,10 +89765,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
89355
89765
|
startLinkedStills(data, timestamp, cameras) {
|
|
89356
89766
|
const sensorKind = EVENT_KIND_BY_CAP[data.capName]?.kind;
|
|
89357
89767
|
if (sensorKind === void 0) return void 0;
|
|
89768
|
+
const sensorRaised = sensorRaisedOf(sensorKind, sensorSliceRecord(data.slice));
|
|
89358
89769
|
try {
|
|
89359
89770
|
return this.notificationCenter?.beginLinkedStills({
|
|
89360
89771
|
sensorId: data.deviceId,
|
|
89361
89772
|
sensorKind,
|
|
89773
|
+
...sensorRaised !== void 0 ? { sensorRaised } : {},
|
|
89362
89774
|
cameraIds: cameras.mediaCameraIds,
|
|
89363
89775
|
rowCameraIds: cameras.rowCameraIds,
|
|
89364
89776
|
atMs: timestamp
|