@camstack/addon-post-analysis 1.2.322 → 1.2.324
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{clip-model-registry-BFbP9U7z.js → clip-model-registry-BFLeI0dr.js} +5683 -5032
- package/dist/{clip-model-registry-CwK2SLQD.mjs → clip-model-registry-CshjlVTF.mjs} +5450 -4835
- 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-Dmt5eh93.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BiD7k7AM.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CBG_lEGV.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-Bnb-yu_h.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-DtKO_7UT.mjs → hostInit-D0L1yv82.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +745 -137
- package/dist/pipeline-analytics/index.mjs +745 -137
- 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-mzYK6V9S.mjs +0 -26
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CuOcvcK7.mjs +0 -26
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_clip_model_registry = require("../clip-model-registry-
|
|
5
|
+
const require_clip_model_registry = require("../clip-model-registry-BFLeI0dr.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_clip_model_registry.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -484,26 +484,37 @@ function buildArmAnnouncement(coverage, deviceName, bypassed = []) {
|
|
|
484
484
|
};
|
|
485
485
|
}
|
|
486
486
|
/**
|
|
487
|
-
* "Attivi:
|
|
487
|
+
* "Attivi: Porta retro, Cancello — Bypassati: Finestra veranda."
|
|
488
488
|
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
* count pluralises on what IS armed, like every other arm variant.
|
|
489
|
+
* Both halves are named: the bypassed sensors are why the operator pressed
|
|
490
|
+
* Arm anyway, and the remaining ones are what is still promised. An
|
|
491
|
+
* all-devices mode has no finite list to name — it uses a dedicated key.
|
|
493
492
|
*/
|
|
494
493
|
function armedWithBypass(coverage, bypassed, deviceName) {
|
|
495
494
|
const skipped = new Set(coverage.skippedDevices.map((s) => s.deviceId));
|
|
496
495
|
const excluded = new Set(bypassed);
|
|
497
|
-
const
|
|
496
|
+
const bypassedNames = bypassed.map(deviceName).join(", ");
|
|
497
|
+
if (coverage.allDevices) return {
|
|
498
|
+
bodyKey: "alarm.arm.body.bypassedAll",
|
|
499
|
+
textParams: {
|
|
500
|
+
rules: `${coverage.ruleCount}`,
|
|
501
|
+
mode: coverage.mode,
|
|
502
|
+
bypassedNames
|
|
503
|
+
},
|
|
504
|
+
textCount: coverage.ruleCount
|
|
505
|
+
};
|
|
506
|
+
const armedIds = coverage.deviceIds.filter((id) => !skipped.has(id) && !excluded.has(id));
|
|
507
|
+
const names = armedIds.map(deviceName).join(", ");
|
|
498
508
|
return {
|
|
499
509
|
bodyKey: "alarm.arm.body.bypassed",
|
|
500
510
|
textParams: {
|
|
501
511
|
rules: `${coverage.ruleCount}`,
|
|
502
512
|
mode: coverage.mode,
|
|
503
|
-
devices: `${
|
|
504
|
-
|
|
513
|
+
devices: `${armedIds.length}`,
|
|
514
|
+
names,
|
|
515
|
+
bypassedNames
|
|
505
516
|
},
|
|
506
|
-
textCount:
|
|
517
|
+
textCount: armedIds.length
|
|
507
518
|
};
|
|
508
519
|
}
|
|
509
520
|
/**
|
|
@@ -945,7 +956,7 @@ function grantMeta(grant) {
|
|
|
945
956
|
...common,
|
|
946
957
|
ruleId: grant.ruleId,
|
|
947
958
|
rule: grant.ruleName,
|
|
948
|
-
|
|
959
|
+
sequenceId: grant.sequenceId
|
|
949
960
|
};
|
|
950
961
|
case "snooze": return {
|
|
951
962
|
...common,
|
|
@@ -1098,10 +1109,11 @@ var NcActionPlane = class {
|
|
|
1098
1109
|
* Anything short of fully secure is open: a cover stopped half-way, a lock that
|
|
1099
1110
|
* jammed, a cover still closing when the arm is decided, a switch that is on.
|
|
1100
1111
|
*
|
|
1101
|
-
* PRECEDENCE, when one device carries more than one opening slice:
|
|
1102
|
-
*
|
|
1103
|
-
*
|
|
1104
|
-
*
|
|
1112
|
+
* PRECEDENCE, when one device carries more than one opening slice: any OPEN
|
|
1113
|
+
* reading wins over a secure one (a reed that says open beats a motor that
|
|
1114
|
+
* still says `closed`). Among opens, a controllable cap (`cover` / `lock-control`
|
|
1115
|
+
* / `switch`) wins so secure-on-arm knows what to actuate; among secures the
|
|
1116
|
+
* contact wins as the physical "closed" when the motor has not moved yet.
|
|
1105
1117
|
*/
|
|
1106
1118
|
/** The one state word, per cap, that counts as secure. Everything else is open. */
|
|
1107
1119
|
var COVER_SECURE = "closed";
|
|
@@ -1214,6 +1226,11 @@ function readSwitch(runtime) {
|
|
|
1214
1226
|
}
|
|
1215
1227
|
/** D630's table over one device's runtime slices — see the header for the three answers. */
|
|
1216
1228
|
function alarmOpeningFrom(runtime) {
|
|
1229
|
+
let openControllable = null;
|
|
1230
|
+
let openAny = null;
|
|
1231
|
+
let secureContact = null;
|
|
1232
|
+
let secureControllable = null;
|
|
1233
|
+
let secureAny = null;
|
|
1217
1234
|
let firstUnknown = null;
|
|
1218
1235
|
for (const read of [
|
|
1219
1236
|
readContact(runtime),
|
|
@@ -1221,10 +1238,21 @@ function alarmOpeningFrom(runtime) {
|
|
|
1221
1238
|
readLock(runtime),
|
|
1222
1239
|
readSwitch(runtime)
|
|
1223
1240
|
]) {
|
|
1224
|
-
if (read.kind === "
|
|
1225
|
-
|
|
1241
|
+
if (read.kind === "unknown") {
|
|
1242
|
+
if (firstUnknown === null) firstUnknown = read;
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
if (read.kind !== "reading") continue;
|
|
1246
|
+
if (read.reading === "open") {
|
|
1247
|
+
if (require_clip_model_registry.isAlarmControllableCap(read.cap) && openControllable === null) openControllable = read;
|
|
1248
|
+
if (openAny === null) openAny = read;
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
if (read.cap === "contact" && secureContact === null) secureContact = read;
|
|
1252
|
+
if (require_clip_model_registry.isAlarmControllableCap(read.cap) && secureControllable === null) secureControllable = read;
|
|
1253
|
+
if (secureAny === null) secureAny = read;
|
|
1226
1254
|
}
|
|
1227
|
-
return firstUnknown ?? NOT_OPENING;
|
|
1255
|
+
return openControllable ?? openAny ?? secureContact ?? secureControllable ?? secureAny ?? firstUnknown ?? NOT_OPENING;
|
|
1228
1256
|
}
|
|
1229
1257
|
/**
|
|
1230
1258
|
* The sensor-event kind (`CAP_TO_KIND` in `@camstack/types`) → the cap whose
|
|
@@ -2750,12 +2778,6 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2750
2778
|
error: String(err)
|
|
2751
2779
|
};
|
|
2752
2780
|
}
|
|
2753
|
-
const toSecure = openings.open.filter((d) => require_clip_model_registry.alarmDeviceSecureOnArm({
|
|
2754
|
-
controllable: d.controllable,
|
|
2755
|
-
deviceId: d.deviceId,
|
|
2756
|
-
secureOnArmOff: this.secureOnArmOffFor(mode)
|
|
2757
|
-
}));
|
|
2758
|
-
if (toSecure.length > 0) this.armGuard?.secureOnArm(toSecure);
|
|
2759
2781
|
const verdict = decideArm({
|
|
2760
2782
|
mode,
|
|
2761
2783
|
open: openings.open,
|
|
@@ -2763,6 +2785,14 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2763
2785
|
blocking: this.blockingFor(mode),
|
|
2764
2786
|
...acknowledged !== void 0 ? { acknowledged } : {}
|
|
2765
2787
|
});
|
|
2788
|
+
if (verdict.kind === "arm") {
|
|
2789
|
+
const toSecure = openings.open.filter((d) => require_clip_model_registry.alarmDeviceSecureOnArm({
|
|
2790
|
+
controllable: d.controllable,
|
|
2791
|
+
deviceId: d.deviceId,
|
|
2792
|
+
secureOnArmOff: this.secureOnArmOffFor(mode)
|
|
2793
|
+
}));
|
|
2794
|
+
if (toSecure.length > 0) this.armGuard?.secureOnArm(toSecure);
|
|
2795
|
+
}
|
|
2766
2796
|
return {
|
|
2767
2797
|
kind: "read",
|
|
2768
2798
|
openings,
|
|
@@ -2971,6 +3001,7 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2971
3001
|
status(now = Date.now()) {
|
|
2972
3002
|
return {
|
|
2973
3003
|
state: stateAt(this.machine, now),
|
|
3004
|
+
targetMode: armModeOf(this.machine.target, NC_ALARM_MODES),
|
|
2974
3005
|
availableModes: [...NC_ALARM_MODES],
|
|
2975
3006
|
requiresCode: false,
|
|
2976
3007
|
lastChangedAt: this.machine.changedAt
|
|
@@ -5068,6 +5099,27 @@ function planDenseRanges(input) {
|
|
|
5068
5099
|
}
|
|
5069
5100
|
//#endregion
|
|
5070
5101
|
//#region src/notification-center/text-compose.ts
|
|
5102
|
+
/**
|
|
5103
|
+
* The COMPOSITE variables — phrases assembled from catalog fragments before
|
|
5104
|
+
* the one and only render pass.
|
|
5105
|
+
*
|
|
5106
|
+
* A notification body is not a sentence with slots. "person (John) on Front
|
|
5107
|
+
* door in gate" contains three sub-phrases whose SHAPE differs per language
|
|
5108
|
+
* and whose presence is conditional: the label parentheses, the zone clause,
|
|
5109
|
+
* the plural noun. Expressing that with `{{class}}{{label}}{{zones}}` and
|
|
5110
|
+
* three scalars pushes the grammar into the template, where a translator
|
|
5111
|
+
* cannot fix it. So each becomes ONE variable — `{{subject}}`, `{{inZones}}`,
|
|
5112
|
+
* `{{op}}`, `{{detectionSummary}}` — composed here from its own keys and handed
|
|
5113
|
+
* to {@link renderTemplate} as a finished string.
|
|
5114
|
+
*
|
|
5115
|
+
* **Fragments see the SCALARS only.** Never the composites. That is what makes
|
|
5116
|
+
* the depth exactly one and not "one so far": a fragment key edited to say
|
|
5117
|
+
* `{{inZones}}` renders empty rather than starting a second round, and
|
|
5118
|
+
* `renderTemplate` never re-scans what it inserted, so a composite's own value
|
|
5119
|
+
* is printed verbatim whatever it contains.
|
|
5120
|
+
*
|
|
5121
|
+
* Pure: a catalog in, a string out. No clock, no store, no I/O.
|
|
5122
|
+
*/
|
|
5071
5123
|
/** One normalisation, used on both sides of every class lookup — a pipeline
|
|
5072
5124
|
* that emitted `Person` must find the key written `class.person`. */
|
|
5073
5125
|
function normaliseClass(className) {
|
|
@@ -5076,24 +5128,35 @@ function normaliseClass(className) {
|
|
|
5076
5128
|
/**
|
|
5077
5129
|
* The noun for a class, in the count's plural category.
|
|
5078
5130
|
*
|
|
5079
|
-
*
|
|
5080
|
-
*
|
|
5081
|
-
*
|
|
5082
|
-
* detection.
|
|
5131
|
+
* Lookup order: the NC text catalog (`class.*`, plural-aware) → the shared
|
|
5132
|
+
* {@link eventKindLabel} map (viewer filters / EVENT_TAXONOMY) → `class.other`
|
|
5133
|
+
* with the RAW wire id. A custom model class must still appear rather than
|
|
5134
|
+
* vanish; a missing translation is a smaller defect than a missing detection.
|
|
5083
5135
|
*/
|
|
5084
5136
|
function ncClassNoun(texts, className, count) {
|
|
5085
|
-
const
|
|
5137
|
+
const normalised = normaliseClass(className);
|
|
5138
|
+
const key = `class.${normalised}`;
|
|
5086
5139
|
if (texts.has(key)) return texts.text({
|
|
5087
5140
|
key,
|
|
5088
5141
|
count,
|
|
5089
5142
|
vars: { name: className }
|
|
5090
5143
|
});
|
|
5144
|
+
if (require_clip_model_registry.isEventKindLabelKey(normalised)) return require_clip_model_registry.eventKindLabel(normalised, texts.language);
|
|
5091
5145
|
return texts.text({
|
|
5092
5146
|
key: "class.other",
|
|
5093
5147
|
count,
|
|
5094
5148
|
vars: { name: className }
|
|
5095
5149
|
});
|
|
5096
5150
|
}
|
|
5151
|
+
/**
|
|
5152
|
+
* The parenthetical half of a labelled subject: a recognised person or plate
|
|
5153
|
+
* is printed verbatim; a classification (species, COCO sub, …) goes through
|
|
5154
|
+
* the shared event-kind map so "animal (cat)" becomes "animale (Gatto)".
|
|
5155
|
+
*/
|
|
5156
|
+
function ncSubjectLabelText(texts, label, labelKind) {
|
|
5157
|
+
if (labelKind === "identity" || labelKind === "plate") return label;
|
|
5158
|
+
return require_clip_model_registry.eventKindLabel(label, texts.language);
|
|
5159
|
+
}
|
|
5097
5160
|
/** `person (John)` / `persona (John)` — the thing the notification is about. */
|
|
5098
5161
|
function ncSubjectText(texts, input) {
|
|
5099
5162
|
const classNoun = ncClassNoun(texts, input.className, 1);
|
|
@@ -5105,7 +5168,7 @@ function ncSubjectText(texts, input) {
|
|
|
5105
5168
|
key: subjectLabelKey(input.labelKind),
|
|
5106
5169
|
vars: {
|
|
5107
5170
|
classNoun,
|
|
5108
|
-
label: input.label
|
|
5171
|
+
label: ncSubjectLabelText(texts, input.label, input.labelKind)
|
|
5109
5172
|
}
|
|
5110
5173
|
});
|
|
5111
5174
|
}
|
|
@@ -8675,31 +8738,48 @@ function replaceConditionReferences(monitor, condition, reference) {
|
|
|
8675
8738
|
/** The cap whose presence in a sequence makes the rule the alarm's trigger. */
|
|
8676
8739
|
var ALARM_PANEL_CAP$1 = "alarm-panel";
|
|
8677
8740
|
var ALARM_KINDS = new Set(require_clip_model_registry.NC_ALARM_SYSTEM_EVENT_KINDS);
|
|
8678
|
-
function
|
|
8679
|
-
|
|
8741
|
+
function stepActuatesAlarm(step) {
|
|
8742
|
+
return step.kind === "cap" && step.cap === ALARM_PANEL_CAP$1;
|
|
8743
|
+
}
|
|
8744
|
+
function bindingActuatesAlarm(binding, sequenceActionsOf) {
|
|
8745
|
+
if (binding.kind === "do") return stepActuatesAlarm(binding.action);
|
|
8746
|
+
const steps = sequenceActionsOf?.(binding.sequenceId);
|
|
8747
|
+
if (steps === void 0) return false;
|
|
8748
|
+
return steps.some(stepActuatesAlarm);
|
|
8749
|
+
}
|
|
8750
|
+
function actionsActuateAlarm(rule, sequenceActionsOf) {
|
|
8751
|
+
const actions = rule.actions;
|
|
8752
|
+
if (actions === void 0) return false;
|
|
8753
|
+
const hooks = [
|
|
8754
|
+
actions.onActivation,
|
|
8755
|
+
actions.onDeactivation,
|
|
8756
|
+
actions.onTrigger,
|
|
8757
|
+
actions.onTriggerOff
|
|
8758
|
+
];
|
|
8759
|
+
for (const bindings of hooks) for (const binding of bindings ?? []) if (bindingActuatesAlarm(binding, sequenceActionsOf)) return true;
|
|
8760
|
+
for (const button of actions.buttons ?? []) {
|
|
8761
|
+
const steps = sequenceActionsOf?.(button.sequenceId);
|
|
8762
|
+
if (steps !== void 0 && steps.some(stepActuatesAlarm)) return true;
|
|
8763
|
+
}
|
|
8680
8764
|
return false;
|
|
8681
8765
|
}
|
|
8682
8766
|
/**
|
|
8683
8767
|
* May a snooze window silence this rule?
|
|
8684
8768
|
*
|
|
8685
|
-
* `true` = never. Deliberately answered from the rule alone
|
|
8686
|
-
* on the delivery path, where a fallible
|
|
8687
|
-
* DESTROY the exemption (D49) — and the
|
|
8688
|
-
* silencing an alarm.
|
|
8769
|
+
* `true` = never. Deliberately answered from the rule alone (plus an optional
|
|
8770
|
+
* sequence lookup): the gate reads it on the delivery path, where a fallible
|
|
8771
|
+
* lookup would be a read that can DESTROY the exemption (D49) — and the
|
|
8772
|
+
* failure direction of this one is silencing an alarm. A missing sequence
|
|
8773
|
+
* lookup means sequence-ref bindings cannot prove exemption; Do bindings and
|
|
8774
|
+
* system-event kinds still can.
|
|
8689
8775
|
*/
|
|
8690
|
-
function isSnoozeExempt(rule) {
|
|
8776
|
+
function isSnoozeExempt(rule, sequenceActionsOf) {
|
|
8691
8777
|
const kinds = rule.conditions.systemEvent?.kinds ?? [];
|
|
8692
8778
|
for (const kind of kinds) if (ALARM_KINDS.has(kind)) return true;
|
|
8693
|
-
return
|
|
8779
|
+
return actionsActuateAlarm(rule, sequenceActionsOf);
|
|
8694
8780
|
}
|
|
8695
8781
|
//#endregion
|
|
8696
8782
|
//#region src/notification-center/action-buttons.ts
|
|
8697
|
-
/** Sequence names the rule actually declares. A button may only name one. */
|
|
8698
|
-
function declaredSequences(actions) {
|
|
8699
|
-
const names = /* @__PURE__ */ new Set();
|
|
8700
|
-
for (const sequence of actions.onTrigger ?? []) names.add(sequence.name);
|
|
8701
|
-
return names;
|
|
8702
|
-
}
|
|
8703
8783
|
/**
|
|
8704
8784
|
* Build the notification's buttons.
|
|
8705
8785
|
*
|
|
@@ -8712,18 +8792,18 @@ function buildActionButtons(input) {
|
|
|
8712
8792
|
if (actions === void 0) return [];
|
|
8713
8793
|
const buttons = actions.buttons ?? [];
|
|
8714
8794
|
if (buttons.length === 0) return [];
|
|
8715
|
-
const known =
|
|
8795
|
+
const known = input.knownSequenceIds;
|
|
8716
8796
|
const seen = /* @__PURE__ */ new Set();
|
|
8717
8797
|
const out = [];
|
|
8718
8798
|
for (const button of buttons) {
|
|
8719
|
-
if (!known.has(button.
|
|
8799
|
+
if (!known.has(button.sequenceId)) continue;
|
|
8720
8800
|
if (seen.has(button.id)) continue;
|
|
8721
8801
|
seen.add(button.id);
|
|
8722
8802
|
out.push({
|
|
8723
8803
|
id: button.id,
|
|
8724
8804
|
label: button.label,
|
|
8725
8805
|
url: input.mintUrl({
|
|
8726
|
-
|
|
8806
|
+
sequenceId: button.sequenceId,
|
|
8727
8807
|
actionId: button.id
|
|
8728
8808
|
}),
|
|
8729
8809
|
...button.icon !== void 0 ? { icon: button.icon } : {},
|
|
@@ -8733,18 +8813,17 @@ function buildActionButtons(input) {
|
|
|
8733
8813
|
return out;
|
|
8734
8814
|
}
|
|
8735
8815
|
/**
|
|
8736
|
-
* Which of a rule's buttons name a sequence
|
|
8816
|
+
* Which of a rule's buttons name a sequence that is not known.
|
|
8737
8817
|
*
|
|
8738
8818
|
* Exported so the caller can LOG the drop. A button silently missing from a
|
|
8739
8819
|
* notification is the exact shape of failure this repo keeps paying for — the
|
|
8740
8820
|
* operator authored it, it never appeared, and nothing said why.
|
|
8741
8821
|
*/
|
|
8742
|
-
function unresolvableButtons(actions) {
|
|
8822
|
+
function unresolvableButtons(actions, knownSequenceIds) {
|
|
8743
8823
|
if (actions === void 0) return [];
|
|
8744
8824
|
const buttons = actions.buttons ?? [];
|
|
8745
8825
|
if (buttons.length === 0) return [];
|
|
8746
|
-
|
|
8747
|
-
return buttons.filter((b) => !known.has(b.sequence)).map((b) => `${b.id}→${b.sequence}`);
|
|
8826
|
+
return buttons.filter((b) => !knownSequenceIds.has(b.sequenceId)).map((b) => `${b.id}→${b.sequenceId}`);
|
|
8748
8827
|
}
|
|
8749
8828
|
/** How many snooze choices one notification may carry. The schema caps the
|
|
8750
8829
|
* authored list at 4; this is the same bound applied to a stored rule that
|
|
@@ -8795,7 +8874,7 @@ function snoozeChoices(options) {
|
|
|
8795
8874
|
* attention than the notification did.
|
|
8796
8875
|
*/
|
|
8797
8876
|
function buildSnoozeButtons(input) {
|
|
8798
|
-
if (isSnoozeExempt(input.rule)) return [];
|
|
8877
|
+
if (isSnoozeExempt(input.rule, input.sequenceActionsOf)) return [];
|
|
8799
8878
|
return snoozeChoices(input.rule.snoozeOptions).map((minutes) => {
|
|
8800
8879
|
const actionId = snoozeButtonId(minutes);
|
|
8801
8880
|
return {
|
|
@@ -11084,10 +11163,11 @@ function matchesIdentityScope(subject, scope) {
|
|
|
11084
11163
|
* Derived, never a stored flag: both rule editors prune condition keys to the
|
|
11085
11164
|
* catalog on save, so a new key would be dropped by the first edit.
|
|
11086
11165
|
*/
|
|
11087
|
-
function ruleBelongsToAlarm(rule) {
|
|
11166
|
+
function ruleBelongsToAlarm(rule, sequenceActionsOf) {
|
|
11088
11167
|
return require_clip_model_registry.ruleBelongsToAlarm({
|
|
11089
11168
|
conditions: rule.conditions,
|
|
11090
|
-
...rule.actions
|
|
11169
|
+
...rule.actions !== void 0 ? { actions: rule.actions } : {},
|
|
11170
|
+
...sequenceActionsOf !== void 0 ? { sequenceActionsOf } : {}
|
|
11091
11171
|
});
|
|
11092
11172
|
}
|
|
11093
11173
|
/**
|
|
@@ -12862,7 +12942,7 @@ function incomingFromAlarmTransition(input) {
|
|
|
12862
12942
|
const kind = transition.kind;
|
|
12863
12943
|
const keys = alarmTextKeys(transition, input.body);
|
|
12864
12944
|
const sensor = transition.sensor;
|
|
12865
|
-
const alarmSource = kind === "alarm-triggered" ? source : void 0;
|
|
12945
|
+
const alarmSource = (kind === "alarm-triggered" || kind === "alarm-pending") && source !== void 0 ? source : void 0;
|
|
12866
12946
|
const facts = {
|
|
12867
12947
|
eventId: `alarm:${kind}:${String(transition.at)}${sensor !== void 0 ? `:${String(sensor.deviceId)}` : ""}${transition.refusalId !== void 0 ? `:${transition.refusalId}` : ""}`,
|
|
12868
12948
|
timestamp: transition.at,
|
|
@@ -12975,7 +13055,7 @@ function deriveTankBand(reading) {
|
|
|
12975
13055
|
case null: return null;
|
|
12976
13056
|
}
|
|
12977
13057
|
}
|
|
12978
|
-
function isRecord$
|
|
13058
|
+
function isRecord$9(value) {
|
|
12979
13059
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12980
13060
|
}
|
|
12981
13061
|
function tankStatusOf(value) {
|
|
@@ -12991,11 +13071,11 @@ function vacuumTanksOf(data) {
|
|
|
12991
13071
|
if (data["capName"] !== "vacuum-control") return null;
|
|
12992
13072
|
const deviceId = data["deviceId"];
|
|
12993
13073
|
const slice = data["slice"];
|
|
12994
|
-
if (typeof deviceId !== "number" || !isRecord$
|
|
13074
|
+
if (typeof deviceId !== "number" || !isRecord$9(slice)) return null;
|
|
12995
13075
|
const tanks = [];
|
|
12996
13076
|
for (const key of NC_TANK_KEYS) {
|
|
12997
13077
|
const raw = slice[key];
|
|
12998
|
-
if (!isRecord$
|
|
13078
|
+
if (!isRecord$9(raw)) continue;
|
|
12999
13079
|
const level = raw["level"];
|
|
13000
13080
|
tanks.push({
|
|
13001
13081
|
key,
|
|
@@ -14516,7 +14596,7 @@ var en_default = {
|
|
|
14516
14596
|
"snooze.scope.anyRule": "A rule",
|
|
14517
14597
|
"snooze.scope.rule": "“{{rule}}”",
|
|
14518
14598
|
"alarm.arm.body.allDevices": "Active: all devices",
|
|
14519
|
-
"alarm.arm.body.allDevicesExcept": "Active: all devices except {{
|
|
14599
|
+
"alarm.arm.body.allDevicesExcept": "Active: all devices except {{names}}",
|
|
14520
14600
|
"alarm.arm.body.noDevices": "Active: no device restriction",
|
|
14521
14601
|
"alarm.arm.body.devices": "Active: {{names}}",
|
|
14522
14602
|
"alarm.arm.body.devicesSkipped": "Active: {{names}}\nExcluded: {{skippedNames}}",
|
|
@@ -14526,7 +14606,8 @@ var en_default = {
|
|
|
14526
14606
|
"alarm.arming.body.uncovered": "Active: none — no rule uses this mode",
|
|
14527
14607
|
"alarm.arming.body.allSkipped": "Active: none\nExcluded: {{names}}",
|
|
14528
14608
|
"alarm.arming.body.skipped": "Excluded: {{names}}",
|
|
14529
|
-
"alarm.arm.body.bypassed": "Active: {{
|
|
14609
|
+
"alarm.arm.body.bypassed": "Active: {{names}}\nBypassed: {{bypassedNames}}",
|
|
14610
|
+
"alarm.arm.body.bypassedAll": "Active: all devices\nBypassed: {{bypassedNames}}",
|
|
14530
14611
|
"alarm.arming.body.bypassed": "Bypassed: {{bypassedNames}}",
|
|
14531
14612
|
"alarm.armRefused.body": "Open: {{names}}",
|
|
14532
14613
|
"alarm.armRefused.body.hint": "Open: {{names}}\nTap “Arm anyway” to arm with them excluded.",
|
|
@@ -14843,7 +14924,7 @@ var it_default = {
|
|
|
14843
14924
|
"snooze.scope.anyRule": "Una regola",
|
|
14844
14925
|
"snooze.scope.rule": "“{{rule}}”",
|
|
14845
14926
|
"alarm.arm.body.allDevices": "Attivi: tutti i dispositivi",
|
|
14846
|
-
"alarm.arm.body.allDevicesExcept": "Attivi: tutti i dispositivi tranne {{
|
|
14927
|
+
"alarm.arm.body.allDevicesExcept": "Attivi: tutti i dispositivi tranne {{names}}",
|
|
14847
14928
|
"alarm.arm.body.noDevices": "Attivi: nessuna restrizione",
|
|
14848
14929
|
"alarm.arm.body.devices": "Attivi: {{names}}",
|
|
14849
14930
|
"alarm.arm.body.devicesSkipped": "Attivi: {{names}}\nEsclusi: {{skippedNames}}",
|
|
@@ -14853,7 +14934,8 @@ var it_default = {
|
|
|
14853
14934
|
"alarm.arming.body.uncovered": "Attivi: nessuno — nessuna regola usa questa modalità",
|
|
14854
14935
|
"alarm.arming.body.allSkipped": "Attivi: nessuno\nEsclusi: {{names}}",
|
|
14855
14936
|
"alarm.arming.body.skipped": "Esclusi: {{names}}",
|
|
14856
|
-
"alarm.arm.body.bypassed": "Attivi: {{
|
|
14937
|
+
"alarm.arm.body.bypassed": "Attivi: {{names}}\nBypassati: {{bypassedNames}}",
|
|
14938
|
+
"alarm.arm.body.bypassedAll": "Attivi: tutti i dispositivi\nBypassati: {{bypassedNames}}",
|
|
14857
14939
|
"alarm.arming.body.bypassed": "Bypassati: {{bypassedNames}}",
|
|
14858
14940
|
"alarm.armRefused.body": "Aperti: {{names}}",
|
|
14859
14941
|
"alarm.armRefused.body.hint": "Aperti: {{names}}\nTocca “Arma comunque” per armare escludendoli.",
|
|
@@ -20333,6 +20415,350 @@ var NcDeviceMuteStore = class {
|
|
|
20333
20415
|
}
|
|
20334
20416
|
};
|
|
20335
20417
|
//#endregion
|
|
20418
|
+
//#region src/notification-center/migrate-legacy-actions.ts
|
|
20419
|
+
function isRecord$8(value) {
|
|
20420
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20421
|
+
}
|
|
20422
|
+
/** A legacy inline sequence row — detected by `name` + `actions` + `enabled`. */
|
|
20423
|
+
function asLegacySequence(value) {
|
|
20424
|
+
const parsed = require_clip_model_registry.NcLegacyRuleActionSequenceSchema.safeParse(value);
|
|
20425
|
+
return parsed.success ? parsed.data : null;
|
|
20426
|
+
}
|
|
20427
|
+
/**
|
|
20428
|
+
* True when `onTrigger` still looks like the pre-shared-sequence array of
|
|
20429
|
+
* named sequences (not an array of `{ kind: 'sequence' | 'do', … }` bindings).
|
|
20430
|
+
*/
|
|
20431
|
+
function onTriggerLooksLegacy(onTrigger) {
|
|
20432
|
+
if (!Array.isArray(onTrigger) || onTrigger.length === 0) return false;
|
|
20433
|
+
const first = onTrigger[0];
|
|
20434
|
+
if (!isRecord$8(first)) return false;
|
|
20435
|
+
if (first["kind"] === "sequence" || first["kind"] === "do") return false;
|
|
20436
|
+
return asLegacySequence(first) !== null;
|
|
20437
|
+
}
|
|
20438
|
+
function legacyButtonSequenceName(button) {
|
|
20439
|
+
if (!isRecord$8(button)) return null;
|
|
20440
|
+
const name = button["sequence"];
|
|
20441
|
+
return typeof name === "string" && name.length > 0 ? name : null;
|
|
20442
|
+
}
|
|
20443
|
+
/**
|
|
20444
|
+
* Rewrite a raw `actions` block. Returns the (possibly unchanged) new-shape
|
|
20445
|
+
* actions and whether anything was materialised into the sequence store.
|
|
20446
|
+
*/
|
|
20447
|
+
async function migrateLegacyRuleActions(actions, createSequence) {
|
|
20448
|
+
if (actions === void 0 || actions === null) return {
|
|
20449
|
+
actions: void 0,
|
|
20450
|
+
migrated: false
|
|
20451
|
+
};
|
|
20452
|
+
if (!isRecord$8(actions)) return {
|
|
20453
|
+
actions: void 0,
|
|
20454
|
+
migrated: false
|
|
20455
|
+
};
|
|
20456
|
+
if (!onTriggerLooksLegacy(actions["onTrigger"]) && actions["sequence"] === void 0) {
|
|
20457
|
+
const buttons = actions["buttons"];
|
|
20458
|
+
if (!(Array.isArray(buttons) && buttons.some((b) => legacyButtonSequenceName(b) !== null))) {
|
|
20459
|
+
const parsed = require_clip_model_registry.NcRuleActionsSchema.safeParse(actions);
|
|
20460
|
+
if (parsed.success) return {
|
|
20461
|
+
actions: parsed.data,
|
|
20462
|
+
migrated: false
|
|
20463
|
+
};
|
|
20464
|
+
return {
|
|
20465
|
+
actions: void 0,
|
|
20466
|
+
migrated: false
|
|
20467
|
+
};
|
|
20468
|
+
}
|
|
20469
|
+
}
|
|
20470
|
+
const legacySequences = [];
|
|
20471
|
+
const onTriggerRaw = actions["onTrigger"];
|
|
20472
|
+
if (Array.isArray(onTriggerRaw)) for (const entry of onTriggerRaw) {
|
|
20473
|
+
const seq = asLegacySequence(entry);
|
|
20474
|
+
if (seq !== null) legacySequences.push(seq);
|
|
20475
|
+
}
|
|
20476
|
+
const nameToId = /* @__PURE__ */ new Map();
|
|
20477
|
+
for (const sequence of legacySequences) {
|
|
20478
|
+
const key = sequence.name.trim();
|
|
20479
|
+
if (key === "" || nameToId.has(key)) continue;
|
|
20480
|
+
const created = await createSequence({
|
|
20481
|
+
name: sequence.name,
|
|
20482
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
20483
|
+
actions: [...sequence.actions]
|
|
20484
|
+
});
|
|
20485
|
+
nameToId.set(key, created.id);
|
|
20486
|
+
}
|
|
20487
|
+
const onTriggerBindings = [];
|
|
20488
|
+
for (const sequence of legacySequences) {
|
|
20489
|
+
if (!sequence.enabled) continue;
|
|
20490
|
+
const id = nameToId.get(sequence.name.trim());
|
|
20491
|
+
if (id === void 0) continue;
|
|
20492
|
+
onTriggerBindings.push({
|
|
20493
|
+
kind: "sequence",
|
|
20494
|
+
sequenceId: id
|
|
20495
|
+
});
|
|
20496
|
+
}
|
|
20497
|
+
const buttonsRaw = actions["buttons"];
|
|
20498
|
+
const buttons = [];
|
|
20499
|
+
if (Array.isArray(buttonsRaw)) for (const button of buttonsRaw) {
|
|
20500
|
+
if (!isRecord$8(button)) continue;
|
|
20501
|
+
const id = typeof button["id"] === "string" ? button["id"] : "";
|
|
20502
|
+
const label = typeof button["label"] === "string" ? button["label"] : "";
|
|
20503
|
+
if (id === "" || label === "") continue;
|
|
20504
|
+
const existingId = typeof button["sequenceId"] === "string" && button["sequenceId"].length > 0 ? button["sequenceId"] : void 0;
|
|
20505
|
+
const legacyName = legacyButtonSequenceName(button);
|
|
20506
|
+
const sequenceId = existingId ?? (legacyName !== null ? nameToId.get(legacyName.trim()) : void 0);
|
|
20507
|
+
if (sequenceId === void 0) continue;
|
|
20508
|
+
buttons.push({
|
|
20509
|
+
id,
|
|
20510
|
+
label,
|
|
20511
|
+
sequenceId,
|
|
20512
|
+
...typeof button["icon"] === "string" ? { icon: button["icon"] } : {},
|
|
20513
|
+
...typeof button["destructive"] === "boolean" ? { destructive: button["destructive"] } : {}
|
|
20514
|
+
});
|
|
20515
|
+
}
|
|
20516
|
+
const next = {
|
|
20517
|
+
...Array.isArray(actions["onActivation"]) ? { onActivation: actions["onActivation"] } : {},
|
|
20518
|
+
...Array.isArray(actions["onDeactivation"]) ? { onDeactivation: actions["onDeactivation"] } : {},
|
|
20519
|
+
...onTriggerBindings.length > 0 ? { onTrigger: onTriggerBindings } : {},
|
|
20520
|
+
...Array.isArray(actions["onTriggerOff"]) ? { onTriggerOff: actions["onTriggerOff"] } : {},
|
|
20521
|
+
...buttons.length > 0 ? { buttons } : {}
|
|
20522
|
+
};
|
|
20523
|
+
const parsed = require_clip_model_registry.NcRuleActionsSchema.safeParse(next);
|
|
20524
|
+
if (!parsed.success) return {
|
|
20525
|
+
actions: void 0,
|
|
20526
|
+
migrated: true
|
|
20527
|
+
};
|
|
20528
|
+
return {
|
|
20529
|
+
actions: (parsed.data.onActivation?.length ?? 0) === 0 && (parsed.data.onDeactivation?.length ?? 0) === 0 && (parsed.data.onTrigger?.length ?? 0) === 0 && (parsed.data.onTriggerOff?.length ?? 0) === 0 && (parsed.data.buttons?.length ?? 0) === 0 ? void 0 : parsed.data,
|
|
20530
|
+
migrated: true
|
|
20531
|
+
};
|
|
20532
|
+
}
|
|
20533
|
+
//#endregion
|
|
20534
|
+
//#region src/notification-center/sequence-store.ts
|
|
20535
|
+
/**
|
|
20536
|
+
* Shared action sequences for the notification centre.
|
|
20537
|
+
*
|
|
20538
|
+
* A sequence is a named, ordered run of steps (`wait` | `cap`) with its own
|
|
20539
|
+
* throttle. Rules and notification buttons bind to it by id — never by copying
|
|
20540
|
+
* the steps inline — so two rules that open the same gate share one budget and
|
|
20541
|
+
* one authoring surface.
|
|
20542
|
+
*
|
|
20543
|
+
* Mirrors {@link NcDeviceMuteStore}: declare the collection, mirror into RAM,
|
|
20544
|
+
* write-through so a failed persist never leaves a phantom sequence the next
|
|
20545
|
+
* boot will not know.
|
|
20546
|
+
*/
|
|
20547
|
+
/**
|
|
20548
|
+
* @durable class=config owner=notification-center
|
|
20549
|
+
* write="an admin creates/edits/deletes a shared sequence via
|
|
20550
|
+
* `notificationRules.createSequence` / `updateSequence` / `deleteSequence`,
|
|
20551
|
+
* or the one-shot boot migration that materialises legacy inline sequences"
|
|
20552
|
+
* retention="none — a row goes only when an admin deletes it. Bounded by the
|
|
20553
|
+
* sequence set a human is willing to author."
|
|
20554
|
+
*/
|
|
20555
|
+
var NC_SEQUENCES_COLLECTION = "notification-center:sequences";
|
|
20556
|
+
var NC_SEQUENCES_COLUMNS = [
|
|
20557
|
+
{
|
|
20558
|
+
name: "id",
|
|
20559
|
+
type: "TEXT",
|
|
20560
|
+
primaryKey: true,
|
|
20561
|
+
notNull: true
|
|
20562
|
+
},
|
|
20563
|
+
{
|
|
20564
|
+
name: "name",
|
|
20565
|
+
type: "TEXT",
|
|
20566
|
+
notNull: true
|
|
20567
|
+
},
|
|
20568
|
+
{
|
|
20569
|
+
name: "minDelaySec",
|
|
20570
|
+
type: "INTEGER"
|
|
20571
|
+
},
|
|
20572
|
+
{
|
|
20573
|
+
name: "actions",
|
|
20574
|
+
type: "JSON",
|
|
20575
|
+
notNull: true
|
|
20576
|
+
},
|
|
20577
|
+
{
|
|
20578
|
+
name: "createdAt",
|
|
20579
|
+
type: "INTEGER",
|
|
20580
|
+
notNull: true
|
|
20581
|
+
},
|
|
20582
|
+
{
|
|
20583
|
+
name: "updatedAt",
|
|
20584
|
+
type: "INTEGER",
|
|
20585
|
+
notNull: true
|
|
20586
|
+
}
|
|
20587
|
+
];
|
|
20588
|
+
function isRecord$7(value) {
|
|
20589
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20590
|
+
}
|
|
20591
|
+
function parseActions(raw) {
|
|
20592
|
+
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
20593
|
+
const out = [];
|
|
20594
|
+
for (const entry of raw) {
|
|
20595
|
+
const parsed = require_clip_model_registry.NcRuleActionSchema.safeParse(entry);
|
|
20596
|
+
if (!parsed.success) return null;
|
|
20597
|
+
out.push(parsed.data);
|
|
20598
|
+
}
|
|
20599
|
+
return out;
|
|
20600
|
+
}
|
|
20601
|
+
function rowToSequence(data) {
|
|
20602
|
+
const id = data["id"];
|
|
20603
|
+
const name = data["name"];
|
|
20604
|
+
if (typeof id !== "string" || id.length === 0) return null;
|
|
20605
|
+
if (typeof name !== "string" || name.length === 0) return null;
|
|
20606
|
+
const actions = parseActions(data["actions"]);
|
|
20607
|
+
if (actions === null) return null;
|
|
20608
|
+
const minDelayRaw = data["minDelaySec"];
|
|
20609
|
+
const minDelaySec = typeof minDelayRaw === "number" && Number.isInteger(minDelayRaw) ? minDelayRaw : void 0;
|
|
20610
|
+
const candidate = {
|
|
20611
|
+
id,
|
|
20612
|
+
name,
|
|
20613
|
+
...minDelaySec !== void 0 ? { minDelaySec } : {},
|
|
20614
|
+
actions: [...actions]
|
|
20615
|
+
};
|
|
20616
|
+
const parsed = require_clip_model_registry.NcSharedSequenceSchema.safeParse(candidate);
|
|
20617
|
+
return parsed.success ? parsed.data : null;
|
|
20618
|
+
}
|
|
20619
|
+
var NcSequenceStore = class {
|
|
20620
|
+
byId = /* @__PURE__ */ new Map();
|
|
20621
|
+
store;
|
|
20622
|
+
logger;
|
|
20623
|
+
now;
|
|
20624
|
+
constructor(deps) {
|
|
20625
|
+
this.store = deps.store;
|
|
20626
|
+
this.logger = deps.logger;
|
|
20627
|
+
this.now = deps.now ?? (() => Date.now());
|
|
20628
|
+
}
|
|
20629
|
+
static async declare(store) {
|
|
20630
|
+
await store.declareCollection.mutate({
|
|
20631
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20632
|
+
columns: [...NC_SEQUENCES_COLUMNS]
|
|
20633
|
+
});
|
|
20634
|
+
}
|
|
20635
|
+
/**
|
|
20636
|
+
* (Re)hydrate. A failed load KEEPS what is already in memory — dropping every
|
|
20637
|
+
* sequence on a transient store error would leave every button and binding
|
|
20638
|
+
* pointing at nothing until the next successful load.
|
|
20639
|
+
*/
|
|
20640
|
+
async load() {
|
|
20641
|
+
try {
|
|
20642
|
+
const rows = await this.store.query.query({
|
|
20643
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20644
|
+
filter: { limit: 1e4 }
|
|
20645
|
+
});
|
|
20646
|
+
const next = /* @__PURE__ */ new Map();
|
|
20647
|
+
let skipped = 0;
|
|
20648
|
+
for (const row of rows) {
|
|
20649
|
+
if (!isRecord$7(row.data)) {
|
|
20650
|
+
skipped += 1;
|
|
20651
|
+
continue;
|
|
20652
|
+
}
|
|
20653
|
+
const sequence = rowToSequence(row.data);
|
|
20654
|
+
if (sequence === null) {
|
|
20655
|
+
skipped += 1;
|
|
20656
|
+
continue;
|
|
20657
|
+
}
|
|
20658
|
+
next.set(sequence.id, sequence);
|
|
20659
|
+
}
|
|
20660
|
+
this.byId.clear();
|
|
20661
|
+
for (const [id, sequence] of next) this.byId.set(id, sequence);
|
|
20662
|
+
if (skipped > 0) this.logger.warn("sequence rows skipped as malformed — bindings to them will miss", { meta: { skipped } });
|
|
20663
|
+
} catch (err) {
|
|
20664
|
+
this.logger.warn("sequence load failed — keeping the sequences already in memory", { meta: {
|
|
20665
|
+
error: String(err),
|
|
20666
|
+
sequences: this.byId.size
|
|
20667
|
+
} });
|
|
20668
|
+
}
|
|
20669
|
+
}
|
|
20670
|
+
list() {
|
|
20671
|
+
return [...this.byId.values()].toSorted((a, b) => a.name.localeCompare(b.name));
|
|
20672
|
+
}
|
|
20673
|
+
get(sequenceId) {
|
|
20674
|
+
return this.byId.get(sequenceId) ?? null;
|
|
20675
|
+
}
|
|
20676
|
+
/** Hot-path existence check for button minting. */
|
|
20677
|
+
has(sequenceId) {
|
|
20678
|
+
return this.byId.has(sequenceId);
|
|
20679
|
+
}
|
|
20680
|
+
knownIds() {
|
|
20681
|
+
return new Set(this.byId.keys());
|
|
20682
|
+
}
|
|
20683
|
+
async create(input) {
|
|
20684
|
+
const now = this.now();
|
|
20685
|
+
const sequence = {
|
|
20686
|
+
id: (0, node_crypto.randomUUID)(),
|
|
20687
|
+
name: input.name,
|
|
20688
|
+
...input.minDelaySec !== void 0 ? { minDelaySec: input.minDelaySec } : {},
|
|
20689
|
+
actions: [...input.actions]
|
|
20690
|
+
};
|
|
20691
|
+
await this.persist(sequence, now, now);
|
|
20692
|
+
this.byId.set(sequence.id, sequence);
|
|
20693
|
+
return sequence;
|
|
20694
|
+
}
|
|
20695
|
+
/**
|
|
20696
|
+
* Create with a CALLER-chosen id — used by the legacy migration so a rewrite
|
|
20697
|
+
* can name the sequence before rewriting the rule that points at it.
|
|
20698
|
+
*/
|
|
20699
|
+
async createWithId(sequence) {
|
|
20700
|
+
const now = this.now();
|
|
20701
|
+
const stored = {
|
|
20702
|
+
id: sequence.id,
|
|
20703
|
+
name: sequence.name,
|
|
20704
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
20705
|
+
actions: [...sequence.actions]
|
|
20706
|
+
};
|
|
20707
|
+
await this.persist(stored, now, now);
|
|
20708
|
+
this.byId.set(stored.id, stored);
|
|
20709
|
+
return stored;
|
|
20710
|
+
}
|
|
20711
|
+
async update(sequenceId, input) {
|
|
20712
|
+
if (this.byId.get(sequenceId) === void 0) throw new Error(`sequence ${sequenceId} does not exist`);
|
|
20713
|
+
const now = this.now();
|
|
20714
|
+
const sequence = {
|
|
20715
|
+
id: sequenceId,
|
|
20716
|
+
name: input.name,
|
|
20717
|
+
...input.minDelaySec !== void 0 ? { minDelaySec: input.minDelaySec } : {},
|
|
20718
|
+
actions: [...input.actions]
|
|
20719
|
+
};
|
|
20720
|
+
const createdAt = await this.readCreatedAt(sequenceId, now);
|
|
20721
|
+
await this.persist(sequence, createdAt, now);
|
|
20722
|
+
this.byId.set(sequenceId, sequence);
|
|
20723
|
+
return sequence;
|
|
20724
|
+
}
|
|
20725
|
+
async delete(sequenceId) {
|
|
20726
|
+
await this.store.delete.mutate({
|
|
20727
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20728
|
+
key: sequenceId
|
|
20729
|
+
});
|
|
20730
|
+
this.byId.delete(sequenceId);
|
|
20731
|
+
}
|
|
20732
|
+
async readCreatedAt(sequenceId, fallback) {
|
|
20733
|
+
try {
|
|
20734
|
+
const raw = (await this.store.query.query({
|
|
20735
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20736
|
+
filter: {
|
|
20737
|
+
where: { id: sequenceId },
|
|
20738
|
+
limit: 1
|
|
20739
|
+
}
|
|
20740
|
+
}))[0]?.data["createdAt"];
|
|
20741
|
+
return typeof raw === "number" && Number.isFinite(raw) ? raw : fallback;
|
|
20742
|
+
} catch {
|
|
20743
|
+
return fallback;
|
|
20744
|
+
}
|
|
20745
|
+
}
|
|
20746
|
+
async persist(sequence, createdAt, updatedAt) {
|
|
20747
|
+
await this.store.set.mutate({
|
|
20748
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20749
|
+
key: sequence.id,
|
|
20750
|
+
value: {
|
|
20751
|
+
id: sequence.id,
|
|
20752
|
+
name: sequence.name,
|
|
20753
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
20754
|
+
actions: sequence.actions,
|
|
20755
|
+
createdAt,
|
|
20756
|
+
updatedAt
|
|
20757
|
+
}
|
|
20758
|
+
});
|
|
20759
|
+
}
|
|
20760
|
+
};
|
|
20761
|
+
//#endregion
|
|
20336
20762
|
//#region src/notification-center/device-prune.ts
|
|
20337
20763
|
function judgeList(list, isDead) {
|
|
20338
20764
|
const dead = list.filter(isDead);
|
|
@@ -20446,58 +20872,49 @@ function pruneRuleConditions(conditions, isDead, notes) {
|
|
|
20446
20872
|
if (sceneDevice !== void 0 && isDead(sceneDevice)) notes.kept("conditions.scene", [String(sceneDevice)]);
|
|
20447
20873
|
return changed ? next : null;
|
|
20448
20874
|
}
|
|
20449
|
-
/** A
|
|
20450
|
-
|
|
20451
|
-
|
|
20452
|
-
|
|
20453
|
-
|
|
20454
|
-
|
|
20455
|
-
return false;
|
|
20456
|
-
});
|
|
20457
|
-
if (dead.length === 0) return null;
|
|
20458
|
-
const anyDeviceStep = kept.some((step) => step.kind === "cap");
|
|
20459
|
-
const [first, ...rest] = kept;
|
|
20460
|
-
if (!anyDeviceStep || first === void 0) return {
|
|
20461
|
-
next: "removed",
|
|
20462
|
-
dead
|
|
20463
|
-
};
|
|
20464
|
-
return {
|
|
20465
|
-
next: {
|
|
20466
|
-
...sequence,
|
|
20467
|
-
actions: [first, ...rest]
|
|
20468
|
-
},
|
|
20469
|
-
dead
|
|
20875
|
+
/** A Do binding whose device is dead is dropped; sequence refs are left alone
|
|
20876
|
+
* (shared sequences are pruned on their own store, not per-rule). */
|
|
20877
|
+
function pruneHookBindings(bindings, field, isDead, notes) {
|
|
20878
|
+
if (bindings === void 0) return {
|
|
20879
|
+
next: void 0,
|
|
20880
|
+
changed: false
|
|
20470
20881
|
};
|
|
20471
|
-
|
|
20472
|
-
/** The actions after a device prune — `null` when no step moved. */
|
|
20473
|
-
function pruneRuleActions(actions, isDead, notes) {
|
|
20474
|
-
const sequences = actions.onTrigger;
|
|
20475
|
-
if (sequences === void 0) return null;
|
|
20882
|
+
const kept = [];
|
|
20476
20883
|
let changed = false;
|
|
20477
|
-
const
|
|
20478
|
-
|
|
20479
|
-
|
|
20480
|
-
const pruned = pruneSequence(sequence, isDead);
|
|
20481
|
-
if (pruned === null) {
|
|
20482
|
-
nextSequences.push(sequence);
|
|
20884
|
+
for (const binding of bindings) {
|
|
20885
|
+
if (binding.kind === "sequence") {
|
|
20886
|
+
kept.push(binding);
|
|
20483
20887
|
continue;
|
|
20484
20888
|
}
|
|
20485
|
-
|
|
20486
|
-
|
|
20487
|
-
|
|
20488
|
-
|
|
20489
|
-
notes.removed(field, deviceSubjects(pruned.dead), "sequence-removed");
|
|
20490
|
-
} else {
|
|
20491
|
-
nextSequences.push(pruned.next);
|
|
20492
|
-
notes.removed(field, deviceSubjects(pruned.dead));
|
|
20889
|
+
if (binding.action.kind === "cap" && isDead(binding.action.deviceId)) {
|
|
20890
|
+
changed = true;
|
|
20891
|
+
notes.removed(field, deviceSubjects([binding.action.deviceId]), "binding-removed");
|
|
20892
|
+
continue;
|
|
20493
20893
|
}
|
|
20894
|
+
kept.push(binding);
|
|
20494
20895
|
}
|
|
20495
|
-
if (!changed) return
|
|
20496
|
-
|
|
20896
|
+
if (!changed) return {
|
|
20897
|
+
next: bindings,
|
|
20898
|
+
changed: false
|
|
20899
|
+
};
|
|
20900
|
+
return {
|
|
20901
|
+
next: kept.length > 0 ? kept : void 0,
|
|
20902
|
+
changed: true
|
|
20903
|
+
};
|
|
20904
|
+
}
|
|
20905
|
+
/** The actions after a device prune — `null` when no binding moved. */
|
|
20906
|
+
function pruneRuleActions(actions, isDead, notes) {
|
|
20907
|
+
const onActivation = pruneHookBindings(actions.onActivation, "actions.onActivation", isDead, notes);
|
|
20908
|
+
const onDeactivation = pruneHookBindings(actions.onDeactivation, "actions.onDeactivation", isDead, notes);
|
|
20909
|
+
const onTrigger = pruneHookBindings(actions.onTrigger, "actions.onTrigger", isDead, notes);
|
|
20910
|
+
const onTriggerOff = pruneHookBindings(actions.onTriggerOff, "actions.onTriggerOff", isDead, notes);
|
|
20911
|
+
if (!onActivation.changed && !onDeactivation.changed && !onTrigger.changed && !onTriggerOff.changed) return null;
|
|
20497
20912
|
return {
|
|
20498
|
-
...
|
|
20499
|
-
|
|
20500
|
-
...
|
|
20913
|
+
...onActivation.next !== void 0 ? { onActivation: [...onActivation.next] } : {},
|
|
20914
|
+
...onDeactivation.next !== void 0 ? { onDeactivation: [...onDeactivation.next] } : {},
|
|
20915
|
+
...onTrigger.next !== void 0 ? { onTrigger: [...onTrigger.next] } : {},
|
|
20916
|
+
...onTriggerOff.next !== void 0 ? { onTriggerOff: [...onTriggerOff.next] } : {},
|
|
20917
|
+
...actions.buttons !== void 0 ? { buttons: actions.buttons } : {}
|
|
20501
20918
|
};
|
|
20502
20919
|
}
|
|
20503
20920
|
function planRuleDevices(rule, isDead, guard) {
|
|
@@ -24732,6 +25149,10 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
|
|
|
24732
25149
|
return {
|
|
24733
25150
|
camera: deviceName,
|
|
24734
25151
|
class: detection?.className ?? (systemEvent !== void 0 ? "" : subject.className),
|
|
25152
|
+
classLabel: (() => {
|
|
25153
|
+
const wire = detection?.className ?? (systemEvent !== void 0 ? "" : subject.className);
|
|
25154
|
+
return wire.length === 0 ? "" : require_clip_model_registry.eventKindLabel(wire, texts.language);
|
|
25155
|
+
})(),
|
|
24735
25156
|
label: detection?.label ?? subject.label ?? "",
|
|
24736
25157
|
zones: zoneLabels.join(", "),
|
|
24737
25158
|
subject: detection?.className !== void 0 ? ncSubjectText(texts, {
|
|
@@ -28873,9 +29294,32 @@ function lastRunRow(data) {
|
|
|
28873
29294
|
}
|
|
28874
29295
|
//#endregion
|
|
28875
29296
|
//#region src/notification-center/rule-actions.ts
|
|
29297
|
+
function wrapDo(action) {
|
|
29298
|
+
const name = action.kind === "wait" ? `wait ${String(action.seconds)}s` : `${action.cap}.${action.method}`;
|
|
29299
|
+
return {
|
|
29300
|
+
id: `do:${name}`,
|
|
29301
|
+
name,
|
|
29302
|
+
enabled: true,
|
|
29303
|
+
actions: [action],
|
|
29304
|
+
throttle: false
|
|
29305
|
+
};
|
|
29306
|
+
}
|
|
29307
|
+
function resolveBinding(binding, lookup) {
|
|
29308
|
+
if (binding.kind === "do") return wrapDo(binding.action);
|
|
29309
|
+
const sequence = lookup(binding.sequenceId);
|
|
29310
|
+
if (sequence === null) return { missing: binding.sequenceId };
|
|
29311
|
+
return {
|
|
29312
|
+
id: sequence.id,
|
|
29313
|
+
name: sequence.name,
|
|
29314
|
+
enabled: true,
|
|
29315
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
29316
|
+
actions: sequence.actions,
|
|
29317
|
+
throttle: true
|
|
29318
|
+
};
|
|
29319
|
+
}
|
|
28876
29320
|
var NcRuleActionRunner = class {
|
|
28877
29321
|
deps;
|
|
28878
|
-
/** Last run per
|
|
29322
|
+
/** Last run per `sequenceId` — the throttle's memory (shared across rules). */
|
|
28879
29323
|
lastRunAt = /* @__PURE__ */ new Map();
|
|
28880
29324
|
now;
|
|
28881
29325
|
sleep;
|
|
@@ -28885,11 +29329,49 @@ var NcRuleActionRunner = class {
|
|
|
28885
29329
|
this.sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
28886
29330
|
}
|
|
28887
29331
|
/**
|
|
28888
|
-
*
|
|
29332
|
+
* Resolve bindings via the sequence store and run each, in order.
|
|
28889
29333
|
*
|
|
28890
29334
|
* Never throws: an actuation that fails must not cost the NOTIFICATION about
|
|
28891
|
-
* the very same event.
|
|
28892
|
-
|
|
29335
|
+
* the very same event.
|
|
29336
|
+
*/
|
|
29337
|
+
async runBindings(input) {
|
|
29338
|
+
const sequences = [];
|
|
29339
|
+
const missing = [];
|
|
29340
|
+
for (const binding of input.bindings) {
|
|
29341
|
+
const resolved = resolveBinding(binding, input.lookup);
|
|
29342
|
+
if ("missing" in resolved) {
|
|
29343
|
+
this.deps.logger.warn("rule action binding skipped — sequence missing", {
|
|
29344
|
+
tags: { deviceId: input.deviceId },
|
|
29345
|
+
meta: {
|
|
29346
|
+
ruleId: input.ruleId,
|
|
29347
|
+
sequenceId: resolved.missing
|
|
29348
|
+
}
|
|
29349
|
+
});
|
|
29350
|
+
missing.push({
|
|
29351
|
+
id: resolved.missing,
|
|
29352
|
+
name: resolved.missing,
|
|
29353
|
+
ran: false,
|
|
29354
|
+
stepsRun: 0,
|
|
29355
|
+
skippedReason: "missing"
|
|
29356
|
+
});
|
|
29357
|
+
continue;
|
|
29358
|
+
}
|
|
29359
|
+
sequences.push(resolved);
|
|
29360
|
+
}
|
|
29361
|
+
const ran = await this.run({
|
|
29362
|
+
ruleId: input.ruleId,
|
|
29363
|
+
ruleName: input.ruleName,
|
|
29364
|
+
deviceId: input.deviceId,
|
|
29365
|
+
sequences,
|
|
29366
|
+
...input.origin !== void 0 ? { origin: input.origin } : {}
|
|
29367
|
+
});
|
|
29368
|
+
return [...missing, ...ran];
|
|
29369
|
+
}
|
|
29370
|
+
/**
|
|
29371
|
+
* Run every enabled sequence, in order.
|
|
29372
|
+
*
|
|
29373
|
+
* Prefer {@link runBindings} at call sites that hold bindings. This remains
|
|
29374
|
+
* for button grants (force-enabled shared sequence) and tests.
|
|
28893
29375
|
*/
|
|
28894
29376
|
async run(input) {
|
|
28895
29377
|
const out = [];
|
|
@@ -28898,38 +29380,41 @@ var NcRuleActionRunner = class {
|
|
|
28898
29380
|
}
|
|
28899
29381
|
async runOne(input, sequence) {
|
|
28900
29382
|
if (!sequence.enabled) return {
|
|
29383
|
+
id: sequence.id,
|
|
28901
29384
|
name: sequence.name,
|
|
28902
29385
|
ran: false,
|
|
28903
29386
|
stepsRun: 0,
|
|
28904
29387
|
skippedReason: "disabled"
|
|
28905
29388
|
};
|
|
28906
29389
|
if (sequence.actions.length === 0) return {
|
|
29390
|
+
id: sequence.id,
|
|
28907
29391
|
name: sequence.name,
|
|
28908
29392
|
ran: false,
|
|
28909
29393
|
stepsRun: 0,
|
|
28910
29394
|
skippedReason: "empty"
|
|
28911
29395
|
};
|
|
28912
|
-
const key = `${input.ruleId}:${sequence.name}`;
|
|
28913
29396
|
const now = this.now();
|
|
28914
|
-
const minDelayMs = (sequence.minDelaySec ?? 0) * 1e3;
|
|
28915
|
-
const last = this.lastRunAt.get(
|
|
29397
|
+
const minDelayMs = sequence.throttle ? (sequence.minDelaySec ?? 0) * 1e3 : 0;
|
|
29398
|
+
const last = this.lastRunAt.get(sequence.id);
|
|
28916
29399
|
if (minDelayMs > 0 && last !== void 0 && now - last < minDelayMs) {
|
|
28917
29400
|
this.deps.logger.debug("rule action sequence throttled", {
|
|
28918
29401
|
tags: { deviceId: input.deviceId },
|
|
28919
29402
|
meta: {
|
|
28920
29403
|
ruleId: input.ruleId,
|
|
29404
|
+
sequenceId: sequence.id,
|
|
28921
29405
|
sequence: sequence.name,
|
|
28922
29406
|
sinceLastMs: now - last
|
|
28923
29407
|
}
|
|
28924
29408
|
});
|
|
28925
29409
|
return {
|
|
29410
|
+
id: sequence.id,
|
|
28926
29411
|
name: sequence.name,
|
|
28927
29412
|
ran: false,
|
|
28928
29413
|
stepsRun: 0,
|
|
28929
29414
|
skippedReason: "throttled"
|
|
28930
29415
|
};
|
|
28931
29416
|
}
|
|
28932
|
-
this.lastRunAt.set(
|
|
29417
|
+
if (sequence.throttle) this.lastRunAt.set(sequence.id, now);
|
|
28933
29418
|
let stepsRun = 0;
|
|
28934
29419
|
for (const [index, action] of sequence.actions.entries()) try {
|
|
28935
29420
|
await this.step(action, input.origin);
|
|
@@ -28940,6 +29425,7 @@ var NcRuleActionRunner = class {
|
|
|
28940
29425
|
meta: {
|
|
28941
29426
|
ruleId: input.ruleId,
|
|
28942
29427
|
rule: input.ruleName,
|
|
29428
|
+
sequenceId: sequence.id,
|
|
28943
29429
|
sequence: sequence.name,
|
|
28944
29430
|
step: index,
|
|
28945
29431
|
action: action.kind === "cap" ? `${action.cap}.${action.method}` : "wait",
|
|
@@ -28947,6 +29433,7 @@ var NcRuleActionRunner = class {
|
|
|
28947
29433
|
}
|
|
28948
29434
|
});
|
|
28949
29435
|
return {
|
|
29436
|
+
id: sequence.id,
|
|
28950
29437
|
name: sequence.name,
|
|
28951
29438
|
ran: true,
|
|
28952
29439
|
stepsRun,
|
|
@@ -28958,11 +29445,13 @@ var NcRuleActionRunner = class {
|
|
|
28958
29445
|
meta: {
|
|
28959
29446
|
ruleId: input.ruleId,
|
|
28960
29447
|
rule: input.ruleName,
|
|
29448
|
+
sequenceId: sequence.id,
|
|
28961
29449
|
sequence: sequence.name,
|
|
28962
29450
|
steps: stepsRun
|
|
28963
29451
|
}
|
|
28964
29452
|
});
|
|
28965
29453
|
return {
|
|
29454
|
+
id: sequence.id,
|
|
28966
29455
|
name: sequence.name,
|
|
28967
29456
|
ran: true,
|
|
28968
29457
|
stepsRun
|
|
@@ -28983,6 +29472,17 @@ var NcRuleActionRunner = class {
|
|
|
28983
29472
|
});
|
|
28984
29473
|
}
|
|
28985
29474
|
};
|
|
29475
|
+
/** Force-enable a shared sequence for a button tap (always runs, still throttled). */
|
|
29476
|
+
function asForcedSequence(sequence) {
|
|
29477
|
+
return {
|
|
29478
|
+
id: sequence.id,
|
|
29479
|
+
name: sequence.name,
|
|
29480
|
+
enabled: true,
|
|
29481
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
29482
|
+
actions: sequence.actions,
|
|
29483
|
+
throttle: true
|
|
29484
|
+
};
|
|
29485
|
+
}
|
|
28986
29486
|
//#endregion
|
|
28987
29487
|
//#region src/notification-center/rule-ai-analyst.ts
|
|
28988
29488
|
/**
|
|
@@ -30829,7 +31329,7 @@ function snoozeVerdict(input, deps) {
|
|
|
30829
31329
|
});
|
|
30830
31330
|
if (active === null) return { kind: "clear" };
|
|
30831
31331
|
const rule = deps.ruleFor(input.ruleId);
|
|
30832
|
-
if (rule !== null && isSnoozeExempt(rule)) return {
|
|
31332
|
+
if (rule !== null && isSnoozeExempt(rule, deps.sequenceActionsOf)) return {
|
|
30833
31333
|
kind: "exempt",
|
|
30834
31334
|
snooze: active
|
|
30835
31335
|
};
|
|
@@ -36630,13 +37130,16 @@ function frozenOccupancy(rule, occ) {
|
|
|
36630
37130
|
function fireRefFor(rule, subject, kind) {
|
|
36631
37131
|
const hasEventMedia = kind === "object-event" || kind === "package-event";
|
|
36632
37132
|
const className = subject.classNames[0];
|
|
37133
|
+
const linkedOwners = subject.trackId === void 0 && subject.linkedStills !== void 0 ? linkedMediaOwners(subject.linkedStills, linkedCameraMode(rule.media)) : void 0;
|
|
37134
|
+
const trackId = subject.trackId ?? linkedOwners?.trackId;
|
|
36633
37135
|
return {
|
|
36634
37136
|
ruleId: rule.id,
|
|
36635
37137
|
ruleName: rule.name,
|
|
36636
37138
|
deviceId: subject.deviceId,
|
|
36637
37139
|
...subject.sourceDeviceId !== void 0 ? { sourceDeviceId: subject.sourceDeviceId } : {},
|
|
36638
37140
|
recordId: subject.recordId,
|
|
36639
|
-
...
|
|
37141
|
+
...trackId !== void 0 ? { trackId } : {},
|
|
37142
|
+
...linkedOwners !== void 0 && linkedOwners.extraStills.length > 0 ? { extraStills: linkedOwners.extraStills.map((x) => ({ ...x })) } : {},
|
|
36640
37143
|
...hasEventMedia ? { eventId: subject.recordId } : {},
|
|
36641
37144
|
timestamp: subject.timestamp,
|
|
36642
37145
|
...className !== void 0 ? { className } : {},
|
|
@@ -36941,6 +37444,8 @@ var NotificationCenter = class {
|
|
|
36941
37444
|
snoozes;
|
|
36942
37445
|
/** The permanent per-camera notification off switch — see `device-mute-store.ts`. */
|
|
36943
37446
|
deviceMutes;
|
|
37447
|
+
/** Shared action sequences rules and buttons bind to by id. */
|
|
37448
|
+
sequences;
|
|
36944
37449
|
/** The durable episode rows behind the sustained-state alerts. */
|
|
36945
37450
|
alertStore;
|
|
36946
37451
|
/** Battery band → notification, and the reminder sweep. */
|
|
@@ -37179,7 +37684,8 @@ var NotificationCenter = class {
|
|
|
37179
37684
|
const drafts = [];
|
|
37180
37685
|
const sequenceButtons = this.actionRunner === null || stored === null ? [] : buildActionButtons({
|
|
37181
37686
|
actions: stored.actions,
|
|
37182
|
-
|
|
37687
|
+
knownSequenceIds: this.sequences.knownIds(),
|
|
37688
|
+
mintUrl: ({ sequenceId, actionId }) => {
|
|
37183
37689
|
const id = (0, node_crypto.randomUUID)();
|
|
37184
37690
|
drafts.push({
|
|
37185
37691
|
id,
|
|
@@ -37189,7 +37695,7 @@ var NotificationCenter = class {
|
|
|
37189
37695
|
id,
|
|
37190
37696
|
ruleId: input.ruleId,
|
|
37191
37697
|
ruleName: input.ruleName,
|
|
37192
|
-
|
|
37698
|
+
sequenceId,
|
|
37193
37699
|
deviceId: input.deviceId,
|
|
37194
37700
|
actionId,
|
|
37195
37701
|
targetId: input.targetId,
|
|
@@ -37201,6 +37707,7 @@ var NotificationCenter = class {
|
|
|
37201
37707
|
});
|
|
37202
37708
|
const snoozeButtons = input.ownerUserId === void 0 || stored === null ? [] : buildSnoozeButtons({
|
|
37203
37709
|
rule: stored,
|
|
37710
|
+
sequenceActionsOf: this.sequenceActionsOf,
|
|
37204
37711
|
label: (minutes) => this.snoozeButtonLabel(minutes),
|
|
37205
37712
|
mintUrl: ({ actionId, minutes }) => {
|
|
37206
37713
|
const id = (0, node_crypto.randomUUID)();
|
|
@@ -37441,7 +37948,7 @@ var NotificationCenter = class {
|
|
|
37441
37948
|
*/
|
|
37442
37949
|
async applyGrantedSnooze(grant) {
|
|
37443
37950
|
const rule = this.rules.get(grant.ruleId);
|
|
37444
|
-
if (rule !== null && isSnoozeExempt(rule)) throw new Error(`"${grant.ruleName}" is an alarm rule and cannot be snoozed`);
|
|
37951
|
+
if (rule !== null && isSnoozeExempt(rule, this.sequenceActionsOf)) throw new Error(`"${grant.ruleName}" is an alarm rule and cannot be snoozed`);
|
|
37445
37952
|
const snooze = await this.snoozes.create({
|
|
37446
37953
|
userId: grant.userId,
|
|
37447
37954
|
scope: "device",
|
|
@@ -37740,16 +38247,13 @@ var NotificationCenter = class {
|
|
|
37740
38247
|
if (this.actionRunner === null) throw new Error("no actuator is wired on this node");
|
|
37741
38248
|
const rule = this.rules.get(grant.ruleId);
|
|
37742
38249
|
if (rule === null) throw new Error(`rule ${grant.ruleId} no longer exists`);
|
|
37743
|
-
const sequence =
|
|
37744
|
-
if (sequence ===
|
|
38250
|
+
const sequence = this.sequences.get(grant.sequenceId);
|
|
38251
|
+
if (sequence === null) throw new Error(`sequence "${grant.sequenceId}" no longer exists`);
|
|
37745
38252
|
const [outcome] = await this.actionRunner.run({
|
|
37746
38253
|
ruleId: rule.id,
|
|
37747
38254
|
ruleName: rule.name,
|
|
37748
38255
|
deviceId: grant.deviceId,
|
|
37749
|
-
sequences: [
|
|
37750
|
-
...sequence,
|
|
37751
|
-
enabled: true
|
|
37752
|
-
}]
|
|
38256
|
+
sequences: [asForcedSequence(sequence)]
|
|
37753
38257
|
});
|
|
37754
38258
|
if (outcome === void 0) throw new Error("the sequence did not run");
|
|
37755
38259
|
if (outcome.failedAt !== void 0) throw new Error(`step ${String(outcome.failedAt)} of "${sequence.name}" failed`);
|
|
@@ -37868,6 +38372,11 @@ var NotificationCenter = class {
|
|
|
37868
38372
|
logger: this.logger.child("device-mute"),
|
|
37869
38373
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
37870
38374
|
});
|
|
38375
|
+
this.sequences = new NcSequenceStore({
|
|
38376
|
+
store: deps.store,
|
|
38377
|
+
logger: this.logger.child("sequences"),
|
|
38378
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
38379
|
+
});
|
|
37871
38380
|
this.alertStore = new NcAlertStore({
|
|
37872
38381
|
store: deps.store,
|
|
37873
38382
|
logger: this.logger.child("device-alerts")
|
|
@@ -38134,6 +38643,7 @@ var NotificationCenter = class {
|
|
|
38134
38643
|
await NcStateSummaryService.declare(store);
|
|
38135
38644
|
await NcSnoozeStore.declare(store);
|
|
38136
38645
|
await NcDeviceMuteStore.declare(store);
|
|
38646
|
+
await NcSequenceStore.declare(store);
|
|
38137
38647
|
await TimelapseStore.declare(store);
|
|
38138
38648
|
await NcSummaryStore.declare(store);
|
|
38139
38649
|
await NcCalendarEventsStore.declare(store);
|
|
@@ -38195,6 +38705,8 @@ var NotificationCenter = class {
|
|
|
38195
38705
|
sinceStartMs: now - t0
|
|
38196
38706
|
} });
|
|
38197
38707
|
};
|
|
38708
|
+
await timed("sequences", () => this.sequences.load());
|
|
38709
|
+
await timed("migrateLegacyActions", () => this.migrateLegacyRuleActions());
|
|
38198
38710
|
await Promise.all([
|
|
38199
38711
|
timed("texts", () => this.reloadTexts()),
|
|
38200
38712
|
timed("rules", () => this.rules.load()),
|
|
@@ -39494,7 +40006,32 @@ var NotificationCenter = class {
|
|
|
39494
40006
|
return { success: true };
|
|
39495
40007
|
},
|
|
39496
40008
|
setRuleEnabled: async ({ ruleId, enabled }) => {
|
|
39497
|
-
await this.rules.setEnabled(ruleId, enabled);
|
|
40009
|
+
const updated = await this.rules.setEnabled(ruleId, enabled);
|
|
40010
|
+
const bindings = enabled ? updated.actions?.onActivation : updated.actions?.onDeactivation;
|
|
40011
|
+
if (bindings !== void 0 && bindings.length > 0 && this.actionRunner !== null) this.actionRunner.runBindings({
|
|
40012
|
+
ruleId: updated.id,
|
|
40013
|
+
ruleName: updated.name,
|
|
40014
|
+
deviceId: this.deviceIdForRuleActions(updated),
|
|
40015
|
+
bindings,
|
|
40016
|
+
lookup: (id) => this.sequences.get(id)
|
|
40017
|
+
});
|
|
40018
|
+
return { success: true };
|
|
40019
|
+
},
|
|
40020
|
+
listSequences: async () => ({ sequences: [...this.sequences.list()] }),
|
|
40021
|
+
getSequence: async ({ sequenceId }) => ({ sequence: this.sequences.get(sequenceId) }),
|
|
40022
|
+
createSequence: async ({ sequence }) => {
|
|
40023
|
+
const created = await this.sequences.create(sequence);
|
|
40024
|
+
this.logger.info("notification sequence created", { meta: { sequenceId: created.id } });
|
|
40025
|
+
return { sequence: created };
|
|
40026
|
+
},
|
|
40027
|
+
updateSequence: async ({ sequenceId, sequence }) => {
|
|
40028
|
+
const updated = await this.sequences.update(sequenceId, sequence);
|
|
40029
|
+
this.logger.info("notification sequence updated", { meta: { sequenceId } });
|
|
40030
|
+
return { sequence: updated };
|
|
40031
|
+
},
|
|
40032
|
+
deleteSequence: async ({ sequenceId }) => {
|
|
40033
|
+
await this.sequences.delete(sequenceId);
|
|
40034
|
+
this.logger.info("notification sequence deleted", { meta: { sequenceId } });
|
|
39498
40035
|
return { success: true };
|
|
39499
40036
|
},
|
|
39500
40037
|
testRule: async ({ rule, lookbackMinutes }) => ({ results: [...await this.dryRun(rule, lookbackMinutes)] }),
|
|
@@ -40228,10 +40765,11 @@ var NotificationCenter = class {
|
|
|
40228
40765
|
const alarmSource = subject.systemEvent?.alarmSource;
|
|
40229
40766
|
const sourceRule = alarmSource !== void 0 ? this.rules.get(alarmSource.ruleId) : null;
|
|
40230
40767
|
const media = sourceRule !== null ? mediaPolicyFields(sourceRule, void 0) : { media: "none" };
|
|
40768
|
+
const mediaDeviceId = alarmSource?.deviceId ?? panel.deviceId;
|
|
40231
40769
|
const entries = targets.map((targetId) => ({
|
|
40232
40770
|
ruleId: NC_ALARM_ANNOUNCE_RULE_ID,
|
|
40233
40771
|
targetId,
|
|
40234
|
-
deviceId:
|
|
40772
|
+
deviceId: mediaDeviceId,
|
|
40235
40773
|
recordKind: "system-event",
|
|
40236
40774
|
recordId: subject.recordId,
|
|
40237
40775
|
payload: {
|
|
@@ -40240,8 +40778,9 @@ var NotificationCenter = class {
|
|
|
40240
40778
|
priority: 2,
|
|
40241
40779
|
...media,
|
|
40242
40780
|
subject: {
|
|
40243
|
-
deviceId:
|
|
40781
|
+
deviceId: mediaDeviceId,
|
|
40244
40782
|
...alarmSource?.trackId !== void 0 ? { trackId: alarmSource.trackId } : {},
|
|
40783
|
+
...alarmSource?.extraStills !== void 0 && alarmSource.extraStills.length > 0 ? { extraStills: alarmSource.extraStills.map((x) => ({ ...x })) } : {},
|
|
40245
40784
|
...alarmSource?.eventId !== void 0 ? { eventId: alarmSource.eventId } : {},
|
|
40246
40785
|
className: "alarm",
|
|
40247
40786
|
zones: [],
|
|
@@ -40991,7 +41530,7 @@ var NotificationCenter = class {
|
|
|
40991
41530
|
} : {}
|
|
40992
41531
|
}
|
|
40993
41532
|
});
|
|
40994
|
-
const unresolvable = unresolvableButtons(rule.actions);
|
|
41533
|
+
const unresolvable = unresolvableButtons(rule.actions, this.sequences.knownIds());
|
|
40995
41534
|
if (unresolvable.length > 0) this.logger.warn("rule buttons reference sequences that do not exist — not sent", {
|
|
40996
41535
|
tags: { deviceId: subject.deviceId },
|
|
40997
41536
|
meta: {
|
|
@@ -41118,20 +41657,85 @@ var NotificationCenter = class {
|
|
|
41118
41657
|
return this.groupStore.list(query);
|
|
41119
41658
|
}
|
|
41120
41659
|
/**
|
|
41121
|
-
*
|
|
41660
|
+
* Materialise legacy inline `onTrigger` sequences into the shared store and
|
|
41661
|
+
* rewrite each rule's actions to bindings + `buttons.sequenceId`. Runs on RAW
|
|
41662
|
+
* rows before {@link NcRuleStore.load} so Zod never skips them.
|
|
41663
|
+
*/
|
|
41664
|
+
async migrateLegacyRuleActions() {
|
|
41665
|
+
let rows;
|
|
41666
|
+
try {
|
|
41667
|
+
rows = await this.deps.store.query.query({
|
|
41668
|
+
collection: NC_RULES_COLLECTION,
|
|
41669
|
+
filter: { limit: 1e4 }
|
|
41670
|
+
});
|
|
41671
|
+
} catch (err) {
|
|
41672
|
+
this.logger.warn("legacy actions migration skipped — could not read rules", { meta: { error: String(err) } });
|
|
41673
|
+
return;
|
|
41674
|
+
}
|
|
41675
|
+
let migrated = 0;
|
|
41676
|
+
for (const row of rows) {
|
|
41677
|
+
const ruleBlob = row.data["rule"];
|
|
41678
|
+
if (typeof ruleBlob !== "object" || ruleBlob === null || Array.isArray(ruleBlob)) continue;
|
|
41679
|
+
const rule = { ...ruleBlob };
|
|
41680
|
+
const ruleId = typeof rule["id"] === "string" ? rule["id"] : void 0;
|
|
41681
|
+
if (ruleId === void 0) continue;
|
|
41682
|
+
const result = await migrateLegacyRuleActions(rule["actions"], async (input) => {
|
|
41683
|
+
return { id: (await this.sequences.create(input)).id };
|
|
41684
|
+
});
|
|
41685
|
+
if (!result.migrated) continue;
|
|
41686
|
+
if (result.actions !== void 0) rule["actions"] = result.actions;
|
|
41687
|
+
else delete rule["actions"];
|
|
41688
|
+
const { id: _rowId, ...columns } = row.data;
|
|
41689
|
+
await this.deps.store.set.mutate({
|
|
41690
|
+
collection: NC_RULES_COLLECTION,
|
|
41691
|
+
key: ruleId,
|
|
41692
|
+
value: {
|
|
41693
|
+
...columns,
|
|
41694
|
+
rule
|
|
41695
|
+
}
|
|
41696
|
+
});
|
|
41697
|
+
migrated += 1;
|
|
41698
|
+
}
|
|
41699
|
+
if (migrated > 0) this.logger.info("migrated legacy inline rule actions into shared sequences", { meta: {
|
|
41700
|
+
rules: migrated,
|
|
41701
|
+
sequences: this.sequences.list().length
|
|
41702
|
+
} });
|
|
41703
|
+
}
|
|
41704
|
+
/**
|
|
41705
|
+
* True when this match is a clear / leave edge — the subject for
|
|
41706
|
+
* `onTriggerOff`. Raise / enter / ordinary matches use `onTrigger`.
|
|
41707
|
+
*/
|
|
41708
|
+
isTriggerOffSubject(subject) {
|
|
41709
|
+
if (subject.sensorRaised === false) return true;
|
|
41710
|
+
if (subject.occupancy !== void 0 && subject.occupancy.occupied === false) return true;
|
|
41711
|
+
return false;
|
|
41712
|
+
}
|
|
41713
|
+
/** Device id for activation/deactivation log tags — first scoped device, else 0. */
|
|
41714
|
+
deviceIdForRuleActions(rule) {
|
|
41715
|
+
const devices = rule.conditions.devices;
|
|
41716
|
+
if (devices !== void 0 && devices.length > 0) {
|
|
41717
|
+
const first = devices[0];
|
|
41718
|
+
if (first !== void 0) return first;
|
|
41719
|
+
}
|
|
41720
|
+
return 0;
|
|
41721
|
+
}
|
|
41722
|
+
sequenceActionsOf = (sequenceId) => this.sequences.get(sequenceId)?.actions;
|
|
41723
|
+
/**
|
|
41724
|
+
* Run this rule's onTrigger / onTriggerOff bindings for this match.
|
|
41122
41725
|
*
|
|
41123
41726
|
* Called BEFORE the cooldown gate — see the call site for why. Everything
|
|
41124
41727
|
* here is fire-and-forget and nothing may throw into the evaluation.
|
|
41125
41728
|
*/
|
|
41126
41729
|
runRuleActions(rule, subject, kind, controls) {
|
|
41127
|
-
const sequences = rule.actions?.onTrigger;
|
|
41128
|
-
if (sequences === void 0 || sequences.length === 0) return;
|
|
41129
41730
|
if (this.actionRunner === null || controls.runActions === false) return;
|
|
41130
|
-
this.
|
|
41731
|
+
const bindings = this.isTriggerOffSubject(subject) ? rule.actions?.onTriggerOff : rule.actions?.onTrigger;
|
|
41732
|
+
if (bindings === void 0 || bindings.length === 0) return;
|
|
41733
|
+
this.actionRunner.runBindings({
|
|
41131
41734
|
ruleId: rule.id,
|
|
41132
41735
|
ruleName: rule.name,
|
|
41133
41736
|
deviceId: subject.deviceId,
|
|
41134
|
-
|
|
41737
|
+
bindings,
|
|
41738
|
+
lookup: (id) => this.sequences.get(id),
|
|
41135
41739
|
origin: fireRefFor(rule, subject, kind)
|
|
41136
41740
|
});
|
|
41137
41741
|
}
|
|
@@ -41261,7 +41865,10 @@ var NotificationCenter = class {
|
|
|
41261
41865
|
...target.params !== void 0 ? { params: target.params } : {},
|
|
41262
41866
|
subject: {
|
|
41263
41867
|
deviceId: subject.deviceId,
|
|
41264
|
-
...alarmSource?.trackId !== void 0 ? {
|
|
41868
|
+
...alarmSource?.trackId !== void 0 ? {
|
|
41869
|
+
trackId: alarmSource.trackId,
|
|
41870
|
+
...alarmSource.extraStills !== void 0 && alarmSource.extraStills.length > 0 ? { extraStills: alarmSource.extraStills.map((x) => ({ ...x })) } : {}
|
|
41871
|
+
} : linkedOwners !== void 0 ? {
|
|
41265
41872
|
trackId: linkedOwners.trackId,
|
|
41266
41873
|
...linkedOwners.extraStills.length > 0 ? { extraStills: linkedOwners.extraStills.map((x) => ({ ...x })) } : {},
|
|
41267
41874
|
analysisTiles: linkedOwners.analysisTiles.map((x) => ({ ...x }))
|
|
@@ -42035,7 +42642,8 @@ var NotificationCenter = class {
|
|
|
42035
42642
|
snoozeDecision(input) {
|
|
42036
42643
|
const verdict = snoozeVerdict(input, {
|
|
42037
42644
|
activeFor: (query) => this.snoozes.activeFor(query),
|
|
42038
|
-
ruleFor: (ruleId) => this.rules.get(ruleId)
|
|
42645
|
+
ruleFor: (ruleId) => this.rules.get(ruleId),
|
|
42646
|
+
sequenceActionsOf: this.sequenceActionsOf
|
|
42039
42647
|
});
|
|
42040
42648
|
if (verdict.kind === "exempt") this.logger.info("snooze does not apply — alarm rules are never silenced", {
|
|
42041
42649
|
tags: { deviceId: input.deviceId },
|