@camstack/addon-post-analysis 1.2.322 → 1.2.323
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-8IMzj7jJ.js} +5674 -5032
- package/dist/{clip-model-registry-CwK2SLQD.mjs → clip-model-registry-BENGs4rw.mjs} +5441 -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-CGo6RmK3.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-D5UUhDKa.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +673 -106
- package/dist/pipeline-analytics/index.mjs +673 -106
- 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-8IMzj7jJ.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");
|
|
@@ -945,7 +945,7 @@ function grantMeta(grant) {
|
|
|
945
945
|
...common,
|
|
946
946
|
ruleId: grant.ruleId,
|
|
947
947
|
rule: grant.ruleName,
|
|
948
|
-
|
|
948
|
+
sequenceId: grant.sequenceId
|
|
949
949
|
};
|
|
950
950
|
case "snooze": return {
|
|
951
951
|
...common,
|
|
@@ -5068,6 +5068,27 @@ function planDenseRanges(input) {
|
|
|
5068
5068
|
}
|
|
5069
5069
|
//#endregion
|
|
5070
5070
|
//#region src/notification-center/text-compose.ts
|
|
5071
|
+
/**
|
|
5072
|
+
* The COMPOSITE variables — phrases assembled from catalog fragments before
|
|
5073
|
+
* the one and only render pass.
|
|
5074
|
+
*
|
|
5075
|
+
* A notification body is not a sentence with slots. "person (John) on Front
|
|
5076
|
+
* door in gate" contains three sub-phrases whose SHAPE differs per language
|
|
5077
|
+
* and whose presence is conditional: the label parentheses, the zone clause,
|
|
5078
|
+
* the plural noun. Expressing that with `{{class}}{{label}}{{zones}}` and
|
|
5079
|
+
* three scalars pushes the grammar into the template, where a translator
|
|
5080
|
+
* cannot fix it. So each becomes ONE variable — `{{subject}}`, `{{inZones}}`,
|
|
5081
|
+
* `{{op}}`, `{{detectionSummary}}` — composed here from its own keys and handed
|
|
5082
|
+
* to {@link renderTemplate} as a finished string.
|
|
5083
|
+
*
|
|
5084
|
+
* **Fragments see the SCALARS only.** Never the composites. That is what makes
|
|
5085
|
+
* the depth exactly one and not "one so far": a fragment key edited to say
|
|
5086
|
+
* `{{inZones}}` renders empty rather than starting a second round, and
|
|
5087
|
+
* `renderTemplate` never re-scans what it inserted, so a composite's own value
|
|
5088
|
+
* is printed verbatim whatever it contains.
|
|
5089
|
+
*
|
|
5090
|
+
* Pure: a catalog in, a string out. No clock, no store, no I/O.
|
|
5091
|
+
*/
|
|
5071
5092
|
/** One normalisation, used on both sides of every class lookup — a pipeline
|
|
5072
5093
|
* that emitted `Person` must find the key written `class.person`. */
|
|
5073
5094
|
function normaliseClass(className) {
|
|
@@ -5076,24 +5097,35 @@ function normaliseClass(className) {
|
|
|
5076
5097
|
/**
|
|
5077
5098
|
* The noun for a class, in the count's plural category.
|
|
5078
5099
|
*
|
|
5079
|
-
*
|
|
5080
|
-
*
|
|
5081
|
-
*
|
|
5082
|
-
* detection.
|
|
5100
|
+
* Lookup order: the NC text catalog (`class.*`, plural-aware) → the shared
|
|
5101
|
+
* {@link eventKindLabel} map (viewer filters / EVENT_TAXONOMY) → `class.other`
|
|
5102
|
+
* with the RAW wire id. A custom model class must still appear rather than
|
|
5103
|
+
* vanish; a missing translation is a smaller defect than a missing detection.
|
|
5083
5104
|
*/
|
|
5084
5105
|
function ncClassNoun(texts, className, count) {
|
|
5085
|
-
const
|
|
5106
|
+
const normalised = normaliseClass(className);
|
|
5107
|
+
const key = `class.${normalised}`;
|
|
5086
5108
|
if (texts.has(key)) return texts.text({
|
|
5087
5109
|
key,
|
|
5088
5110
|
count,
|
|
5089
5111
|
vars: { name: className }
|
|
5090
5112
|
});
|
|
5113
|
+
if (require_clip_model_registry.isEventKindLabelKey(normalised)) return require_clip_model_registry.eventKindLabel(normalised, texts.language);
|
|
5091
5114
|
return texts.text({
|
|
5092
5115
|
key: "class.other",
|
|
5093
5116
|
count,
|
|
5094
5117
|
vars: { name: className }
|
|
5095
5118
|
});
|
|
5096
5119
|
}
|
|
5120
|
+
/**
|
|
5121
|
+
* The parenthetical half of a labelled subject: a recognised person or plate
|
|
5122
|
+
* is printed verbatim; a classification (species, COCO sub, …) goes through
|
|
5123
|
+
* the shared event-kind map so "animal (cat)" becomes "animale (Gatto)".
|
|
5124
|
+
*/
|
|
5125
|
+
function ncSubjectLabelText(texts, label, labelKind) {
|
|
5126
|
+
if (labelKind === "identity" || labelKind === "plate") return label;
|
|
5127
|
+
return require_clip_model_registry.eventKindLabel(label, texts.language);
|
|
5128
|
+
}
|
|
5097
5129
|
/** `person (John)` / `persona (John)` — the thing the notification is about. */
|
|
5098
5130
|
function ncSubjectText(texts, input) {
|
|
5099
5131
|
const classNoun = ncClassNoun(texts, input.className, 1);
|
|
@@ -5105,7 +5137,7 @@ function ncSubjectText(texts, input) {
|
|
|
5105
5137
|
key: subjectLabelKey(input.labelKind),
|
|
5106
5138
|
vars: {
|
|
5107
5139
|
classNoun,
|
|
5108
|
-
label: input.label
|
|
5140
|
+
label: ncSubjectLabelText(texts, input.label, input.labelKind)
|
|
5109
5141
|
}
|
|
5110
5142
|
});
|
|
5111
5143
|
}
|
|
@@ -8675,31 +8707,48 @@ function replaceConditionReferences(monitor, condition, reference) {
|
|
|
8675
8707
|
/** The cap whose presence in a sequence makes the rule the alarm's trigger. */
|
|
8676
8708
|
var ALARM_PANEL_CAP$1 = "alarm-panel";
|
|
8677
8709
|
var ALARM_KINDS = new Set(require_clip_model_registry.NC_ALARM_SYSTEM_EVENT_KINDS);
|
|
8678
|
-
function
|
|
8679
|
-
|
|
8710
|
+
function stepActuatesAlarm(step) {
|
|
8711
|
+
return step.kind === "cap" && step.cap === ALARM_PANEL_CAP$1;
|
|
8712
|
+
}
|
|
8713
|
+
function bindingActuatesAlarm(binding, sequenceActionsOf) {
|
|
8714
|
+
if (binding.kind === "do") return stepActuatesAlarm(binding.action);
|
|
8715
|
+
const steps = sequenceActionsOf?.(binding.sequenceId);
|
|
8716
|
+
if (steps === void 0) return false;
|
|
8717
|
+
return steps.some(stepActuatesAlarm);
|
|
8718
|
+
}
|
|
8719
|
+
function actionsActuateAlarm(rule, sequenceActionsOf) {
|
|
8720
|
+
const actions = rule.actions;
|
|
8721
|
+
if (actions === void 0) return false;
|
|
8722
|
+
const hooks = [
|
|
8723
|
+
actions.onActivation,
|
|
8724
|
+
actions.onDeactivation,
|
|
8725
|
+
actions.onTrigger,
|
|
8726
|
+
actions.onTriggerOff
|
|
8727
|
+
];
|
|
8728
|
+
for (const bindings of hooks) for (const binding of bindings ?? []) if (bindingActuatesAlarm(binding, sequenceActionsOf)) return true;
|
|
8729
|
+
for (const button of actions.buttons ?? []) {
|
|
8730
|
+
const steps = sequenceActionsOf?.(button.sequenceId);
|
|
8731
|
+
if (steps !== void 0 && steps.some(stepActuatesAlarm)) return true;
|
|
8732
|
+
}
|
|
8680
8733
|
return false;
|
|
8681
8734
|
}
|
|
8682
8735
|
/**
|
|
8683
8736
|
* May a snooze window silence this rule?
|
|
8684
8737
|
*
|
|
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.
|
|
8738
|
+
* `true` = never. Deliberately answered from the rule alone (plus an optional
|
|
8739
|
+
* sequence lookup): the gate reads it on the delivery path, where a fallible
|
|
8740
|
+
* lookup would be a read that can DESTROY the exemption (D49) — and the
|
|
8741
|
+
* failure direction of this one is silencing an alarm. A missing sequence
|
|
8742
|
+
* lookup means sequence-ref bindings cannot prove exemption; Do bindings and
|
|
8743
|
+
* system-event kinds still can.
|
|
8689
8744
|
*/
|
|
8690
|
-
function isSnoozeExempt(rule) {
|
|
8745
|
+
function isSnoozeExempt(rule, sequenceActionsOf) {
|
|
8691
8746
|
const kinds = rule.conditions.systemEvent?.kinds ?? [];
|
|
8692
8747
|
for (const kind of kinds) if (ALARM_KINDS.has(kind)) return true;
|
|
8693
|
-
return
|
|
8748
|
+
return actionsActuateAlarm(rule, sequenceActionsOf);
|
|
8694
8749
|
}
|
|
8695
8750
|
//#endregion
|
|
8696
8751
|
//#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
8752
|
/**
|
|
8704
8753
|
* Build the notification's buttons.
|
|
8705
8754
|
*
|
|
@@ -8712,18 +8761,18 @@ function buildActionButtons(input) {
|
|
|
8712
8761
|
if (actions === void 0) return [];
|
|
8713
8762
|
const buttons = actions.buttons ?? [];
|
|
8714
8763
|
if (buttons.length === 0) return [];
|
|
8715
|
-
const known =
|
|
8764
|
+
const known = input.knownSequenceIds;
|
|
8716
8765
|
const seen = /* @__PURE__ */ new Set();
|
|
8717
8766
|
const out = [];
|
|
8718
8767
|
for (const button of buttons) {
|
|
8719
|
-
if (!known.has(button.
|
|
8768
|
+
if (!known.has(button.sequenceId)) continue;
|
|
8720
8769
|
if (seen.has(button.id)) continue;
|
|
8721
8770
|
seen.add(button.id);
|
|
8722
8771
|
out.push({
|
|
8723
8772
|
id: button.id,
|
|
8724
8773
|
label: button.label,
|
|
8725
8774
|
url: input.mintUrl({
|
|
8726
|
-
|
|
8775
|
+
sequenceId: button.sequenceId,
|
|
8727
8776
|
actionId: button.id
|
|
8728
8777
|
}),
|
|
8729
8778
|
...button.icon !== void 0 ? { icon: button.icon } : {},
|
|
@@ -8733,18 +8782,17 @@ function buildActionButtons(input) {
|
|
|
8733
8782
|
return out;
|
|
8734
8783
|
}
|
|
8735
8784
|
/**
|
|
8736
|
-
* Which of a rule's buttons name a sequence
|
|
8785
|
+
* Which of a rule's buttons name a sequence that is not known.
|
|
8737
8786
|
*
|
|
8738
8787
|
* Exported so the caller can LOG the drop. A button silently missing from a
|
|
8739
8788
|
* notification is the exact shape of failure this repo keeps paying for — the
|
|
8740
8789
|
* operator authored it, it never appeared, and nothing said why.
|
|
8741
8790
|
*/
|
|
8742
|
-
function unresolvableButtons(actions) {
|
|
8791
|
+
function unresolvableButtons(actions, knownSequenceIds) {
|
|
8743
8792
|
if (actions === void 0) return [];
|
|
8744
8793
|
const buttons = actions.buttons ?? [];
|
|
8745
8794
|
if (buttons.length === 0) return [];
|
|
8746
|
-
|
|
8747
|
-
return buttons.filter((b) => !known.has(b.sequence)).map((b) => `${b.id}→${b.sequence}`);
|
|
8795
|
+
return buttons.filter((b) => !knownSequenceIds.has(b.sequenceId)).map((b) => `${b.id}→${b.sequenceId}`);
|
|
8748
8796
|
}
|
|
8749
8797
|
/** How many snooze choices one notification may carry. The schema caps the
|
|
8750
8798
|
* authored list at 4; this is the same bound applied to a stored rule that
|
|
@@ -8795,7 +8843,7 @@ function snoozeChoices(options) {
|
|
|
8795
8843
|
* attention than the notification did.
|
|
8796
8844
|
*/
|
|
8797
8845
|
function buildSnoozeButtons(input) {
|
|
8798
|
-
if (isSnoozeExempt(input.rule)) return [];
|
|
8846
|
+
if (isSnoozeExempt(input.rule, input.sequenceActionsOf)) return [];
|
|
8799
8847
|
return snoozeChoices(input.rule.snoozeOptions).map((minutes) => {
|
|
8800
8848
|
const actionId = snoozeButtonId(minutes);
|
|
8801
8849
|
return {
|
|
@@ -11084,10 +11132,11 @@ function matchesIdentityScope(subject, scope) {
|
|
|
11084
11132
|
* Derived, never a stored flag: both rule editors prune condition keys to the
|
|
11085
11133
|
* catalog on save, so a new key would be dropped by the first edit.
|
|
11086
11134
|
*/
|
|
11087
|
-
function ruleBelongsToAlarm(rule) {
|
|
11135
|
+
function ruleBelongsToAlarm(rule, sequenceActionsOf) {
|
|
11088
11136
|
return require_clip_model_registry.ruleBelongsToAlarm({
|
|
11089
11137
|
conditions: rule.conditions,
|
|
11090
|
-
...rule.actions
|
|
11138
|
+
...rule.actions !== void 0 ? { actions: rule.actions } : {},
|
|
11139
|
+
...sequenceActionsOf !== void 0 ? { sequenceActionsOf } : {}
|
|
11091
11140
|
});
|
|
11092
11141
|
}
|
|
11093
11142
|
/**
|
|
@@ -12975,7 +13024,7 @@ function deriveTankBand(reading) {
|
|
|
12975
13024
|
case null: return null;
|
|
12976
13025
|
}
|
|
12977
13026
|
}
|
|
12978
|
-
function isRecord$
|
|
13027
|
+
function isRecord$9(value) {
|
|
12979
13028
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12980
13029
|
}
|
|
12981
13030
|
function tankStatusOf(value) {
|
|
@@ -12991,11 +13040,11 @@ function vacuumTanksOf(data) {
|
|
|
12991
13040
|
if (data["capName"] !== "vacuum-control") return null;
|
|
12992
13041
|
const deviceId = data["deviceId"];
|
|
12993
13042
|
const slice = data["slice"];
|
|
12994
|
-
if (typeof deviceId !== "number" || !isRecord$
|
|
13043
|
+
if (typeof deviceId !== "number" || !isRecord$9(slice)) return null;
|
|
12995
13044
|
const tanks = [];
|
|
12996
13045
|
for (const key of NC_TANK_KEYS) {
|
|
12997
13046
|
const raw = slice[key];
|
|
12998
|
-
if (!isRecord$
|
|
13047
|
+
if (!isRecord$9(raw)) continue;
|
|
12999
13048
|
const level = raw["level"];
|
|
13000
13049
|
tanks.push({
|
|
13001
13050
|
key,
|
|
@@ -20333,6 +20382,350 @@ var NcDeviceMuteStore = class {
|
|
|
20333
20382
|
}
|
|
20334
20383
|
};
|
|
20335
20384
|
//#endregion
|
|
20385
|
+
//#region src/notification-center/migrate-legacy-actions.ts
|
|
20386
|
+
function isRecord$8(value) {
|
|
20387
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20388
|
+
}
|
|
20389
|
+
/** A legacy inline sequence row — detected by `name` + `actions` + `enabled`. */
|
|
20390
|
+
function asLegacySequence(value) {
|
|
20391
|
+
const parsed = require_clip_model_registry.NcLegacyRuleActionSequenceSchema.safeParse(value);
|
|
20392
|
+
return parsed.success ? parsed.data : null;
|
|
20393
|
+
}
|
|
20394
|
+
/**
|
|
20395
|
+
* True when `onTrigger` still looks like the pre-shared-sequence array of
|
|
20396
|
+
* named sequences (not an array of `{ kind: 'sequence' | 'do', … }` bindings).
|
|
20397
|
+
*/
|
|
20398
|
+
function onTriggerLooksLegacy(onTrigger) {
|
|
20399
|
+
if (!Array.isArray(onTrigger) || onTrigger.length === 0) return false;
|
|
20400
|
+
const first = onTrigger[0];
|
|
20401
|
+
if (!isRecord$8(first)) return false;
|
|
20402
|
+
if (first["kind"] === "sequence" || first["kind"] === "do") return false;
|
|
20403
|
+
return asLegacySequence(first) !== null;
|
|
20404
|
+
}
|
|
20405
|
+
function legacyButtonSequenceName(button) {
|
|
20406
|
+
if (!isRecord$8(button)) return null;
|
|
20407
|
+
const name = button["sequence"];
|
|
20408
|
+
return typeof name === "string" && name.length > 0 ? name : null;
|
|
20409
|
+
}
|
|
20410
|
+
/**
|
|
20411
|
+
* Rewrite a raw `actions` block. Returns the (possibly unchanged) new-shape
|
|
20412
|
+
* actions and whether anything was materialised into the sequence store.
|
|
20413
|
+
*/
|
|
20414
|
+
async function migrateLegacyRuleActions(actions, createSequence) {
|
|
20415
|
+
if (actions === void 0 || actions === null) return {
|
|
20416
|
+
actions: void 0,
|
|
20417
|
+
migrated: false
|
|
20418
|
+
};
|
|
20419
|
+
if (!isRecord$8(actions)) return {
|
|
20420
|
+
actions: void 0,
|
|
20421
|
+
migrated: false
|
|
20422
|
+
};
|
|
20423
|
+
if (!onTriggerLooksLegacy(actions["onTrigger"]) && actions["sequence"] === void 0) {
|
|
20424
|
+
const buttons = actions["buttons"];
|
|
20425
|
+
if (!(Array.isArray(buttons) && buttons.some((b) => legacyButtonSequenceName(b) !== null))) {
|
|
20426
|
+
const parsed = require_clip_model_registry.NcRuleActionsSchema.safeParse(actions);
|
|
20427
|
+
if (parsed.success) return {
|
|
20428
|
+
actions: parsed.data,
|
|
20429
|
+
migrated: false
|
|
20430
|
+
};
|
|
20431
|
+
return {
|
|
20432
|
+
actions: void 0,
|
|
20433
|
+
migrated: false
|
|
20434
|
+
};
|
|
20435
|
+
}
|
|
20436
|
+
}
|
|
20437
|
+
const legacySequences = [];
|
|
20438
|
+
const onTriggerRaw = actions["onTrigger"];
|
|
20439
|
+
if (Array.isArray(onTriggerRaw)) for (const entry of onTriggerRaw) {
|
|
20440
|
+
const seq = asLegacySequence(entry);
|
|
20441
|
+
if (seq !== null) legacySequences.push(seq);
|
|
20442
|
+
}
|
|
20443
|
+
const nameToId = /* @__PURE__ */ new Map();
|
|
20444
|
+
for (const sequence of legacySequences) {
|
|
20445
|
+
const key = sequence.name.trim();
|
|
20446
|
+
if (key === "" || nameToId.has(key)) continue;
|
|
20447
|
+
const created = await createSequence({
|
|
20448
|
+
name: sequence.name,
|
|
20449
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
20450
|
+
actions: [...sequence.actions]
|
|
20451
|
+
});
|
|
20452
|
+
nameToId.set(key, created.id);
|
|
20453
|
+
}
|
|
20454
|
+
const onTriggerBindings = [];
|
|
20455
|
+
for (const sequence of legacySequences) {
|
|
20456
|
+
if (!sequence.enabled) continue;
|
|
20457
|
+
const id = nameToId.get(sequence.name.trim());
|
|
20458
|
+
if (id === void 0) continue;
|
|
20459
|
+
onTriggerBindings.push({
|
|
20460
|
+
kind: "sequence",
|
|
20461
|
+
sequenceId: id
|
|
20462
|
+
});
|
|
20463
|
+
}
|
|
20464
|
+
const buttonsRaw = actions["buttons"];
|
|
20465
|
+
const buttons = [];
|
|
20466
|
+
if (Array.isArray(buttonsRaw)) for (const button of buttonsRaw) {
|
|
20467
|
+
if (!isRecord$8(button)) continue;
|
|
20468
|
+
const id = typeof button["id"] === "string" ? button["id"] : "";
|
|
20469
|
+
const label = typeof button["label"] === "string" ? button["label"] : "";
|
|
20470
|
+
if (id === "" || label === "") continue;
|
|
20471
|
+
const existingId = typeof button["sequenceId"] === "string" && button["sequenceId"].length > 0 ? button["sequenceId"] : void 0;
|
|
20472
|
+
const legacyName = legacyButtonSequenceName(button);
|
|
20473
|
+
const sequenceId = existingId ?? (legacyName !== null ? nameToId.get(legacyName.trim()) : void 0);
|
|
20474
|
+
if (sequenceId === void 0) continue;
|
|
20475
|
+
buttons.push({
|
|
20476
|
+
id,
|
|
20477
|
+
label,
|
|
20478
|
+
sequenceId,
|
|
20479
|
+
...typeof button["icon"] === "string" ? { icon: button["icon"] } : {},
|
|
20480
|
+
...typeof button["destructive"] === "boolean" ? { destructive: button["destructive"] } : {}
|
|
20481
|
+
});
|
|
20482
|
+
}
|
|
20483
|
+
const next = {
|
|
20484
|
+
...Array.isArray(actions["onActivation"]) ? { onActivation: actions["onActivation"] } : {},
|
|
20485
|
+
...Array.isArray(actions["onDeactivation"]) ? { onDeactivation: actions["onDeactivation"] } : {},
|
|
20486
|
+
...onTriggerBindings.length > 0 ? { onTrigger: onTriggerBindings } : {},
|
|
20487
|
+
...Array.isArray(actions["onTriggerOff"]) ? { onTriggerOff: actions["onTriggerOff"] } : {},
|
|
20488
|
+
...buttons.length > 0 ? { buttons } : {}
|
|
20489
|
+
};
|
|
20490
|
+
const parsed = require_clip_model_registry.NcRuleActionsSchema.safeParse(next);
|
|
20491
|
+
if (!parsed.success) return {
|
|
20492
|
+
actions: void 0,
|
|
20493
|
+
migrated: true
|
|
20494
|
+
};
|
|
20495
|
+
return {
|
|
20496
|
+
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,
|
|
20497
|
+
migrated: true
|
|
20498
|
+
};
|
|
20499
|
+
}
|
|
20500
|
+
//#endregion
|
|
20501
|
+
//#region src/notification-center/sequence-store.ts
|
|
20502
|
+
/**
|
|
20503
|
+
* Shared action sequences for the notification centre.
|
|
20504
|
+
*
|
|
20505
|
+
* A sequence is a named, ordered run of steps (`wait` | `cap`) with its own
|
|
20506
|
+
* throttle. Rules and notification buttons bind to it by id — never by copying
|
|
20507
|
+
* the steps inline — so two rules that open the same gate share one budget and
|
|
20508
|
+
* one authoring surface.
|
|
20509
|
+
*
|
|
20510
|
+
* Mirrors {@link NcDeviceMuteStore}: declare the collection, mirror into RAM,
|
|
20511
|
+
* write-through so a failed persist never leaves a phantom sequence the next
|
|
20512
|
+
* boot will not know.
|
|
20513
|
+
*/
|
|
20514
|
+
/**
|
|
20515
|
+
* @durable class=config owner=notification-center
|
|
20516
|
+
* write="an admin creates/edits/deletes a shared sequence via
|
|
20517
|
+
* `notificationRules.createSequence` / `updateSequence` / `deleteSequence`,
|
|
20518
|
+
* or the one-shot boot migration that materialises legacy inline sequences"
|
|
20519
|
+
* retention="none — a row goes only when an admin deletes it. Bounded by the
|
|
20520
|
+
* sequence set a human is willing to author."
|
|
20521
|
+
*/
|
|
20522
|
+
var NC_SEQUENCES_COLLECTION = "notification-center:sequences";
|
|
20523
|
+
var NC_SEQUENCES_COLUMNS = [
|
|
20524
|
+
{
|
|
20525
|
+
name: "id",
|
|
20526
|
+
type: "TEXT",
|
|
20527
|
+
primaryKey: true,
|
|
20528
|
+
notNull: true
|
|
20529
|
+
},
|
|
20530
|
+
{
|
|
20531
|
+
name: "name",
|
|
20532
|
+
type: "TEXT",
|
|
20533
|
+
notNull: true
|
|
20534
|
+
},
|
|
20535
|
+
{
|
|
20536
|
+
name: "minDelaySec",
|
|
20537
|
+
type: "INTEGER"
|
|
20538
|
+
},
|
|
20539
|
+
{
|
|
20540
|
+
name: "actions",
|
|
20541
|
+
type: "JSON",
|
|
20542
|
+
notNull: true
|
|
20543
|
+
},
|
|
20544
|
+
{
|
|
20545
|
+
name: "createdAt",
|
|
20546
|
+
type: "INTEGER",
|
|
20547
|
+
notNull: true
|
|
20548
|
+
},
|
|
20549
|
+
{
|
|
20550
|
+
name: "updatedAt",
|
|
20551
|
+
type: "INTEGER",
|
|
20552
|
+
notNull: true
|
|
20553
|
+
}
|
|
20554
|
+
];
|
|
20555
|
+
function isRecord$7(value) {
|
|
20556
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20557
|
+
}
|
|
20558
|
+
function parseActions(raw) {
|
|
20559
|
+
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
20560
|
+
const out = [];
|
|
20561
|
+
for (const entry of raw) {
|
|
20562
|
+
const parsed = require_clip_model_registry.NcRuleActionSchema.safeParse(entry);
|
|
20563
|
+
if (!parsed.success) return null;
|
|
20564
|
+
out.push(parsed.data);
|
|
20565
|
+
}
|
|
20566
|
+
return out;
|
|
20567
|
+
}
|
|
20568
|
+
function rowToSequence(data) {
|
|
20569
|
+
const id = data["id"];
|
|
20570
|
+
const name = data["name"];
|
|
20571
|
+
if (typeof id !== "string" || id.length === 0) return null;
|
|
20572
|
+
if (typeof name !== "string" || name.length === 0) return null;
|
|
20573
|
+
const actions = parseActions(data["actions"]);
|
|
20574
|
+
if (actions === null) return null;
|
|
20575
|
+
const minDelayRaw = data["minDelaySec"];
|
|
20576
|
+
const minDelaySec = typeof minDelayRaw === "number" && Number.isInteger(minDelayRaw) ? minDelayRaw : void 0;
|
|
20577
|
+
const candidate = {
|
|
20578
|
+
id,
|
|
20579
|
+
name,
|
|
20580
|
+
...minDelaySec !== void 0 ? { minDelaySec } : {},
|
|
20581
|
+
actions: [...actions]
|
|
20582
|
+
};
|
|
20583
|
+
const parsed = require_clip_model_registry.NcSharedSequenceSchema.safeParse(candidate);
|
|
20584
|
+
return parsed.success ? parsed.data : null;
|
|
20585
|
+
}
|
|
20586
|
+
var NcSequenceStore = class {
|
|
20587
|
+
byId = /* @__PURE__ */ new Map();
|
|
20588
|
+
store;
|
|
20589
|
+
logger;
|
|
20590
|
+
now;
|
|
20591
|
+
constructor(deps) {
|
|
20592
|
+
this.store = deps.store;
|
|
20593
|
+
this.logger = deps.logger;
|
|
20594
|
+
this.now = deps.now ?? (() => Date.now());
|
|
20595
|
+
}
|
|
20596
|
+
static async declare(store) {
|
|
20597
|
+
await store.declareCollection.mutate({
|
|
20598
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20599
|
+
columns: [...NC_SEQUENCES_COLUMNS]
|
|
20600
|
+
});
|
|
20601
|
+
}
|
|
20602
|
+
/**
|
|
20603
|
+
* (Re)hydrate. A failed load KEEPS what is already in memory — dropping every
|
|
20604
|
+
* sequence on a transient store error would leave every button and binding
|
|
20605
|
+
* pointing at nothing until the next successful load.
|
|
20606
|
+
*/
|
|
20607
|
+
async load() {
|
|
20608
|
+
try {
|
|
20609
|
+
const rows = await this.store.query.query({
|
|
20610
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20611
|
+
filter: { limit: 1e4 }
|
|
20612
|
+
});
|
|
20613
|
+
const next = /* @__PURE__ */ new Map();
|
|
20614
|
+
let skipped = 0;
|
|
20615
|
+
for (const row of rows) {
|
|
20616
|
+
if (!isRecord$7(row.data)) {
|
|
20617
|
+
skipped += 1;
|
|
20618
|
+
continue;
|
|
20619
|
+
}
|
|
20620
|
+
const sequence = rowToSequence(row.data);
|
|
20621
|
+
if (sequence === null) {
|
|
20622
|
+
skipped += 1;
|
|
20623
|
+
continue;
|
|
20624
|
+
}
|
|
20625
|
+
next.set(sequence.id, sequence);
|
|
20626
|
+
}
|
|
20627
|
+
this.byId.clear();
|
|
20628
|
+
for (const [id, sequence] of next) this.byId.set(id, sequence);
|
|
20629
|
+
if (skipped > 0) this.logger.warn("sequence rows skipped as malformed — bindings to them will miss", { meta: { skipped } });
|
|
20630
|
+
} catch (err) {
|
|
20631
|
+
this.logger.warn("sequence load failed — keeping the sequences already in memory", { meta: {
|
|
20632
|
+
error: String(err),
|
|
20633
|
+
sequences: this.byId.size
|
|
20634
|
+
} });
|
|
20635
|
+
}
|
|
20636
|
+
}
|
|
20637
|
+
list() {
|
|
20638
|
+
return [...this.byId.values()].toSorted((a, b) => a.name.localeCompare(b.name));
|
|
20639
|
+
}
|
|
20640
|
+
get(sequenceId) {
|
|
20641
|
+
return this.byId.get(sequenceId) ?? null;
|
|
20642
|
+
}
|
|
20643
|
+
/** Hot-path existence check for button minting. */
|
|
20644
|
+
has(sequenceId) {
|
|
20645
|
+
return this.byId.has(sequenceId);
|
|
20646
|
+
}
|
|
20647
|
+
knownIds() {
|
|
20648
|
+
return new Set(this.byId.keys());
|
|
20649
|
+
}
|
|
20650
|
+
async create(input) {
|
|
20651
|
+
const now = this.now();
|
|
20652
|
+
const sequence = {
|
|
20653
|
+
id: (0, node_crypto.randomUUID)(),
|
|
20654
|
+
name: input.name,
|
|
20655
|
+
...input.minDelaySec !== void 0 ? { minDelaySec: input.minDelaySec } : {},
|
|
20656
|
+
actions: [...input.actions]
|
|
20657
|
+
};
|
|
20658
|
+
await this.persist(sequence, now, now);
|
|
20659
|
+
this.byId.set(sequence.id, sequence);
|
|
20660
|
+
return sequence;
|
|
20661
|
+
}
|
|
20662
|
+
/**
|
|
20663
|
+
* Create with a CALLER-chosen id — used by the legacy migration so a rewrite
|
|
20664
|
+
* can name the sequence before rewriting the rule that points at it.
|
|
20665
|
+
*/
|
|
20666
|
+
async createWithId(sequence) {
|
|
20667
|
+
const now = this.now();
|
|
20668
|
+
const stored = {
|
|
20669
|
+
id: sequence.id,
|
|
20670
|
+
name: sequence.name,
|
|
20671
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
20672
|
+
actions: [...sequence.actions]
|
|
20673
|
+
};
|
|
20674
|
+
await this.persist(stored, now, now);
|
|
20675
|
+
this.byId.set(stored.id, stored);
|
|
20676
|
+
return stored;
|
|
20677
|
+
}
|
|
20678
|
+
async update(sequenceId, input) {
|
|
20679
|
+
if (this.byId.get(sequenceId) === void 0) throw new Error(`sequence ${sequenceId} does not exist`);
|
|
20680
|
+
const now = this.now();
|
|
20681
|
+
const sequence = {
|
|
20682
|
+
id: sequenceId,
|
|
20683
|
+
name: input.name,
|
|
20684
|
+
...input.minDelaySec !== void 0 ? { minDelaySec: input.minDelaySec } : {},
|
|
20685
|
+
actions: [...input.actions]
|
|
20686
|
+
};
|
|
20687
|
+
const createdAt = await this.readCreatedAt(sequenceId, now);
|
|
20688
|
+
await this.persist(sequence, createdAt, now);
|
|
20689
|
+
this.byId.set(sequenceId, sequence);
|
|
20690
|
+
return sequence;
|
|
20691
|
+
}
|
|
20692
|
+
async delete(sequenceId) {
|
|
20693
|
+
await this.store.delete.mutate({
|
|
20694
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20695
|
+
key: sequenceId
|
|
20696
|
+
});
|
|
20697
|
+
this.byId.delete(sequenceId);
|
|
20698
|
+
}
|
|
20699
|
+
async readCreatedAt(sequenceId, fallback) {
|
|
20700
|
+
try {
|
|
20701
|
+
const raw = (await this.store.query.query({
|
|
20702
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20703
|
+
filter: {
|
|
20704
|
+
where: { id: sequenceId },
|
|
20705
|
+
limit: 1
|
|
20706
|
+
}
|
|
20707
|
+
}))[0]?.data["createdAt"];
|
|
20708
|
+
return typeof raw === "number" && Number.isFinite(raw) ? raw : fallback;
|
|
20709
|
+
} catch {
|
|
20710
|
+
return fallback;
|
|
20711
|
+
}
|
|
20712
|
+
}
|
|
20713
|
+
async persist(sequence, createdAt, updatedAt) {
|
|
20714
|
+
await this.store.set.mutate({
|
|
20715
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20716
|
+
key: sequence.id,
|
|
20717
|
+
value: {
|
|
20718
|
+
id: sequence.id,
|
|
20719
|
+
name: sequence.name,
|
|
20720
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
20721
|
+
actions: sequence.actions,
|
|
20722
|
+
createdAt,
|
|
20723
|
+
updatedAt
|
|
20724
|
+
}
|
|
20725
|
+
});
|
|
20726
|
+
}
|
|
20727
|
+
};
|
|
20728
|
+
//#endregion
|
|
20336
20729
|
//#region src/notification-center/device-prune.ts
|
|
20337
20730
|
function judgeList(list, isDead) {
|
|
20338
20731
|
const dead = list.filter(isDead);
|
|
@@ -20446,58 +20839,49 @@ function pruneRuleConditions(conditions, isDead, notes) {
|
|
|
20446
20839
|
if (sceneDevice !== void 0 && isDead(sceneDevice)) notes.kept("conditions.scene", [String(sceneDevice)]);
|
|
20447
20840
|
return changed ? next : null;
|
|
20448
20841
|
}
|
|
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
|
|
20842
|
+
/** A Do binding whose device is dead is dropped; sequence refs are left alone
|
|
20843
|
+
* (shared sequences are pruned on their own store, not per-rule). */
|
|
20844
|
+
function pruneHookBindings(bindings, field, isDead, notes) {
|
|
20845
|
+
if (bindings === void 0) return {
|
|
20846
|
+
next: void 0,
|
|
20847
|
+
changed: false
|
|
20470
20848
|
};
|
|
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;
|
|
20849
|
+
const kept = [];
|
|
20476
20850
|
let changed = false;
|
|
20477
|
-
const
|
|
20478
|
-
|
|
20479
|
-
|
|
20480
|
-
const pruned = pruneSequence(sequence, isDead);
|
|
20481
|
-
if (pruned === null) {
|
|
20482
|
-
nextSequences.push(sequence);
|
|
20851
|
+
for (const binding of bindings) {
|
|
20852
|
+
if (binding.kind === "sequence") {
|
|
20853
|
+
kept.push(binding);
|
|
20483
20854
|
continue;
|
|
20484
20855
|
}
|
|
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));
|
|
20856
|
+
if (binding.action.kind === "cap" && isDead(binding.action.deviceId)) {
|
|
20857
|
+
changed = true;
|
|
20858
|
+
notes.removed(field, deviceSubjects([binding.action.deviceId]), "binding-removed");
|
|
20859
|
+
continue;
|
|
20493
20860
|
}
|
|
20861
|
+
kept.push(binding);
|
|
20494
20862
|
}
|
|
20495
|
-
if (!changed) return
|
|
20496
|
-
|
|
20863
|
+
if (!changed) return {
|
|
20864
|
+
next: bindings,
|
|
20865
|
+
changed: false
|
|
20866
|
+
};
|
|
20867
|
+
return {
|
|
20868
|
+
next: kept.length > 0 ? kept : void 0,
|
|
20869
|
+
changed: true
|
|
20870
|
+
};
|
|
20871
|
+
}
|
|
20872
|
+
/** The actions after a device prune — `null` when no binding moved. */
|
|
20873
|
+
function pruneRuleActions(actions, isDead, notes) {
|
|
20874
|
+
const onActivation = pruneHookBindings(actions.onActivation, "actions.onActivation", isDead, notes);
|
|
20875
|
+
const onDeactivation = pruneHookBindings(actions.onDeactivation, "actions.onDeactivation", isDead, notes);
|
|
20876
|
+
const onTrigger = pruneHookBindings(actions.onTrigger, "actions.onTrigger", isDead, notes);
|
|
20877
|
+
const onTriggerOff = pruneHookBindings(actions.onTriggerOff, "actions.onTriggerOff", isDead, notes);
|
|
20878
|
+
if (!onActivation.changed && !onDeactivation.changed && !onTrigger.changed && !onTriggerOff.changed) return null;
|
|
20497
20879
|
return {
|
|
20498
|
-
...
|
|
20499
|
-
|
|
20500
|
-
...
|
|
20880
|
+
...onActivation.next !== void 0 ? { onActivation: [...onActivation.next] } : {},
|
|
20881
|
+
...onDeactivation.next !== void 0 ? { onDeactivation: [...onDeactivation.next] } : {},
|
|
20882
|
+
...onTrigger.next !== void 0 ? { onTrigger: [...onTrigger.next] } : {},
|
|
20883
|
+
...onTriggerOff.next !== void 0 ? { onTriggerOff: [...onTriggerOff.next] } : {},
|
|
20884
|
+
...actions.buttons !== void 0 ? { buttons: actions.buttons } : {}
|
|
20501
20885
|
};
|
|
20502
20886
|
}
|
|
20503
20887
|
function planRuleDevices(rule, isDead, guard) {
|
|
@@ -24732,6 +25116,10 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
|
|
|
24732
25116
|
return {
|
|
24733
25117
|
camera: deviceName,
|
|
24734
25118
|
class: detection?.className ?? (systemEvent !== void 0 ? "" : subject.className),
|
|
25119
|
+
classLabel: (() => {
|
|
25120
|
+
const wire = detection?.className ?? (systemEvent !== void 0 ? "" : subject.className);
|
|
25121
|
+
return wire.length === 0 ? "" : require_clip_model_registry.eventKindLabel(wire, texts.language);
|
|
25122
|
+
})(),
|
|
24735
25123
|
label: detection?.label ?? subject.label ?? "",
|
|
24736
25124
|
zones: zoneLabels.join(", "),
|
|
24737
25125
|
subject: detection?.className !== void 0 ? ncSubjectText(texts, {
|
|
@@ -28873,9 +29261,32 @@ function lastRunRow(data) {
|
|
|
28873
29261
|
}
|
|
28874
29262
|
//#endregion
|
|
28875
29263
|
//#region src/notification-center/rule-actions.ts
|
|
29264
|
+
function wrapDo(action) {
|
|
29265
|
+
const name = action.kind === "wait" ? `wait ${String(action.seconds)}s` : `${action.cap}.${action.method}`;
|
|
29266
|
+
return {
|
|
29267
|
+
id: `do:${name}`,
|
|
29268
|
+
name,
|
|
29269
|
+
enabled: true,
|
|
29270
|
+
actions: [action],
|
|
29271
|
+
throttle: false
|
|
29272
|
+
};
|
|
29273
|
+
}
|
|
29274
|
+
function resolveBinding(binding, lookup) {
|
|
29275
|
+
if (binding.kind === "do") return wrapDo(binding.action);
|
|
29276
|
+
const sequence = lookup(binding.sequenceId);
|
|
29277
|
+
if (sequence === null) return { missing: binding.sequenceId };
|
|
29278
|
+
return {
|
|
29279
|
+
id: sequence.id,
|
|
29280
|
+
name: sequence.name,
|
|
29281
|
+
enabled: true,
|
|
29282
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
29283
|
+
actions: sequence.actions,
|
|
29284
|
+
throttle: true
|
|
29285
|
+
};
|
|
29286
|
+
}
|
|
28876
29287
|
var NcRuleActionRunner = class {
|
|
28877
29288
|
deps;
|
|
28878
|
-
/** Last run per
|
|
29289
|
+
/** Last run per `sequenceId` — the throttle's memory (shared across rules). */
|
|
28879
29290
|
lastRunAt = /* @__PURE__ */ new Map();
|
|
28880
29291
|
now;
|
|
28881
29292
|
sleep;
|
|
@@ -28885,11 +29296,49 @@ var NcRuleActionRunner = class {
|
|
|
28885
29296
|
this.sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
28886
29297
|
}
|
|
28887
29298
|
/**
|
|
28888
|
-
*
|
|
29299
|
+
* Resolve bindings via the sequence store and run each, in order.
|
|
28889
29300
|
*
|
|
28890
29301
|
* Never throws: an actuation that fails must not cost the NOTIFICATION about
|
|
28891
|
-
* the very same event.
|
|
28892
|
-
|
|
29302
|
+
* the very same event.
|
|
29303
|
+
*/
|
|
29304
|
+
async runBindings(input) {
|
|
29305
|
+
const sequences = [];
|
|
29306
|
+
const missing = [];
|
|
29307
|
+
for (const binding of input.bindings) {
|
|
29308
|
+
const resolved = resolveBinding(binding, input.lookup);
|
|
29309
|
+
if ("missing" in resolved) {
|
|
29310
|
+
this.deps.logger.warn("rule action binding skipped — sequence missing", {
|
|
29311
|
+
tags: { deviceId: input.deviceId },
|
|
29312
|
+
meta: {
|
|
29313
|
+
ruleId: input.ruleId,
|
|
29314
|
+
sequenceId: resolved.missing
|
|
29315
|
+
}
|
|
29316
|
+
});
|
|
29317
|
+
missing.push({
|
|
29318
|
+
id: resolved.missing,
|
|
29319
|
+
name: resolved.missing,
|
|
29320
|
+
ran: false,
|
|
29321
|
+
stepsRun: 0,
|
|
29322
|
+
skippedReason: "missing"
|
|
29323
|
+
});
|
|
29324
|
+
continue;
|
|
29325
|
+
}
|
|
29326
|
+
sequences.push(resolved);
|
|
29327
|
+
}
|
|
29328
|
+
const ran = await this.run({
|
|
29329
|
+
ruleId: input.ruleId,
|
|
29330
|
+
ruleName: input.ruleName,
|
|
29331
|
+
deviceId: input.deviceId,
|
|
29332
|
+
sequences,
|
|
29333
|
+
...input.origin !== void 0 ? { origin: input.origin } : {}
|
|
29334
|
+
});
|
|
29335
|
+
return [...missing, ...ran];
|
|
29336
|
+
}
|
|
29337
|
+
/**
|
|
29338
|
+
* Run every enabled sequence, in order.
|
|
29339
|
+
*
|
|
29340
|
+
* Prefer {@link runBindings} at call sites that hold bindings. This remains
|
|
29341
|
+
* for button grants (force-enabled shared sequence) and tests.
|
|
28893
29342
|
*/
|
|
28894
29343
|
async run(input) {
|
|
28895
29344
|
const out = [];
|
|
@@ -28898,38 +29347,41 @@ var NcRuleActionRunner = class {
|
|
|
28898
29347
|
}
|
|
28899
29348
|
async runOne(input, sequence) {
|
|
28900
29349
|
if (!sequence.enabled) return {
|
|
29350
|
+
id: sequence.id,
|
|
28901
29351
|
name: sequence.name,
|
|
28902
29352
|
ran: false,
|
|
28903
29353
|
stepsRun: 0,
|
|
28904
29354
|
skippedReason: "disabled"
|
|
28905
29355
|
};
|
|
28906
29356
|
if (sequence.actions.length === 0) return {
|
|
29357
|
+
id: sequence.id,
|
|
28907
29358
|
name: sequence.name,
|
|
28908
29359
|
ran: false,
|
|
28909
29360
|
stepsRun: 0,
|
|
28910
29361
|
skippedReason: "empty"
|
|
28911
29362
|
};
|
|
28912
|
-
const key = `${input.ruleId}:${sequence.name}`;
|
|
28913
29363
|
const now = this.now();
|
|
28914
|
-
const minDelayMs = (sequence.minDelaySec ?? 0) * 1e3;
|
|
28915
|
-
const last = this.lastRunAt.get(
|
|
29364
|
+
const minDelayMs = sequence.throttle ? (sequence.minDelaySec ?? 0) * 1e3 : 0;
|
|
29365
|
+
const last = this.lastRunAt.get(sequence.id);
|
|
28916
29366
|
if (minDelayMs > 0 && last !== void 0 && now - last < minDelayMs) {
|
|
28917
29367
|
this.deps.logger.debug("rule action sequence throttled", {
|
|
28918
29368
|
tags: { deviceId: input.deviceId },
|
|
28919
29369
|
meta: {
|
|
28920
29370
|
ruleId: input.ruleId,
|
|
29371
|
+
sequenceId: sequence.id,
|
|
28921
29372
|
sequence: sequence.name,
|
|
28922
29373
|
sinceLastMs: now - last
|
|
28923
29374
|
}
|
|
28924
29375
|
});
|
|
28925
29376
|
return {
|
|
29377
|
+
id: sequence.id,
|
|
28926
29378
|
name: sequence.name,
|
|
28927
29379
|
ran: false,
|
|
28928
29380
|
stepsRun: 0,
|
|
28929
29381
|
skippedReason: "throttled"
|
|
28930
29382
|
};
|
|
28931
29383
|
}
|
|
28932
|
-
this.lastRunAt.set(
|
|
29384
|
+
if (sequence.throttle) this.lastRunAt.set(sequence.id, now);
|
|
28933
29385
|
let stepsRun = 0;
|
|
28934
29386
|
for (const [index, action] of sequence.actions.entries()) try {
|
|
28935
29387
|
await this.step(action, input.origin);
|
|
@@ -28940,6 +29392,7 @@ var NcRuleActionRunner = class {
|
|
|
28940
29392
|
meta: {
|
|
28941
29393
|
ruleId: input.ruleId,
|
|
28942
29394
|
rule: input.ruleName,
|
|
29395
|
+
sequenceId: sequence.id,
|
|
28943
29396
|
sequence: sequence.name,
|
|
28944
29397
|
step: index,
|
|
28945
29398
|
action: action.kind === "cap" ? `${action.cap}.${action.method}` : "wait",
|
|
@@ -28947,6 +29400,7 @@ var NcRuleActionRunner = class {
|
|
|
28947
29400
|
}
|
|
28948
29401
|
});
|
|
28949
29402
|
return {
|
|
29403
|
+
id: sequence.id,
|
|
28950
29404
|
name: sequence.name,
|
|
28951
29405
|
ran: true,
|
|
28952
29406
|
stepsRun,
|
|
@@ -28958,11 +29412,13 @@ var NcRuleActionRunner = class {
|
|
|
28958
29412
|
meta: {
|
|
28959
29413
|
ruleId: input.ruleId,
|
|
28960
29414
|
rule: input.ruleName,
|
|
29415
|
+
sequenceId: sequence.id,
|
|
28961
29416
|
sequence: sequence.name,
|
|
28962
29417
|
steps: stepsRun
|
|
28963
29418
|
}
|
|
28964
29419
|
});
|
|
28965
29420
|
return {
|
|
29421
|
+
id: sequence.id,
|
|
28966
29422
|
name: sequence.name,
|
|
28967
29423
|
ran: true,
|
|
28968
29424
|
stepsRun
|
|
@@ -28983,6 +29439,17 @@ var NcRuleActionRunner = class {
|
|
|
28983
29439
|
});
|
|
28984
29440
|
}
|
|
28985
29441
|
};
|
|
29442
|
+
/** Force-enable a shared sequence for a button tap (always runs, still throttled). */
|
|
29443
|
+
function asForcedSequence(sequence) {
|
|
29444
|
+
return {
|
|
29445
|
+
id: sequence.id,
|
|
29446
|
+
name: sequence.name,
|
|
29447
|
+
enabled: true,
|
|
29448
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
29449
|
+
actions: sequence.actions,
|
|
29450
|
+
throttle: true
|
|
29451
|
+
};
|
|
29452
|
+
}
|
|
28986
29453
|
//#endregion
|
|
28987
29454
|
//#region src/notification-center/rule-ai-analyst.ts
|
|
28988
29455
|
/**
|
|
@@ -30829,7 +31296,7 @@ function snoozeVerdict(input, deps) {
|
|
|
30829
31296
|
});
|
|
30830
31297
|
if (active === null) return { kind: "clear" };
|
|
30831
31298
|
const rule = deps.ruleFor(input.ruleId);
|
|
30832
|
-
if (rule !== null && isSnoozeExempt(rule)) return {
|
|
31299
|
+
if (rule !== null && isSnoozeExempt(rule, deps.sequenceActionsOf)) return {
|
|
30833
31300
|
kind: "exempt",
|
|
30834
31301
|
snooze: active
|
|
30835
31302
|
};
|
|
@@ -36941,6 +37408,8 @@ var NotificationCenter = class {
|
|
|
36941
37408
|
snoozes;
|
|
36942
37409
|
/** The permanent per-camera notification off switch — see `device-mute-store.ts`. */
|
|
36943
37410
|
deviceMutes;
|
|
37411
|
+
/** Shared action sequences rules and buttons bind to by id. */
|
|
37412
|
+
sequences;
|
|
36944
37413
|
/** The durable episode rows behind the sustained-state alerts. */
|
|
36945
37414
|
alertStore;
|
|
36946
37415
|
/** Battery band → notification, and the reminder sweep. */
|
|
@@ -37179,7 +37648,8 @@ var NotificationCenter = class {
|
|
|
37179
37648
|
const drafts = [];
|
|
37180
37649
|
const sequenceButtons = this.actionRunner === null || stored === null ? [] : buildActionButtons({
|
|
37181
37650
|
actions: stored.actions,
|
|
37182
|
-
|
|
37651
|
+
knownSequenceIds: this.sequences.knownIds(),
|
|
37652
|
+
mintUrl: ({ sequenceId, actionId }) => {
|
|
37183
37653
|
const id = (0, node_crypto.randomUUID)();
|
|
37184
37654
|
drafts.push({
|
|
37185
37655
|
id,
|
|
@@ -37189,7 +37659,7 @@ var NotificationCenter = class {
|
|
|
37189
37659
|
id,
|
|
37190
37660
|
ruleId: input.ruleId,
|
|
37191
37661
|
ruleName: input.ruleName,
|
|
37192
|
-
|
|
37662
|
+
sequenceId,
|
|
37193
37663
|
deviceId: input.deviceId,
|
|
37194
37664
|
actionId,
|
|
37195
37665
|
targetId: input.targetId,
|
|
@@ -37201,6 +37671,7 @@ var NotificationCenter = class {
|
|
|
37201
37671
|
});
|
|
37202
37672
|
const snoozeButtons = input.ownerUserId === void 0 || stored === null ? [] : buildSnoozeButtons({
|
|
37203
37673
|
rule: stored,
|
|
37674
|
+
sequenceActionsOf: this.sequenceActionsOf,
|
|
37204
37675
|
label: (minutes) => this.snoozeButtonLabel(minutes),
|
|
37205
37676
|
mintUrl: ({ actionId, minutes }) => {
|
|
37206
37677
|
const id = (0, node_crypto.randomUUID)();
|
|
@@ -37441,7 +37912,7 @@ var NotificationCenter = class {
|
|
|
37441
37912
|
*/
|
|
37442
37913
|
async applyGrantedSnooze(grant) {
|
|
37443
37914
|
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`);
|
|
37915
|
+
if (rule !== null && isSnoozeExempt(rule, this.sequenceActionsOf)) throw new Error(`"${grant.ruleName}" is an alarm rule and cannot be snoozed`);
|
|
37445
37916
|
const snooze = await this.snoozes.create({
|
|
37446
37917
|
userId: grant.userId,
|
|
37447
37918
|
scope: "device",
|
|
@@ -37740,16 +38211,13 @@ var NotificationCenter = class {
|
|
|
37740
38211
|
if (this.actionRunner === null) throw new Error("no actuator is wired on this node");
|
|
37741
38212
|
const rule = this.rules.get(grant.ruleId);
|
|
37742
38213
|
if (rule === null) throw new Error(`rule ${grant.ruleId} no longer exists`);
|
|
37743
|
-
const sequence =
|
|
37744
|
-
if (sequence ===
|
|
38214
|
+
const sequence = this.sequences.get(grant.sequenceId);
|
|
38215
|
+
if (sequence === null) throw new Error(`sequence "${grant.sequenceId}" no longer exists`);
|
|
37745
38216
|
const [outcome] = await this.actionRunner.run({
|
|
37746
38217
|
ruleId: rule.id,
|
|
37747
38218
|
ruleName: rule.name,
|
|
37748
38219
|
deviceId: grant.deviceId,
|
|
37749
|
-
sequences: [
|
|
37750
|
-
...sequence,
|
|
37751
|
-
enabled: true
|
|
37752
|
-
}]
|
|
38220
|
+
sequences: [asForcedSequence(sequence)]
|
|
37753
38221
|
});
|
|
37754
38222
|
if (outcome === void 0) throw new Error("the sequence did not run");
|
|
37755
38223
|
if (outcome.failedAt !== void 0) throw new Error(`step ${String(outcome.failedAt)} of "${sequence.name}" failed`);
|
|
@@ -37868,6 +38336,11 @@ var NotificationCenter = class {
|
|
|
37868
38336
|
logger: this.logger.child("device-mute"),
|
|
37869
38337
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
37870
38338
|
});
|
|
38339
|
+
this.sequences = new NcSequenceStore({
|
|
38340
|
+
store: deps.store,
|
|
38341
|
+
logger: this.logger.child("sequences"),
|
|
38342
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
38343
|
+
});
|
|
37871
38344
|
this.alertStore = new NcAlertStore({
|
|
37872
38345
|
store: deps.store,
|
|
37873
38346
|
logger: this.logger.child("device-alerts")
|
|
@@ -38134,6 +38607,7 @@ var NotificationCenter = class {
|
|
|
38134
38607
|
await NcStateSummaryService.declare(store);
|
|
38135
38608
|
await NcSnoozeStore.declare(store);
|
|
38136
38609
|
await NcDeviceMuteStore.declare(store);
|
|
38610
|
+
await NcSequenceStore.declare(store);
|
|
38137
38611
|
await TimelapseStore.declare(store);
|
|
38138
38612
|
await NcSummaryStore.declare(store);
|
|
38139
38613
|
await NcCalendarEventsStore.declare(store);
|
|
@@ -38195,6 +38669,8 @@ var NotificationCenter = class {
|
|
|
38195
38669
|
sinceStartMs: now - t0
|
|
38196
38670
|
} });
|
|
38197
38671
|
};
|
|
38672
|
+
await timed("sequences", () => this.sequences.load());
|
|
38673
|
+
await timed("migrateLegacyActions", () => this.migrateLegacyRuleActions());
|
|
38198
38674
|
await Promise.all([
|
|
38199
38675
|
timed("texts", () => this.reloadTexts()),
|
|
38200
38676
|
timed("rules", () => this.rules.load()),
|
|
@@ -39494,7 +39970,32 @@ var NotificationCenter = class {
|
|
|
39494
39970
|
return { success: true };
|
|
39495
39971
|
},
|
|
39496
39972
|
setRuleEnabled: async ({ ruleId, enabled }) => {
|
|
39497
|
-
await this.rules.setEnabled(ruleId, enabled);
|
|
39973
|
+
const updated = await this.rules.setEnabled(ruleId, enabled);
|
|
39974
|
+
const bindings = enabled ? updated.actions?.onActivation : updated.actions?.onDeactivation;
|
|
39975
|
+
if (bindings !== void 0 && bindings.length > 0 && this.actionRunner !== null) this.actionRunner.runBindings({
|
|
39976
|
+
ruleId: updated.id,
|
|
39977
|
+
ruleName: updated.name,
|
|
39978
|
+
deviceId: this.deviceIdForRuleActions(updated),
|
|
39979
|
+
bindings,
|
|
39980
|
+
lookup: (id) => this.sequences.get(id)
|
|
39981
|
+
});
|
|
39982
|
+
return { success: true };
|
|
39983
|
+
},
|
|
39984
|
+
listSequences: async () => ({ sequences: [...this.sequences.list()] }),
|
|
39985
|
+
getSequence: async ({ sequenceId }) => ({ sequence: this.sequences.get(sequenceId) }),
|
|
39986
|
+
createSequence: async ({ sequence }) => {
|
|
39987
|
+
const created = await this.sequences.create(sequence);
|
|
39988
|
+
this.logger.info("notification sequence created", { meta: { sequenceId: created.id } });
|
|
39989
|
+
return { sequence: created };
|
|
39990
|
+
},
|
|
39991
|
+
updateSequence: async ({ sequenceId, sequence }) => {
|
|
39992
|
+
const updated = await this.sequences.update(sequenceId, sequence);
|
|
39993
|
+
this.logger.info("notification sequence updated", { meta: { sequenceId } });
|
|
39994
|
+
return { sequence: updated };
|
|
39995
|
+
},
|
|
39996
|
+
deleteSequence: async ({ sequenceId }) => {
|
|
39997
|
+
await this.sequences.delete(sequenceId);
|
|
39998
|
+
this.logger.info("notification sequence deleted", { meta: { sequenceId } });
|
|
39498
39999
|
return { success: true };
|
|
39499
40000
|
},
|
|
39500
40001
|
testRule: async ({ rule, lookbackMinutes }) => ({ results: [...await this.dryRun(rule, lookbackMinutes)] }),
|
|
@@ -40991,7 +41492,7 @@ var NotificationCenter = class {
|
|
|
40991
41492
|
} : {}
|
|
40992
41493
|
}
|
|
40993
41494
|
});
|
|
40994
|
-
const unresolvable = unresolvableButtons(rule.actions);
|
|
41495
|
+
const unresolvable = unresolvableButtons(rule.actions, this.sequences.knownIds());
|
|
40995
41496
|
if (unresolvable.length > 0) this.logger.warn("rule buttons reference sequences that do not exist — not sent", {
|
|
40996
41497
|
tags: { deviceId: subject.deviceId },
|
|
40997
41498
|
meta: {
|
|
@@ -41118,20 +41619,85 @@ var NotificationCenter = class {
|
|
|
41118
41619
|
return this.groupStore.list(query);
|
|
41119
41620
|
}
|
|
41120
41621
|
/**
|
|
41121
|
-
*
|
|
41622
|
+
* Materialise legacy inline `onTrigger` sequences into the shared store and
|
|
41623
|
+
* rewrite each rule's actions to bindings + `buttons.sequenceId`. Runs on RAW
|
|
41624
|
+
* rows before {@link NcRuleStore.load} so Zod never skips them.
|
|
41625
|
+
*/
|
|
41626
|
+
async migrateLegacyRuleActions() {
|
|
41627
|
+
let rows;
|
|
41628
|
+
try {
|
|
41629
|
+
rows = await this.deps.store.query.query({
|
|
41630
|
+
collection: NC_RULES_COLLECTION,
|
|
41631
|
+
filter: { limit: 1e4 }
|
|
41632
|
+
});
|
|
41633
|
+
} catch (err) {
|
|
41634
|
+
this.logger.warn("legacy actions migration skipped — could not read rules", { meta: { error: String(err) } });
|
|
41635
|
+
return;
|
|
41636
|
+
}
|
|
41637
|
+
let migrated = 0;
|
|
41638
|
+
for (const row of rows) {
|
|
41639
|
+
const ruleBlob = row.data["rule"];
|
|
41640
|
+
if (typeof ruleBlob !== "object" || ruleBlob === null || Array.isArray(ruleBlob)) continue;
|
|
41641
|
+
const rule = { ...ruleBlob };
|
|
41642
|
+
const ruleId = typeof rule["id"] === "string" ? rule["id"] : void 0;
|
|
41643
|
+
if (ruleId === void 0) continue;
|
|
41644
|
+
const result = await migrateLegacyRuleActions(rule["actions"], async (input) => {
|
|
41645
|
+
return { id: (await this.sequences.create(input)).id };
|
|
41646
|
+
});
|
|
41647
|
+
if (!result.migrated) continue;
|
|
41648
|
+
if (result.actions !== void 0) rule["actions"] = result.actions;
|
|
41649
|
+
else delete rule["actions"];
|
|
41650
|
+
const { id: _rowId, ...columns } = row.data;
|
|
41651
|
+
await this.deps.store.set.mutate({
|
|
41652
|
+
collection: NC_RULES_COLLECTION,
|
|
41653
|
+
key: ruleId,
|
|
41654
|
+
value: {
|
|
41655
|
+
...columns,
|
|
41656
|
+
rule
|
|
41657
|
+
}
|
|
41658
|
+
});
|
|
41659
|
+
migrated += 1;
|
|
41660
|
+
}
|
|
41661
|
+
if (migrated > 0) this.logger.info("migrated legacy inline rule actions into shared sequences", { meta: {
|
|
41662
|
+
rules: migrated,
|
|
41663
|
+
sequences: this.sequences.list().length
|
|
41664
|
+
} });
|
|
41665
|
+
}
|
|
41666
|
+
/**
|
|
41667
|
+
* True when this match is a clear / leave edge — the subject for
|
|
41668
|
+
* `onTriggerOff`. Raise / enter / ordinary matches use `onTrigger`.
|
|
41669
|
+
*/
|
|
41670
|
+
isTriggerOffSubject(subject) {
|
|
41671
|
+
if (subject.sensorRaised === false) return true;
|
|
41672
|
+
if (subject.occupancy !== void 0 && subject.occupancy.occupied === false) return true;
|
|
41673
|
+
return false;
|
|
41674
|
+
}
|
|
41675
|
+
/** Device id for activation/deactivation log tags — first scoped device, else 0. */
|
|
41676
|
+
deviceIdForRuleActions(rule) {
|
|
41677
|
+
const devices = rule.conditions.devices;
|
|
41678
|
+
if (devices !== void 0 && devices.length > 0) {
|
|
41679
|
+
const first = devices[0];
|
|
41680
|
+
if (first !== void 0) return first;
|
|
41681
|
+
}
|
|
41682
|
+
return 0;
|
|
41683
|
+
}
|
|
41684
|
+
sequenceActionsOf = (sequenceId) => this.sequences.get(sequenceId)?.actions;
|
|
41685
|
+
/**
|
|
41686
|
+
* Run this rule's onTrigger / onTriggerOff bindings for this match.
|
|
41122
41687
|
*
|
|
41123
41688
|
* Called BEFORE the cooldown gate — see the call site for why. Everything
|
|
41124
41689
|
* here is fire-and-forget and nothing may throw into the evaluation.
|
|
41125
41690
|
*/
|
|
41126
41691
|
runRuleActions(rule, subject, kind, controls) {
|
|
41127
|
-
const sequences = rule.actions?.onTrigger;
|
|
41128
|
-
if (sequences === void 0 || sequences.length === 0) return;
|
|
41129
41692
|
if (this.actionRunner === null || controls.runActions === false) return;
|
|
41130
|
-
this.
|
|
41693
|
+
const bindings = this.isTriggerOffSubject(subject) ? rule.actions?.onTriggerOff : rule.actions?.onTrigger;
|
|
41694
|
+
if (bindings === void 0 || bindings.length === 0) return;
|
|
41695
|
+
this.actionRunner.runBindings({
|
|
41131
41696
|
ruleId: rule.id,
|
|
41132
41697
|
ruleName: rule.name,
|
|
41133
41698
|
deviceId: subject.deviceId,
|
|
41134
|
-
|
|
41699
|
+
bindings,
|
|
41700
|
+
lookup: (id) => this.sequences.get(id),
|
|
41135
41701
|
origin: fireRefFor(rule, subject, kind)
|
|
41136
41702
|
});
|
|
41137
41703
|
}
|
|
@@ -42035,7 +42601,8 @@ var NotificationCenter = class {
|
|
|
42035
42601
|
snoozeDecision(input) {
|
|
42036
42602
|
const verdict = snoozeVerdict(input, {
|
|
42037
42603
|
activeFor: (query) => this.snoozes.activeFor(query),
|
|
42038
|
-
ruleFor: (ruleId) => this.rules.get(ruleId)
|
|
42604
|
+
ruleFor: (ruleId) => this.rules.get(ruleId),
|
|
42605
|
+
sequenceActionsOf: this.sequenceActionsOf
|
|
42039
42606
|
});
|
|
42040
42607
|
if (verdict.kind === "exempt") this.logger.info("snooze does not apply — alarm rules are never silenced", {
|
|
42041
42608
|
tags: { deviceId: input.deviceId },
|