@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as NC_ALARM_DELAY_MAX_SEC, $n as
|
|
1
|
+
import { $ as NC_ALARM_DELAY_MAX_SEC, $n as gifFromMp4Args, $r as DeviceType, $t as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, A as EMBEDDING_REQUIREMENTS_FAILED, An as audioMetricsCapability, Ar as ruleDeviceCompatCapQueries, At as NcRulePatchSchema, B as ListTrackRelationsInputSchema, Bn as describeSystemEventFamilyProblem, Br as systemEventDeliveryProblems, Bt as NcSystemEventKindSchema, C as DEFAULT_EVENT_COLOR, Ci as string, Cn as alarmDeviceSecureOnArm, Cr as readTimelapseGeneratedAt, Ct as NcAlarmSettingsPatchSchema, D as DeclaredDevices, Di as unknown, Dn as applyNonBlockingEdit, Dr as resolveModeDelays, Dt as NcRuleActionSchema, E as DETECTION_PIPELINE_CAP_NAME, Ei as union, En as applyModeDelaysEdits, Er as resolveLocationMode, Et as NcLegacyRuleActionSequenceSchema, F as EXPORT_DENSE_MAX_RANGES, Fn as cosineSimilarity$1, Fr as sensorRaisedOf, Ft as NcSharedSequenceSchema, G as MAX_BIRTH_LATENCY_PROXY_MS, Gr as templateCarriesSummary, Gt as OPS_LOG_DEFAULT_LIMIT, H as LockStateSchema, Hr as systemEventFilterApplies, Ht as NcTemplatePreviewInputSchema, I as FIRST_LEVEL_MACRO_CLASSES, In as createAlarmProfile, Ir as servedTemplateVars, It as NcSnoozeInputSchema, J as MOTION_CLOSE_AFTER_MS, Jn as eventKindLabel, Jr as zoneAnalyticsCapability, Jt as RECORDING_EXPORT_MAX_READ_BYTES, K as MAX_TRACK_PURITIES, Kn as encodeVectorBase64, Kr as templateVarsFor, Kt as OpsLogEntrySchema, L as FULL_IMAGE_BBOX, Ln as deleteAlarmProfile, Lr as storageOccupancyCapability, Lt as NcSnoozeSchema, M as EVENT_DENSITY_BATCH_MAX, Mn as buildEventKindDescriptor, Mr as ruleKindOf, Mt as NcRuleTargetSchema, N as EVENT_KIND_BY_CAP, Nn as cloneAlarmSettings, Nr as sceneMonitorCapability, Nt as NcRuleUpdateInputSchema, O as DetectionSourceSchema, Oi as EventCategory, On as applySecureOnArmEdit, Ot as NcRuleActionsSchema, P as EVENT_OWNER_TYPES, Pn as consumableLowPercentOf, Pr as secureActuationFor, Pt as NcScheduleSchema, Q as NC_ALARM_DELAY_FIELDS, Qn as gifDeriveSpeed, Qr as isSourceCap, Qt as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, R as FailureCounters, Rn as deriveRecordingMode, Rr as subKindsOf, Rt as NcSnoozeSuppressedSchema, S as CoverStateSchema, Si as strictObject, Sn as addonWidgetsSourceCapability, Sr as readMotionTriggeredBy, St as NcAlarmSensorsSchema, T as DETECTION_MACRO_CLASSES, Ti as tuple, Tn as applyBlockingEdit, Tr as resolveClipSearchMinScore, Tt as NcConditionsSchema, U as MAX_ARCHIVED_DEBUG_NOTES, Un as embeddingRefusalOf, Ur as systemEventSummaryScheduleOf, Ut as NcTemplatePreviewSchema, V as ListTrackRelationsResultSchema, Vr as systemEventFamilyProblems, Vt as NcTaxonomySchema, W as MAX_BIRTH_DECISION_RECORDS, Wr as systemEventSummarySplitOf, Wt as NcTemplateVarDescriptorSchema, X as MotionTriggeredBySchema, Xn as faceGalleryCapability, Xr as canonicalHash, Xt as RebuildObjectEmbeddingsRefusalSchema, Y as MediaFileKindEnum, Yn as evictionPolicyOfLocation, Yr as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, Yt as RECORDING_SOURCE_CAMSTACK_ADDON, Z as NC_ALARM_DEFAULT_PROFILE_ID, Zn as failureContributionCapability, Zr as evaluateSensorEdge, Zt as RetrainStatusSchema, _ as BaseDevice, _i as looseObject, _n as TrackRelationKindSchema, _r as occupancyScope, _t as NC_TANK_SYSTEM_EVENT_KINDS, a as CLIP_EMBEDDING_STEP_ID, ai as defineCustomActions, an as SCENE_DIVERGED, ar as isEventOwnerType, at as NC_CLIP_PRE_ROLL_DEFAULT_SEC, b as ClearTrackRelationInputSchema, bi as partialRecord, bn as TrackSourceSchema, br as plateGalleryCapability, bt as NcAlarmConfigSchema, c as resolveClusterModelPin, ci as nodePin, cn as SetTrackPurityInputSchema, cr as isRetiredClearKey, ct as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, d as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, di as ZodObject, dn as TIMELAPSE_DENSE_FLOOR_SEC, dr as isSystemEventSummaryRule, dt as NC_GROUPING_DEFAULT_SEC, ei as errMsg, en as SCENE_DEFAULT_ANCHOR_THRESHOLD, er as isAlarmControllableCap, et as NC_ALARM_SYSTEM_EVENT_KINDS, f as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, fi as _enum, fn as TRACK_RELATIONS_DEFAULT_LIMIT, fr as isVectorIndexDimConflict, ft as NC_GROUPING_MAX_SEC, g as BIRTH_DECISION_DEFAULT_LIMIT, gi as literal, gn as TrackPuritySchema, gr as notificationRulesCapability, gt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, h as ArchivedDebugNoteSchema, hi as discriminatedUnion, hn as TimelapseRuleSchema, hr as memoizePinnedFfmpeg, ht as NC_SNOOZE_MAX_MINUTES, ii as customAction, in as SCENE_DEFAULT_UNCOVERED_POLICY, ir as isEventKindLabelKey, it as NC_CLIP_POST_ROLL_DEFAULT_SEC, j as EMBEDDING_REQUIREMENTS_UNAVAILABLE, jn as audioModeOf, jr as ruleGroupingApplies, jt as NcRuleSchema, k as EMBEDDING_MODEL_FAILED, kn as assertTimelapseCadences, kr as ruleBelongsToAlarm$1, kt as NcRuleInputSchema, l as APPEARANCE_BIN_COUNT, ln as SetTrackPurityResultSchema, lr as isScheduleActive, lt as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, m as AlarmArmModeSchema, mi as boolean, mn as TimelapseRulePatchSchema, mr as mayWriteToLocation, mt as NC_SENSOR_TRANSITION_KINDS, ni as CamProfileSchema, nn as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, nr as isDefaultAlarmProfileId, nt as NC_AUDIO_CONFIRM_HITS_DEFAULT, o as FACE_EMBEDDING_STEP_ID, oi as hydrateSchema, on as SCENE_RESET_RECAPTURES, or as isKnownTemplateVar, ot as NC_CLIP_RETENTION_MAX_SEC, p as AUDIO_MACRO_LABELS, pi as array, pn as TimelapseRuleInputSchema, pr as kebabToCamel, pt as NC_GROUPING_MIN_SEC, q as MAX_TRACK_RELATIONS, qn as ensureAlarmProfiles, qr as vectorDimFromBase64, ri as createEvent, rn as SCENE_DEFAULT_QUIET_SECONDS, rr as isDetectionMacroClass, rt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, s as readClusterModelPinStrict, si as isDeviceScopedCap, sn as SceneMonitorSchema, sr as isMediaOwnerType, st as NC_CONDITION_CATALOG, t as BUILTIN_CLIP_MODELS, ti as BaseAddon, tn as SCENE_DEFAULT_CHECK_INTERVAL_SEC, tr as isAudioClassName, tt as NC_ALARM_TRIGGERED_MAX_SEC, u as APPEARANCE_BLOCKS, ui as sleep$1, un as SetTrackRelationInputSchema, ur as isSystemDelivery, ut as NC_DEFAULT_SNOOZE_MINUTES, v as BirthDecisionRecordSchema, vi as number, vn as TrackRelationSchema, vt as NC_TAXONOMY, w as DEFAULT_TIMELAPSE_PREVIEW_TEXT, wi as templateLiteral, wn as alarmPanelCapability, wr as renameAlarmProfile, wt as NcConditionDescriptorSchema, x as ClearTrackRelationResultSchema, xi as record, xn as activateAlarmProfile, xr as readDeviceStateFrom, xt as NcAlarmSensorsInputSchema, y as COCO_TO_MACRO, yi as object, yn as TrackSideSchema, yr as pipelineAnalyticsCapability, yt as NC_TEMPLATE_VARS, z as LabelAttributionSchema, zn as describeSystemEventDeliveryProblem, zr as syncActiveAlarmProfileSettings, zt as NcStateSummarySectionIdSchema } from "../clip-model-registry-BENGs4rw.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs$1 from "node:fs";
|
|
4
4
|
import { createWriteStream, promises } from "node:fs";
|
|
@@ -938,7 +938,7 @@ function grantMeta(grant) {
|
|
|
938
938
|
...common,
|
|
939
939
|
ruleId: grant.ruleId,
|
|
940
940
|
rule: grant.ruleName,
|
|
941
|
-
|
|
941
|
+
sequenceId: grant.sequenceId
|
|
942
942
|
};
|
|
943
943
|
case "snooze": return {
|
|
944
944
|
...common,
|
|
@@ -5060,6 +5060,27 @@ function planDenseRanges(input) {
|
|
|
5060
5060
|
}
|
|
5061
5061
|
//#endregion
|
|
5062
5062
|
//#region src/notification-center/text-compose.ts
|
|
5063
|
+
/**
|
|
5064
|
+
* The COMPOSITE variables — phrases assembled from catalog fragments before
|
|
5065
|
+
* the one and only render pass.
|
|
5066
|
+
*
|
|
5067
|
+
* A notification body is not a sentence with slots. "person (John) on Front
|
|
5068
|
+
* door in gate" contains three sub-phrases whose SHAPE differs per language
|
|
5069
|
+
* and whose presence is conditional: the label parentheses, the zone clause,
|
|
5070
|
+
* the plural noun. Expressing that with `{{class}}{{label}}{{zones}}` and
|
|
5071
|
+
* three scalars pushes the grammar into the template, where a translator
|
|
5072
|
+
* cannot fix it. So each becomes ONE variable — `{{subject}}`, `{{inZones}}`,
|
|
5073
|
+
* `{{op}}`, `{{detectionSummary}}` — composed here from its own keys and handed
|
|
5074
|
+
* to {@link renderTemplate} as a finished string.
|
|
5075
|
+
*
|
|
5076
|
+
* **Fragments see the SCALARS only.** Never the composites. That is what makes
|
|
5077
|
+
* the depth exactly one and not "one so far": a fragment key edited to say
|
|
5078
|
+
* `{{inZones}}` renders empty rather than starting a second round, and
|
|
5079
|
+
* `renderTemplate` never re-scans what it inserted, so a composite's own value
|
|
5080
|
+
* is printed verbatim whatever it contains.
|
|
5081
|
+
*
|
|
5082
|
+
* Pure: a catalog in, a string out. No clock, no store, no I/O.
|
|
5083
|
+
*/
|
|
5063
5084
|
/** One normalisation, used on both sides of every class lookup — a pipeline
|
|
5064
5085
|
* that emitted `Person` must find the key written `class.person`. */
|
|
5065
5086
|
function normaliseClass(className) {
|
|
@@ -5068,24 +5089,35 @@ function normaliseClass(className) {
|
|
|
5068
5089
|
/**
|
|
5069
5090
|
* The noun for a class, in the count's plural category.
|
|
5070
5091
|
*
|
|
5071
|
-
*
|
|
5072
|
-
*
|
|
5073
|
-
*
|
|
5074
|
-
* detection.
|
|
5092
|
+
* Lookup order: the NC text catalog (`class.*`, plural-aware) → the shared
|
|
5093
|
+
* {@link eventKindLabel} map (viewer filters / EVENT_TAXONOMY) → `class.other`
|
|
5094
|
+
* with the RAW wire id. A custom model class must still appear rather than
|
|
5095
|
+
* vanish; a missing translation is a smaller defect than a missing detection.
|
|
5075
5096
|
*/
|
|
5076
5097
|
function ncClassNoun(texts, className, count) {
|
|
5077
|
-
const
|
|
5098
|
+
const normalised = normaliseClass(className);
|
|
5099
|
+
const key = `class.${normalised}`;
|
|
5078
5100
|
if (texts.has(key)) return texts.text({
|
|
5079
5101
|
key,
|
|
5080
5102
|
count,
|
|
5081
5103
|
vars: { name: className }
|
|
5082
5104
|
});
|
|
5105
|
+
if (isEventKindLabelKey(normalised)) return eventKindLabel(normalised, texts.language);
|
|
5083
5106
|
return texts.text({
|
|
5084
5107
|
key: "class.other",
|
|
5085
5108
|
count,
|
|
5086
5109
|
vars: { name: className }
|
|
5087
5110
|
});
|
|
5088
5111
|
}
|
|
5112
|
+
/**
|
|
5113
|
+
* The parenthetical half of a labelled subject: a recognised person or plate
|
|
5114
|
+
* is printed verbatim; a classification (species, COCO sub, …) goes through
|
|
5115
|
+
* the shared event-kind map so "animal (cat)" becomes "animale (Gatto)".
|
|
5116
|
+
*/
|
|
5117
|
+
function ncSubjectLabelText(texts, label, labelKind) {
|
|
5118
|
+
if (labelKind === "identity" || labelKind === "plate") return label;
|
|
5119
|
+
return eventKindLabel(label, texts.language);
|
|
5120
|
+
}
|
|
5089
5121
|
/** `person (John)` / `persona (John)` — the thing the notification is about. */
|
|
5090
5122
|
function ncSubjectText(texts, input) {
|
|
5091
5123
|
const classNoun = ncClassNoun(texts, input.className, 1);
|
|
@@ -5097,7 +5129,7 @@ function ncSubjectText(texts, input) {
|
|
|
5097
5129
|
key: subjectLabelKey(input.labelKind),
|
|
5098
5130
|
vars: {
|
|
5099
5131
|
classNoun,
|
|
5100
|
-
label: input.label
|
|
5132
|
+
label: ncSubjectLabelText(texts, input.label, input.labelKind)
|
|
5101
5133
|
}
|
|
5102
5134
|
});
|
|
5103
5135
|
}
|
|
@@ -8667,31 +8699,48 @@ function replaceConditionReferences(monitor, condition, reference) {
|
|
|
8667
8699
|
/** The cap whose presence in a sequence makes the rule the alarm's trigger. */
|
|
8668
8700
|
var ALARM_PANEL_CAP$1 = "alarm-panel";
|
|
8669
8701
|
var ALARM_KINDS = new Set(NC_ALARM_SYSTEM_EVENT_KINDS);
|
|
8670
|
-
function
|
|
8671
|
-
|
|
8702
|
+
function stepActuatesAlarm(step) {
|
|
8703
|
+
return step.kind === "cap" && step.cap === ALARM_PANEL_CAP$1;
|
|
8704
|
+
}
|
|
8705
|
+
function bindingActuatesAlarm(binding, sequenceActionsOf) {
|
|
8706
|
+
if (binding.kind === "do") return stepActuatesAlarm(binding.action);
|
|
8707
|
+
const steps = sequenceActionsOf?.(binding.sequenceId);
|
|
8708
|
+
if (steps === void 0) return false;
|
|
8709
|
+
return steps.some(stepActuatesAlarm);
|
|
8710
|
+
}
|
|
8711
|
+
function actionsActuateAlarm(rule, sequenceActionsOf) {
|
|
8712
|
+
const actions = rule.actions;
|
|
8713
|
+
if (actions === void 0) return false;
|
|
8714
|
+
const hooks = [
|
|
8715
|
+
actions.onActivation,
|
|
8716
|
+
actions.onDeactivation,
|
|
8717
|
+
actions.onTrigger,
|
|
8718
|
+
actions.onTriggerOff
|
|
8719
|
+
];
|
|
8720
|
+
for (const bindings of hooks) for (const binding of bindings ?? []) if (bindingActuatesAlarm(binding, sequenceActionsOf)) return true;
|
|
8721
|
+
for (const button of actions.buttons ?? []) {
|
|
8722
|
+
const steps = sequenceActionsOf?.(button.sequenceId);
|
|
8723
|
+
if (steps !== void 0 && steps.some(stepActuatesAlarm)) return true;
|
|
8724
|
+
}
|
|
8672
8725
|
return false;
|
|
8673
8726
|
}
|
|
8674
8727
|
/**
|
|
8675
8728
|
* May a snooze window silence this rule?
|
|
8676
8729
|
*
|
|
8677
|
-
* `true` = never. Deliberately answered from the rule alone
|
|
8678
|
-
* on the delivery path, where a fallible
|
|
8679
|
-
* DESTROY the exemption (D49) — and the
|
|
8680
|
-
* silencing an alarm.
|
|
8730
|
+
* `true` = never. Deliberately answered from the rule alone (plus an optional
|
|
8731
|
+
* sequence lookup): the gate reads it on the delivery path, where a fallible
|
|
8732
|
+
* lookup would be a read that can DESTROY the exemption (D49) — and the
|
|
8733
|
+
* failure direction of this one is silencing an alarm. A missing sequence
|
|
8734
|
+
* lookup means sequence-ref bindings cannot prove exemption; Do bindings and
|
|
8735
|
+
* system-event kinds still can.
|
|
8681
8736
|
*/
|
|
8682
|
-
function isSnoozeExempt(rule) {
|
|
8737
|
+
function isSnoozeExempt(rule, sequenceActionsOf) {
|
|
8683
8738
|
const kinds = rule.conditions.systemEvent?.kinds ?? [];
|
|
8684
8739
|
for (const kind of kinds) if (ALARM_KINDS.has(kind)) return true;
|
|
8685
|
-
return
|
|
8740
|
+
return actionsActuateAlarm(rule, sequenceActionsOf);
|
|
8686
8741
|
}
|
|
8687
8742
|
//#endregion
|
|
8688
8743
|
//#region src/notification-center/action-buttons.ts
|
|
8689
|
-
/** Sequence names the rule actually declares. A button may only name one. */
|
|
8690
|
-
function declaredSequences(actions) {
|
|
8691
|
-
const names = /* @__PURE__ */ new Set();
|
|
8692
|
-
for (const sequence of actions.onTrigger ?? []) names.add(sequence.name);
|
|
8693
|
-
return names;
|
|
8694
|
-
}
|
|
8695
8744
|
/**
|
|
8696
8745
|
* Build the notification's buttons.
|
|
8697
8746
|
*
|
|
@@ -8704,18 +8753,18 @@ function buildActionButtons(input) {
|
|
|
8704
8753
|
if (actions === void 0) return [];
|
|
8705
8754
|
const buttons = actions.buttons ?? [];
|
|
8706
8755
|
if (buttons.length === 0) return [];
|
|
8707
|
-
const known =
|
|
8756
|
+
const known = input.knownSequenceIds;
|
|
8708
8757
|
const seen = /* @__PURE__ */ new Set();
|
|
8709
8758
|
const out = [];
|
|
8710
8759
|
for (const button of buttons) {
|
|
8711
|
-
if (!known.has(button.
|
|
8760
|
+
if (!known.has(button.sequenceId)) continue;
|
|
8712
8761
|
if (seen.has(button.id)) continue;
|
|
8713
8762
|
seen.add(button.id);
|
|
8714
8763
|
out.push({
|
|
8715
8764
|
id: button.id,
|
|
8716
8765
|
label: button.label,
|
|
8717
8766
|
url: input.mintUrl({
|
|
8718
|
-
|
|
8767
|
+
sequenceId: button.sequenceId,
|
|
8719
8768
|
actionId: button.id
|
|
8720
8769
|
}),
|
|
8721
8770
|
...button.icon !== void 0 ? { icon: button.icon } : {},
|
|
@@ -8725,18 +8774,17 @@ function buildActionButtons(input) {
|
|
|
8725
8774
|
return out;
|
|
8726
8775
|
}
|
|
8727
8776
|
/**
|
|
8728
|
-
* Which of a rule's buttons name a sequence
|
|
8777
|
+
* Which of a rule's buttons name a sequence that is not known.
|
|
8729
8778
|
*
|
|
8730
8779
|
* Exported so the caller can LOG the drop. A button silently missing from a
|
|
8731
8780
|
* notification is the exact shape of failure this repo keeps paying for — the
|
|
8732
8781
|
* operator authored it, it never appeared, and nothing said why.
|
|
8733
8782
|
*/
|
|
8734
|
-
function unresolvableButtons(actions) {
|
|
8783
|
+
function unresolvableButtons(actions, knownSequenceIds) {
|
|
8735
8784
|
if (actions === void 0) return [];
|
|
8736
8785
|
const buttons = actions.buttons ?? [];
|
|
8737
8786
|
if (buttons.length === 0) return [];
|
|
8738
|
-
|
|
8739
|
-
return buttons.filter((b) => !known.has(b.sequence)).map((b) => `${b.id}→${b.sequence}`);
|
|
8787
|
+
return buttons.filter((b) => !knownSequenceIds.has(b.sequenceId)).map((b) => `${b.id}→${b.sequenceId}`);
|
|
8740
8788
|
}
|
|
8741
8789
|
/** How many snooze choices one notification may carry. The schema caps the
|
|
8742
8790
|
* authored list at 4; this is the same bound applied to a stored rule that
|
|
@@ -8787,7 +8835,7 @@ function snoozeChoices(options) {
|
|
|
8787
8835
|
* attention than the notification did.
|
|
8788
8836
|
*/
|
|
8789
8837
|
function buildSnoozeButtons(input) {
|
|
8790
|
-
if (isSnoozeExempt(input.rule)) return [];
|
|
8838
|
+
if (isSnoozeExempt(input.rule, input.sequenceActionsOf)) return [];
|
|
8791
8839
|
return snoozeChoices(input.rule.snoozeOptions).map((minutes) => {
|
|
8792
8840
|
const actionId = snoozeButtonId(minutes);
|
|
8793
8841
|
return {
|
|
@@ -11076,10 +11124,11 @@ function matchesIdentityScope(subject, scope) {
|
|
|
11076
11124
|
* Derived, never a stored flag: both rule editors prune condition keys to the
|
|
11077
11125
|
* catalog on save, so a new key would be dropped by the first edit.
|
|
11078
11126
|
*/
|
|
11079
|
-
function ruleBelongsToAlarm(rule) {
|
|
11127
|
+
function ruleBelongsToAlarm(rule, sequenceActionsOf) {
|
|
11080
11128
|
return ruleBelongsToAlarm$1({
|
|
11081
11129
|
conditions: rule.conditions,
|
|
11082
|
-
...rule.actions
|
|
11130
|
+
...rule.actions !== void 0 ? { actions: rule.actions } : {},
|
|
11131
|
+
...sequenceActionsOf !== void 0 ? { sequenceActionsOf } : {}
|
|
11083
11132
|
});
|
|
11084
11133
|
}
|
|
11085
11134
|
/**
|
|
@@ -12967,7 +13016,7 @@ function deriveTankBand(reading) {
|
|
|
12967
13016
|
case null: return null;
|
|
12968
13017
|
}
|
|
12969
13018
|
}
|
|
12970
|
-
function isRecord$
|
|
13019
|
+
function isRecord$9(value) {
|
|
12971
13020
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12972
13021
|
}
|
|
12973
13022
|
function tankStatusOf(value) {
|
|
@@ -12983,11 +13032,11 @@ function vacuumTanksOf(data) {
|
|
|
12983
13032
|
if (data["capName"] !== "vacuum-control") return null;
|
|
12984
13033
|
const deviceId = data["deviceId"];
|
|
12985
13034
|
const slice = data["slice"];
|
|
12986
|
-
if (typeof deviceId !== "number" || !isRecord$
|
|
13035
|
+
if (typeof deviceId !== "number" || !isRecord$9(slice)) return null;
|
|
12987
13036
|
const tanks = [];
|
|
12988
13037
|
for (const key of NC_TANK_KEYS) {
|
|
12989
13038
|
const raw = slice[key];
|
|
12990
|
-
if (!isRecord$
|
|
13039
|
+
if (!isRecord$9(raw)) continue;
|
|
12991
13040
|
const level = raw["level"];
|
|
12992
13041
|
tanks.push({
|
|
12993
13042
|
key,
|
|
@@ -20310,6 +20359,350 @@ var NcDeviceMuteStore = class {
|
|
|
20310
20359
|
}
|
|
20311
20360
|
};
|
|
20312
20361
|
//#endregion
|
|
20362
|
+
//#region src/notification-center/migrate-legacy-actions.ts
|
|
20363
|
+
function isRecord$8(value) {
|
|
20364
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20365
|
+
}
|
|
20366
|
+
/** A legacy inline sequence row — detected by `name` + `actions` + `enabled`. */
|
|
20367
|
+
function asLegacySequence(value) {
|
|
20368
|
+
const parsed = NcLegacyRuleActionSequenceSchema.safeParse(value);
|
|
20369
|
+
return parsed.success ? parsed.data : null;
|
|
20370
|
+
}
|
|
20371
|
+
/**
|
|
20372
|
+
* True when `onTrigger` still looks like the pre-shared-sequence array of
|
|
20373
|
+
* named sequences (not an array of `{ kind: 'sequence' | 'do', … }` bindings).
|
|
20374
|
+
*/
|
|
20375
|
+
function onTriggerLooksLegacy(onTrigger) {
|
|
20376
|
+
if (!Array.isArray(onTrigger) || onTrigger.length === 0) return false;
|
|
20377
|
+
const first = onTrigger[0];
|
|
20378
|
+
if (!isRecord$8(first)) return false;
|
|
20379
|
+
if (first["kind"] === "sequence" || first["kind"] === "do") return false;
|
|
20380
|
+
return asLegacySequence(first) !== null;
|
|
20381
|
+
}
|
|
20382
|
+
function legacyButtonSequenceName(button) {
|
|
20383
|
+
if (!isRecord$8(button)) return null;
|
|
20384
|
+
const name = button["sequence"];
|
|
20385
|
+
return typeof name === "string" && name.length > 0 ? name : null;
|
|
20386
|
+
}
|
|
20387
|
+
/**
|
|
20388
|
+
* Rewrite a raw `actions` block. Returns the (possibly unchanged) new-shape
|
|
20389
|
+
* actions and whether anything was materialised into the sequence store.
|
|
20390
|
+
*/
|
|
20391
|
+
async function migrateLegacyRuleActions(actions, createSequence) {
|
|
20392
|
+
if (actions === void 0 || actions === null) return {
|
|
20393
|
+
actions: void 0,
|
|
20394
|
+
migrated: false
|
|
20395
|
+
};
|
|
20396
|
+
if (!isRecord$8(actions)) return {
|
|
20397
|
+
actions: void 0,
|
|
20398
|
+
migrated: false
|
|
20399
|
+
};
|
|
20400
|
+
if (!onTriggerLooksLegacy(actions["onTrigger"]) && actions["sequence"] === void 0) {
|
|
20401
|
+
const buttons = actions["buttons"];
|
|
20402
|
+
if (!(Array.isArray(buttons) && buttons.some((b) => legacyButtonSequenceName(b) !== null))) {
|
|
20403
|
+
const parsed = NcRuleActionsSchema.safeParse(actions);
|
|
20404
|
+
if (parsed.success) return {
|
|
20405
|
+
actions: parsed.data,
|
|
20406
|
+
migrated: false
|
|
20407
|
+
};
|
|
20408
|
+
return {
|
|
20409
|
+
actions: void 0,
|
|
20410
|
+
migrated: false
|
|
20411
|
+
};
|
|
20412
|
+
}
|
|
20413
|
+
}
|
|
20414
|
+
const legacySequences = [];
|
|
20415
|
+
const onTriggerRaw = actions["onTrigger"];
|
|
20416
|
+
if (Array.isArray(onTriggerRaw)) for (const entry of onTriggerRaw) {
|
|
20417
|
+
const seq = asLegacySequence(entry);
|
|
20418
|
+
if (seq !== null) legacySequences.push(seq);
|
|
20419
|
+
}
|
|
20420
|
+
const nameToId = /* @__PURE__ */ new Map();
|
|
20421
|
+
for (const sequence of legacySequences) {
|
|
20422
|
+
const key = sequence.name.trim();
|
|
20423
|
+
if (key === "" || nameToId.has(key)) continue;
|
|
20424
|
+
const created = await createSequence({
|
|
20425
|
+
name: sequence.name,
|
|
20426
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
20427
|
+
actions: [...sequence.actions]
|
|
20428
|
+
});
|
|
20429
|
+
nameToId.set(key, created.id);
|
|
20430
|
+
}
|
|
20431
|
+
const onTriggerBindings = [];
|
|
20432
|
+
for (const sequence of legacySequences) {
|
|
20433
|
+
if (!sequence.enabled) continue;
|
|
20434
|
+
const id = nameToId.get(sequence.name.trim());
|
|
20435
|
+
if (id === void 0) continue;
|
|
20436
|
+
onTriggerBindings.push({
|
|
20437
|
+
kind: "sequence",
|
|
20438
|
+
sequenceId: id
|
|
20439
|
+
});
|
|
20440
|
+
}
|
|
20441
|
+
const buttonsRaw = actions["buttons"];
|
|
20442
|
+
const buttons = [];
|
|
20443
|
+
if (Array.isArray(buttonsRaw)) for (const button of buttonsRaw) {
|
|
20444
|
+
if (!isRecord$8(button)) continue;
|
|
20445
|
+
const id = typeof button["id"] === "string" ? button["id"] : "";
|
|
20446
|
+
const label = typeof button["label"] === "string" ? button["label"] : "";
|
|
20447
|
+
if (id === "" || label === "") continue;
|
|
20448
|
+
const existingId = typeof button["sequenceId"] === "string" && button["sequenceId"].length > 0 ? button["sequenceId"] : void 0;
|
|
20449
|
+
const legacyName = legacyButtonSequenceName(button);
|
|
20450
|
+
const sequenceId = existingId ?? (legacyName !== null ? nameToId.get(legacyName.trim()) : void 0);
|
|
20451
|
+
if (sequenceId === void 0) continue;
|
|
20452
|
+
buttons.push({
|
|
20453
|
+
id,
|
|
20454
|
+
label,
|
|
20455
|
+
sequenceId,
|
|
20456
|
+
...typeof button["icon"] === "string" ? { icon: button["icon"] } : {},
|
|
20457
|
+
...typeof button["destructive"] === "boolean" ? { destructive: button["destructive"] } : {}
|
|
20458
|
+
});
|
|
20459
|
+
}
|
|
20460
|
+
const next = {
|
|
20461
|
+
...Array.isArray(actions["onActivation"]) ? { onActivation: actions["onActivation"] } : {},
|
|
20462
|
+
...Array.isArray(actions["onDeactivation"]) ? { onDeactivation: actions["onDeactivation"] } : {},
|
|
20463
|
+
...onTriggerBindings.length > 0 ? { onTrigger: onTriggerBindings } : {},
|
|
20464
|
+
...Array.isArray(actions["onTriggerOff"]) ? { onTriggerOff: actions["onTriggerOff"] } : {},
|
|
20465
|
+
...buttons.length > 0 ? { buttons } : {}
|
|
20466
|
+
};
|
|
20467
|
+
const parsed = NcRuleActionsSchema.safeParse(next);
|
|
20468
|
+
if (!parsed.success) return {
|
|
20469
|
+
actions: void 0,
|
|
20470
|
+
migrated: true
|
|
20471
|
+
};
|
|
20472
|
+
return {
|
|
20473
|
+
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,
|
|
20474
|
+
migrated: true
|
|
20475
|
+
};
|
|
20476
|
+
}
|
|
20477
|
+
//#endregion
|
|
20478
|
+
//#region src/notification-center/sequence-store.ts
|
|
20479
|
+
/**
|
|
20480
|
+
* Shared action sequences for the notification centre.
|
|
20481
|
+
*
|
|
20482
|
+
* A sequence is a named, ordered run of steps (`wait` | `cap`) with its own
|
|
20483
|
+
* throttle. Rules and notification buttons bind to it by id — never by copying
|
|
20484
|
+
* the steps inline — so two rules that open the same gate share one budget and
|
|
20485
|
+
* one authoring surface.
|
|
20486
|
+
*
|
|
20487
|
+
* Mirrors {@link NcDeviceMuteStore}: declare the collection, mirror into RAM,
|
|
20488
|
+
* write-through so a failed persist never leaves a phantom sequence the next
|
|
20489
|
+
* boot will not know.
|
|
20490
|
+
*/
|
|
20491
|
+
/**
|
|
20492
|
+
* @durable class=config owner=notification-center
|
|
20493
|
+
* write="an admin creates/edits/deletes a shared sequence via
|
|
20494
|
+
* `notificationRules.createSequence` / `updateSequence` / `deleteSequence`,
|
|
20495
|
+
* or the one-shot boot migration that materialises legacy inline sequences"
|
|
20496
|
+
* retention="none — a row goes only when an admin deletes it. Bounded by the
|
|
20497
|
+
* sequence set a human is willing to author."
|
|
20498
|
+
*/
|
|
20499
|
+
var NC_SEQUENCES_COLLECTION = "notification-center:sequences";
|
|
20500
|
+
var NC_SEQUENCES_COLUMNS = [
|
|
20501
|
+
{
|
|
20502
|
+
name: "id",
|
|
20503
|
+
type: "TEXT",
|
|
20504
|
+
primaryKey: true,
|
|
20505
|
+
notNull: true
|
|
20506
|
+
},
|
|
20507
|
+
{
|
|
20508
|
+
name: "name",
|
|
20509
|
+
type: "TEXT",
|
|
20510
|
+
notNull: true
|
|
20511
|
+
},
|
|
20512
|
+
{
|
|
20513
|
+
name: "minDelaySec",
|
|
20514
|
+
type: "INTEGER"
|
|
20515
|
+
},
|
|
20516
|
+
{
|
|
20517
|
+
name: "actions",
|
|
20518
|
+
type: "JSON",
|
|
20519
|
+
notNull: true
|
|
20520
|
+
},
|
|
20521
|
+
{
|
|
20522
|
+
name: "createdAt",
|
|
20523
|
+
type: "INTEGER",
|
|
20524
|
+
notNull: true
|
|
20525
|
+
},
|
|
20526
|
+
{
|
|
20527
|
+
name: "updatedAt",
|
|
20528
|
+
type: "INTEGER",
|
|
20529
|
+
notNull: true
|
|
20530
|
+
}
|
|
20531
|
+
];
|
|
20532
|
+
function isRecord$7(value) {
|
|
20533
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20534
|
+
}
|
|
20535
|
+
function parseActions(raw) {
|
|
20536
|
+
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
20537
|
+
const out = [];
|
|
20538
|
+
for (const entry of raw) {
|
|
20539
|
+
const parsed = NcRuleActionSchema.safeParse(entry);
|
|
20540
|
+
if (!parsed.success) return null;
|
|
20541
|
+
out.push(parsed.data);
|
|
20542
|
+
}
|
|
20543
|
+
return out;
|
|
20544
|
+
}
|
|
20545
|
+
function rowToSequence(data) {
|
|
20546
|
+
const id = data["id"];
|
|
20547
|
+
const name = data["name"];
|
|
20548
|
+
if (typeof id !== "string" || id.length === 0) return null;
|
|
20549
|
+
if (typeof name !== "string" || name.length === 0) return null;
|
|
20550
|
+
const actions = parseActions(data["actions"]);
|
|
20551
|
+
if (actions === null) return null;
|
|
20552
|
+
const minDelayRaw = data["minDelaySec"];
|
|
20553
|
+
const minDelaySec = typeof minDelayRaw === "number" && Number.isInteger(minDelayRaw) ? minDelayRaw : void 0;
|
|
20554
|
+
const candidate = {
|
|
20555
|
+
id,
|
|
20556
|
+
name,
|
|
20557
|
+
...minDelaySec !== void 0 ? { minDelaySec } : {},
|
|
20558
|
+
actions: [...actions]
|
|
20559
|
+
};
|
|
20560
|
+
const parsed = NcSharedSequenceSchema.safeParse(candidate);
|
|
20561
|
+
return parsed.success ? parsed.data : null;
|
|
20562
|
+
}
|
|
20563
|
+
var NcSequenceStore = class {
|
|
20564
|
+
byId = /* @__PURE__ */ new Map();
|
|
20565
|
+
store;
|
|
20566
|
+
logger;
|
|
20567
|
+
now;
|
|
20568
|
+
constructor(deps) {
|
|
20569
|
+
this.store = deps.store;
|
|
20570
|
+
this.logger = deps.logger;
|
|
20571
|
+
this.now = deps.now ?? (() => Date.now());
|
|
20572
|
+
}
|
|
20573
|
+
static async declare(store) {
|
|
20574
|
+
await store.declareCollection.mutate({
|
|
20575
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20576
|
+
columns: [...NC_SEQUENCES_COLUMNS]
|
|
20577
|
+
});
|
|
20578
|
+
}
|
|
20579
|
+
/**
|
|
20580
|
+
* (Re)hydrate. A failed load KEEPS what is already in memory — dropping every
|
|
20581
|
+
* sequence on a transient store error would leave every button and binding
|
|
20582
|
+
* pointing at nothing until the next successful load.
|
|
20583
|
+
*/
|
|
20584
|
+
async load() {
|
|
20585
|
+
try {
|
|
20586
|
+
const rows = await this.store.query.query({
|
|
20587
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20588
|
+
filter: { limit: 1e4 }
|
|
20589
|
+
});
|
|
20590
|
+
const next = /* @__PURE__ */ new Map();
|
|
20591
|
+
let skipped = 0;
|
|
20592
|
+
for (const row of rows) {
|
|
20593
|
+
if (!isRecord$7(row.data)) {
|
|
20594
|
+
skipped += 1;
|
|
20595
|
+
continue;
|
|
20596
|
+
}
|
|
20597
|
+
const sequence = rowToSequence(row.data);
|
|
20598
|
+
if (sequence === null) {
|
|
20599
|
+
skipped += 1;
|
|
20600
|
+
continue;
|
|
20601
|
+
}
|
|
20602
|
+
next.set(sequence.id, sequence);
|
|
20603
|
+
}
|
|
20604
|
+
this.byId.clear();
|
|
20605
|
+
for (const [id, sequence] of next) this.byId.set(id, sequence);
|
|
20606
|
+
if (skipped > 0) this.logger.warn("sequence rows skipped as malformed — bindings to them will miss", { meta: { skipped } });
|
|
20607
|
+
} catch (err) {
|
|
20608
|
+
this.logger.warn("sequence load failed — keeping the sequences already in memory", { meta: {
|
|
20609
|
+
error: String(err),
|
|
20610
|
+
sequences: this.byId.size
|
|
20611
|
+
} });
|
|
20612
|
+
}
|
|
20613
|
+
}
|
|
20614
|
+
list() {
|
|
20615
|
+
return [...this.byId.values()].toSorted((a, b) => a.name.localeCompare(b.name));
|
|
20616
|
+
}
|
|
20617
|
+
get(sequenceId) {
|
|
20618
|
+
return this.byId.get(sequenceId) ?? null;
|
|
20619
|
+
}
|
|
20620
|
+
/** Hot-path existence check for button minting. */
|
|
20621
|
+
has(sequenceId) {
|
|
20622
|
+
return this.byId.has(sequenceId);
|
|
20623
|
+
}
|
|
20624
|
+
knownIds() {
|
|
20625
|
+
return new Set(this.byId.keys());
|
|
20626
|
+
}
|
|
20627
|
+
async create(input) {
|
|
20628
|
+
const now = this.now();
|
|
20629
|
+
const sequence = {
|
|
20630
|
+
id: randomUUID(),
|
|
20631
|
+
name: input.name,
|
|
20632
|
+
...input.minDelaySec !== void 0 ? { minDelaySec: input.minDelaySec } : {},
|
|
20633
|
+
actions: [...input.actions]
|
|
20634
|
+
};
|
|
20635
|
+
await this.persist(sequence, now, now);
|
|
20636
|
+
this.byId.set(sequence.id, sequence);
|
|
20637
|
+
return sequence;
|
|
20638
|
+
}
|
|
20639
|
+
/**
|
|
20640
|
+
* Create with a CALLER-chosen id — used by the legacy migration so a rewrite
|
|
20641
|
+
* can name the sequence before rewriting the rule that points at it.
|
|
20642
|
+
*/
|
|
20643
|
+
async createWithId(sequence) {
|
|
20644
|
+
const now = this.now();
|
|
20645
|
+
const stored = {
|
|
20646
|
+
id: sequence.id,
|
|
20647
|
+
name: sequence.name,
|
|
20648
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
20649
|
+
actions: [...sequence.actions]
|
|
20650
|
+
};
|
|
20651
|
+
await this.persist(stored, now, now);
|
|
20652
|
+
this.byId.set(stored.id, stored);
|
|
20653
|
+
return stored;
|
|
20654
|
+
}
|
|
20655
|
+
async update(sequenceId, input) {
|
|
20656
|
+
if (this.byId.get(sequenceId) === void 0) throw new Error(`sequence ${sequenceId} does not exist`);
|
|
20657
|
+
const now = this.now();
|
|
20658
|
+
const sequence = {
|
|
20659
|
+
id: sequenceId,
|
|
20660
|
+
name: input.name,
|
|
20661
|
+
...input.minDelaySec !== void 0 ? { minDelaySec: input.minDelaySec } : {},
|
|
20662
|
+
actions: [...input.actions]
|
|
20663
|
+
};
|
|
20664
|
+
const createdAt = await this.readCreatedAt(sequenceId, now);
|
|
20665
|
+
await this.persist(sequence, createdAt, now);
|
|
20666
|
+
this.byId.set(sequenceId, sequence);
|
|
20667
|
+
return sequence;
|
|
20668
|
+
}
|
|
20669
|
+
async delete(sequenceId) {
|
|
20670
|
+
await this.store.delete.mutate({
|
|
20671
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20672
|
+
key: sequenceId
|
|
20673
|
+
});
|
|
20674
|
+
this.byId.delete(sequenceId);
|
|
20675
|
+
}
|
|
20676
|
+
async readCreatedAt(sequenceId, fallback) {
|
|
20677
|
+
try {
|
|
20678
|
+
const raw = (await this.store.query.query({
|
|
20679
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20680
|
+
filter: {
|
|
20681
|
+
where: { id: sequenceId },
|
|
20682
|
+
limit: 1
|
|
20683
|
+
}
|
|
20684
|
+
}))[0]?.data["createdAt"];
|
|
20685
|
+
return typeof raw === "number" && Number.isFinite(raw) ? raw : fallback;
|
|
20686
|
+
} catch {
|
|
20687
|
+
return fallback;
|
|
20688
|
+
}
|
|
20689
|
+
}
|
|
20690
|
+
async persist(sequence, createdAt, updatedAt) {
|
|
20691
|
+
await this.store.set.mutate({
|
|
20692
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20693
|
+
key: sequence.id,
|
|
20694
|
+
value: {
|
|
20695
|
+
id: sequence.id,
|
|
20696
|
+
name: sequence.name,
|
|
20697
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
20698
|
+
actions: sequence.actions,
|
|
20699
|
+
createdAt,
|
|
20700
|
+
updatedAt
|
|
20701
|
+
}
|
|
20702
|
+
});
|
|
20703
|
+
}
|
|
20704
|
+
};
|
|
20705
|
+
//#endregion
|
|
20313
20706
|
//#region src/notification-center/device-prune.ts
|
|
20314
20707
|
function judgeList(list, isDead) {
|
|
20315
20708
|
const dead = list.filter(isDead);
|
|
@@ -20423,58 +20816,49 @@ function pruneRuleConditions(conditions, isDead, notes) {
|
|
|
20423
20816
|
if (sceneDevice !== void 0 && isDead(sceneDevice)) notes.kept("conditions.scene", [String(sceneDevice)]);
|
|
20424
20817
|
return changed ? next : null;
|
|
20425
20818
|
}
|
|
20426
|
-
/** A
|
|
20427
|
-
|
|
20428
|
-
|
|
20429
|
-
|
|
20430
|
-
|
|
20431
|
-
|
|
20432
|
-
return false;
|
|
20433
|
-
});
|
|
20434
|
-
if (dead.length === 0) return null;
|
|
20435
|
-
const anyDeviceStep = kept.some((step) => step.kind === "cap");
|
|
20436
|
-
const [first, ...rest] = kept;
|
|
20437
|
-
if (!anyDeviceStep || first === void 0) return {
|
|
20438
|
-
next: "removed",
|
|
20439
|
-
dead
|
|
20440
|
-
};
|
|
20441
|
-
return {
|
|
20442
|
-
next: {
|
|
20443
|
-
...sequence,
|
|
20444
|
-
actions: [first, ...rest]
|
|
20445
|
-
},
|
|
20446
|
-
dead
|
|
20819
|
+
/** A Do binding whose device is dead is dropped; sequence refs are left alone
|
|
20820
|
+
* (shared sequences are pruned on their own store, not per-rule). */
|
|
20821
|
+
function pruneHookBindings(bindings, field, isDead, notes) {
|
|
20822
|
+
if (bindings === void 0) return {
|
|
20823
|
+
next: void 0,
|
|
20824
|
+
changed: false
|
|
20447
20825
|
};
|
|
20448
|
-
|
|
20449
|
-
/** The actions after a device prune — `null` when no step moved. */
|
|
20450
|
-
function pruneRuleActions(actions, isDead, notes) {
|
|
20451
|
-
const sequences = actions.onTrigger;
|
|
20452
|
-
if (sequences === void 0) return null;
|
|
20826
|
+
const kept = [];
|
|
20453
20827
|
let changed = false;
|
|
20454
|
-
const
|
|
20455
|
-
|
|
20456
|
-
|
|
20457
|
-
const pruned = pruneSequence(sequence, isDead);
|
|
20458
|
-
if (pruned === null) {
|
|
20459
|
-
nextSequences.push(sequence);
|
|
20828
|
+
for (const binding of bindings) {
|
|
20829
|
+
if (binding.kind === "sequence") {
|
|
20830
|
+
kept.push(binding);
|
|
20460
20831
|
continue;
|
|
20461
20832
|
}
|
|
20462
|
-
|
|
20463
|
-
|
|
20464
|
-
|
|
20465
|
-
|
|
20466
|
-
notes.removed(field, deviceSubjects(pruned.dead), "sequence-removed");
|
|
20467
|
-
} else {
|
|
20468
|
-
nextSequences.push(pruned.next);
|
|
20469
|
-
notes.removed(field, deviceSubjects(pruned.dead));
|
|
20833
|
+
if (binding.action.kind === "cap" && isDead(binding.action.deviceId)) {
|
|
20834
|
+
changed = true;
|
|
20835
|
+
notes.removed(field, deviceSubjects([binding.action.deviceId]), "binding-removed");
|
|
20836
|
+
continue;
|
|
20470
20837
|
}
|
|
20838
|
+
kept.push(binding);
|
|
20471
20839
|
}
|
|
20472
|
-
if (!changed) return
|
|
20473
|
-
|
|
20840
|
+
if (!changed) return {
|
|
20841
|
+
next: bindings,
|
|
20842
|
+
changed: false
|
|
20843
|
+
};
|
|
20844
|
+
return {
|
|
20845
|
+
next: kept.length > 0 ? kept : void 0,
|
|
20846
|
+
changed: true
|
|
20847
|
+
};
|
|
20848
|
+
}
|
|
20849
|
+
/** The actions after a device prune — `null` when no binding moved. */
|
|
20850
|
+
function pruneRuleActions(actions, isDead, notes) {
|
|
20851
|
+
const onActivation = pruneHookBindings(actions.onActivation, "actions.onActivation", isDead, notes);
|
|
20852
|
+
const onDeactivation = pruneHookBindings(actions.onDeactivation, "actions.onDeactivation", isDead, notes);
|
|
20853
|
+
const onTrigger = pruneHookBindings(actions.onTrigger, "actions.onTrigger", isDead, notes);
|
|
20854
|
+
const onTriggerOff = pruneHookBindings(actions.onTriggerOff, "actions.onTriggerOff", isDead, notes);
|
|
20855
|
+
if (!onActivation.changed && !onDeactivation.changed && !onTrigger.changed && !onTriggerOff.changed) return null;
|
|
20474
20856
|
return {
|
|
20475
|
-
...
|
|
20476
|
-
|
|
20477
|
-
...
|
|
20857
|
+
...onActivation.next !== void 0 ? { onActivation: [...onActivation.next] } : {},
|
|
20858
|
+
...onDeactivation.next !== void 0 ? { onDeactivation: [...onDeactivation.next] } : {},
|
|
20859
|
+
...onTrigger.next !== void 0 ? { onTrigger: [...onTrigger.next] } : {},
|
|
20860
|
+
...onTriggerOff.next !== void 0 ? { onTriggerOff: [...onTriggerOff.next] } : {},
|
|
20861
|
+
...actions.buttons !== void 0 ? { buttons: actions.buttons } : {}
|
|
20478
20862
|
};
|
|
20479
20863
|
}
|
|
20480
20864
|
function planRuleDevices(rule, isDead, guard) {
|
|
@@ -24709,6 +25093,10 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
|
|
|
24709
25093
|
return {
|
|
24710
25094
|
camera: deviceName,
|
|
24711
25095
|
class: detection?.className ?? (systemEvent !== void 0 ? "" : subject.className),
|
|
25096
|
+
classLabel: (() => {
|
|
25097
|
+
const wire = detection?.className ?? (systemEvent !== void 0 ? "" : subject.className);
|
|
25098
|
+
return wire.length === 0 ? "" : eventKindLabel(wire, texts.language);
|
|
25099
|
+
})(),
|
|
24712
25100
|
label: detection?.label ?? subject.label ?? "",
|
|
24713
25101
|
zones: zoneLabels.join(", "),
|
|
24714
25102
|
subject: detection?.className !== void 0 ? ncSubjectText(texts, {
|
|
@@ -28850,9 +29238,32 @@ function lastRunRow(data) {
|
|
|
28850
29238
|
}
|
|
28851
29239
|
//#endregion
|
|
28852
29240
|
//#region src/notification-center/rule-actions.ts
|
|
29241
|
+
function wrapDo(action) {
|
|
29242
|
+
const name = action.kind === "wait" ? `wait ${String(action.seconds)}s` : `${action.cap}.${action.method}`;
|
|
29243
|
+
return {
|
|
29244
|
+
id: `do:${name}`,
|
|
29245
|
+
name,
|
|
29246
|
+
enabled: true,
|
|
29247
|
+
actions: [action],
|
|
29248
|
+
throttle: false
|
|
29249
|
+
};
|
|
29250
|
+
}
|
|
29251
|
+
function resolveBinding(binding, lookup) {
|
|
29252
|
+
if (binding.kind === "do") return wrapDo(binding.action);
|
|
29253
|
+
const sequence = lookup(binding.sequenceId);
|
|
29254
|
+
if (sequence === null) return { missing: binding.sequenceId };
|
|
29255
|
+
return {
|
|
29256
|
+
id: sequence.id,
|
|
29257
|
+
name: sequence.name,
|
|
29258
|
+
enabled: true,
|
|
29259
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
29260
|
+
actions: sequence.actions,
|
|
29261
|
+
throttle: true
|
|
29262
|
+
};
|
|
29263
|
+
}
|
|
28853
29264
|
var NcRuleActionRunner = class {
|
|
28854
29265
|
deps;
|
|
28855
|
-
/** Last run per
|
|
29266
|
+
/** Last run per `sequenceId` — the throttle's memory (shared across rules). */
|
|
28856
29267
|
lastRunAt = /* @__PURE__ */ new Map();
|
|
28857
29268
|
now;
|
|
28858
29269
|
sleep;
|
|
@@ -28862,11 +29273,49 @@ var NcRuleActionRunner = class {
|
|
|
28862
29273
|
this.sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
28863
29274
|
}
|
|
28864
29275
|
/**
|
|
28865
|
-
*
|
|
29276
|
+
* Resolve bindings via the sequence store and run each, in order.
|
|
28866
29277
|
*
|
|
28867
29278
|
* Never throws: an actuation that fails must not cost the NOTIFICATION about
|
|
28868
|
-
* the very same event.
|
|
28869
|
-
|
|
29279
|
+
* the very same event.
|
|
29280
|
+
*/
|
|
29281
|
+
async runBindings(input) {
|
|
29282
|
+
const sequences = [];
|
|
29283
|
+
const missing = [];
|
|
29284
|
+
for (const binding of input.bindings) {
|
|
29285
|
+
const resolved = resolveBinding(binding, input.lookup);
|
|
29286
|
+
if ("missing" in resolved) {
|
|
29287
|
+
this.deps.logger.warn("rule action binding skipped — sequence missing", {
|
|
29288
|
+
tags: { deviceId: input.deviceId },
|
|
29289
|
+
meta: {
|
|
29290
|
+
ruleId: input.ruleId,
|
|
29291
|
+
sequenceId: resolved.missing
|
|
29292
|
+
}
|
|
29293
|
+
});
|
|
29294
|
+
missing.push({
|
|
29295
|
+
id: resolved.missing,
|
|
29296
|
+
name: resolved.missing,
|
|
29297
|
+
ran: false,
|
|
29298
|
+
stepsRun: 0,
|
|
29299
|
+
skippedReason: "missing"
|
|
29300
|
+
});
|
|
29301
|
+
continue;
|
|
29302
|
+
}
|
|
29303
|
+
sequences.push(resolved);
|
|
29304
|
+
}
|
|
29305
|
+
const ran = await this.run({
|
|
29306
|
+
ruleId: input.ruleId,
|
|
29307
|
+
ruleName: input.ruleName,
|
|
29308
|
+
deviceId: input.deviceId,
|
|
29309
|
+
sequences,
|
|
29310
|
+
...input.origin !== void 0 ? { origin: input.origin } : {}
|
|
29311
|
+
});
|
|
29312
|
+
return [...missing, ...ran];
|
|
29313
|
+
}
|
|
29314
|
+
/**
|
|
29315
|
+
* Run every enabled sequence, in order.
|
|
29316
|
+
*
|
|
29317
|
+
* Prefer {@link runBindings} at call sites that hold bindings. This remains
|
|
29318
|
+
* for button grants (force-enabled shared sequence) and tests.
|
|
28870
29319
|
*/
|
|
28871
29320
|
async run(input) {
|
|
28872
29321
|
const out = [];
|
|
@@ -28875,38 +29324,41 @@ var NcRuleActionRunner = class {
|
|
|
28875
29324
|
}
|
|
28876
29325
|
async runOne(input, sequence) {
|
|
28877
29326
|
if (!sequence.enabled) return {
|
|
29327
|
+
id: sequence.id,
|
|
28878
29328
|
name: sequence.name,
|
|
28879
29329
|
ran: false,
|
|
28880
29330
|
stepsRun: 0,
|
|
28881
29331
|
skippedReason: "disabled"
|
|
28882
29332
|
};
|
|
28883
29333
|
if (sequence.actions.length === 0) return {
|
|
29334
|
+
id: sequence.id,
|
|
28884
29335
|
name: sequence.name,
|
|
28885
29336
|
ran: false,
|
|
28886
29337
|
stepsRun: 0,
|
|
28887
29338
|
skippedReason: "empty"
|
|
28888
29339
|
};
|
|
28889
|
-
const key = `${input.ruleId}:${sequence.name}`;
|
|
28890
29340
|
const now = this.now();
|
|
28891
|
-
const minDelayMs = (sequence.minDelaySec ?? 0) * 1e3;
|
|
28892
|
-
const last = this.lastRunAt.get(
|
|
29341
|
+
const minDelayMs = sequence.throttle ? (sequence.minDelaySec ?? 0) * 1e3 : 0;
|
|
29342
|
+
const last = this.lastRunAt.get(sequence.id);
|
|
28893
29343
|
if (minDelayMs > 0 && last !== void 0 && now - last < minDelayMs) {
|
|
28894
29344
|
this.deps.logger.debug("rule action sequence throttled", {
|
|
28895
29345
|
tags: { deviceId: input.deviceId },
|
|
28896
29346
|
meta: {
|
|
28897
29347
|
ruleId: input.ruleId,
|
|
29348
|
+
sequenceId: sequence.id,
|
|
28898
29349
|
sequence: sequence.name,
|
|
28899
29350
|
sinceLastMs: now - last
|
|
28900
29351
|
}
|
|
28901
29352
|
});
|
|
28902
29353
|
return {
|
|
29354
|
+
id: sequence.id,
|
|
28903
29355
|
name: sequence.name,
|
|
28904
29356
|
ran: false,
|
|
28905
29357
|
stepsRun: 0,
|
|
28906
29358
|
skippedReason: "throttled"
|
|
28907
29359
|
};
|
|
28908
29360
|
}
|
|
28909
|
-
this.lastRunAt.set(
|
|
29361
|
+
if (sequence.throttle) this.lastRunAt.set(sequence.id, now);
|
|
28910
29362
|
let stepsRun = 0;
|
|
28911
29363
|
for (const [index, action] of sequence.actions.entries()) try {
|
|
28912
29364
|
await this.step(action, input.origin);
|
|
@@ -28917,6 +29369,7 @@ var NcRuleActionRunner = class {
|
|
|
28917
29369
|
meta: {
|
|
28918
29370
|
ruleId: input.ruleId,
|
|
28919
29371
|
rule: input.ruleName,
|
|
29372
|
+
sequenceId: sequence.id,
|
|
28920
29373
|
sequence: sequence.name,
|
|
28921
29374
|
step: index,
|
|
28922
29375
|
action: action.kind === "cap" ? `${action.cap}.${action.method}` : "wait",
|
|
@@ -28924,6 +29377,7 @@ var NcRuleActionRunner = class {
|
|
|
28924
29377
|
}
|
|
28925
29378
|
});
|
|
28926
29379
|
return {
|
|
29380
|
+
id: sequence.id,
|
|
28927
29381
|
name: sequence.name,
|
|
28928
29382
|
ran: true,
|
|
28929
29383
|
stepsRun,
|
|
@@ -28935,11 +29389,13 @@ var NcRuleActionRunner = class {
|
|
|
28935
29389
|
meta: {
|
|
28936
29390
|
ruleId: input.ruleId,
|
|
28937
29391
|
rule: input.ruleName,
|
|
29392
|
+
sequenceId: sequence.id,
|
|
28938
29393
|
sequence: sequence.name,
|
|
28939
29394
|
steps: stepsRun
|
|
28940
29395
|
}
|
|
28941
29396
|
});
|
|
28942
29397
|
return {
|
|
29398
|
+
id: sequence.id,
|
|
28943
29399
|
name: sequence.name,
|
|
28944
29400
|
ran: true,
|
|
28945
29401
|
stepsRun
|
|
@@ -28960,6 +29416,17 @@ var NcRuleActionRunner = class {
|
|
|
28960
29416
|
});
|
|
28961
29417
|
}
|
|
28962
29418
|
};
|
|
29419
|
+
/** Force-enable a shared sequence for a button tap (always runs, still throttled). */
|
|
29420
|
+
function asForcedSequence(sequence) {
|
|
29421
|
+
return {
|
|
29422
|
+
id: sequence.id,
|
|
29423
|
+
name: sequence.name,
|
|
29424
|
+
enabled: true,
|
|
29425
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
29426
|
+
actions: sequence.actions,
|
|
29427
|
+
throttle: true
|
|
29428
|
+
};
|
|
29429
|
+
}
|
|
28963
29430
|
//#endregion
|
|
28964
29431
|
//#region src/notification-center/rule-ai-analyst.ts
|
|
28965
29432
|
/**
|
|
@@ -30806,7 +31273,7 @@ function snoozeVerdict(input, deps) {
|
|
|
30806
31273
|
});
|
|
30807
31274
|
if (active === null) return { kind: "clear" };
|
|
30808
31275
|
const rule = deps.ruleFor(input.ruleId);
|
|
30809
|
-
if (rule !== null && isSnoozeExempt(rule)) return {
|
|
31276
|
+
if (rule !== null && isSnoozeExempt(rule, deps.sequenceActionsOf)) return {
|
|
30810
31277
|
kind: "exempt",
|
|
30811
31278
|
snooze: active
|
|
30812
31279
|
};
|
|
@@ -36918,6 +37385,8 @@ var NotificationCenter = class {
|
|
|
36918
37385
|
snoozes;
|
|
36919
37386
|
/** The permanent per-camera notification off switch — see `device-mute-store.ts`. */
|
|
36920
37387
|
deviceMutes;
|
|
37388
|
+
/** Shared action sequences rules and buttons bind to by id. */
|
|
37389
|
+
sequences;
|
|
36921
37390
|
/** The durable episode rows behind the sustained-state alerts. */
|
|
36922
37391
|
alertStore;
|
|
36923
37392
|
/** Battery band → notification, and the reminder sweep. */
|
|
@@ -37156,7 +37625,8 @@ var NotificationCenter = class {
|
|
|
37156
37625
|
const drafts = [];
|
|
37157
37626
|
const sequenceButtons = this.actionRunner === null || stored === null ? [] : buildActionButtons({
|
|
37158
37627
|
actions: stored.actions,
|
|
37159
|
-
|
|
37628
|
+
knownSequenceIds: this.sequences.knownIds(),
|
|
37629
|
+
mintUrl: ({ sequenceId, actionId }) => {
|
|
37160
37630
|
const id = randomUUID();
|
|
37161
37631
|
drafts.push({
|
|
37162
37632
|
id,
|
|
@@ -37166,7 +37636,7 @@ var NotificationCenter = class {
|
|
|
37166
37636
|
id,
|
|
37167
37637
|
ruleId: input.ruleId,
|
|
37168
37638
|
ruleName: input.ruleName,
|
|
37169
|
-
|
|
37639
|
+
sequenceId,
|
|
37170
37640
|
deviceId: input.deviceId,
|
|
37171
37641
|
actionId,
|
|
37172
37642
|
targetId: input.targetId,
|
|
@@ -37178,6 +37648,7 @@ var NotificationCenter = class {
|
|
|
37178
37648
|
});
|
|
37179
37649
|
const snoozeButtons = input.ownerUserId === void 0 || stored === null ? [] : buildSnoozeButtons({
|
|
37180
37650
|
rule: stored,
|
|
37651
|
+
sequenceActionsOf: this.sequenceActionsOf,
|
|
37181
37652
|
label: (minutes) => this.snoozeButtonLabel(minutes),
|
|
37182
37653
|
mintUrl: ({ actionId, minutes }) => {
|
|
37183
37654
|
const id = randomUUID();
|
|
@@ -37418,7 +37889,7 @@ var NotificationCenter = class {
|
|
|
37418
37889
|
*/
|
|
37419
37890
|
async applyGrantedSnooze(grant) {
|
|
37420
37891
|
const rule = this.rules.get(grant.ruleId);
|
|
37421
|
-
if (rule !== null && isSnoozeExempt(rule)) throw new Error(`"${grant.ruleName}" is an alarm rule and cannot be snoozed`);
|
|
37892
|
+
if (rule !== null && isSnoozeExempt(rule, this.sequenceActionsOf)) throw new Error(`"${grant.ruleName}" is an alarm rule and cannot be snoozed`);
|
|
37422
37893
|
const snooze = await this.snoozes.create({
|
|
37423
37894
|
userId: grant.userId,
|
|
37424
37895
|
scope: "device",
|
|
@@ -37717,16 +38188,13 @@ var NotificationCenter = class {
|
|
|
37717
38188
|
if (this.actionRunner === null) throw new Error("no actuator is wired on this node");
|
|
37718
38189
|
const rule = this.rules.get(grant.ruleId);
|
|
37719
38190
|
if (rule === null) throw new Error(`rule ${grant.ruleId} no longer exists`);
|
|
37720
|
-
const sequence =
|
|
37721
|
-
if (sequence ===
|
|
38191
|
+
const sequence = this.sequences.get(grant.sequenceId);
|
|
38192
|
+
if (sequence === null) throw new Error(`sequence "${grant.sequenceId}" no longer exists`);
|
|
37722
38193
|
const [outcome] = await this.actionRunner.run({
|
|
37723
38194
|
ruleId: rule.id,
|
|
37724
38195
|
ruleName: rule.name,
|
|
37725
38196
|
deviceId: grant.deviceId,
|
|
37726
|
-
sequences: [
|
|
37727
|
-
...sequence,
|
|
37728
|
-
enabled: true
|
|
37729
|
-
}]
|
|
38197
|
+
sequences: [asForcedSequence(sequence)]
|
|
37730
38198
|
});
|
|
37731
38199
|
if (outcome === void 0) throw new Error("the sequence did not run");
|
|
37732
38200
|
if (outcome.failedAt !== void 0) throw new Error(`step ${String(outcome.failedAt)} of "${sequence.name}" failed`);
|
|
@@ -37845,6 +38313,11 @@ var NotificationCenter = class {
|
|
|
37845
38313
|
logger: this.logger.child("device-mute"),
|
|
37846
38314
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
37847
38315
|
});
|
|
38316
|
+
this.sequences = new NcSequenceStore({
|
|
38317
|
+
store: deps.store,
|
|
38318
|
+
logger: this.logger.child("sequences"),
|
|
38319
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
38320
|
+
});
|
|
37848
38321
|
this.alertStore = new NcAlertStore({
|
|
37849
38322
|
store: deps.store,
|
|
37850
38323
|
logger: this.logger.child("device-alerts")
|
|
@@ -38111,6 +38584,7 @@ var NotificationCenter = class {
|
|
|
38111
38584
|
await NcStateSummaryService.declare(store);
|
|
38112
38585
|
await NcSnoozeStore.declare(store);
|
|
38113
38586
|
await NcDeviceMuteStore.declare(store);
|
|
38587
|
+
await NcSequenceStore.declare(store);
|
|
38114
38588
|
await TimelapseStore.declare(store);
|
|
38115
38589
|
await NcSummaryStore.declare(store);
|
|
38116
38590
|
await NcCalendarEventsStore.declare(store);
|
|
@@ -38172,6 +38646,8 @@ var NotificationCenter = class {
|
|
|
38172
38646
|
sinceStartMs: now - t0
|
|
38173
38647
|
} });
|
|
38174
38648
|
};
|
|
38649
|
+
await timed("sequences", () => this.sequences.load());
|
|
38650
|
+
await timed("migrateLegacyActions", () => this.migrateLegacyRuleActions());
|
|
38175
38651
|
await Promise.all([
|
|
38176
38652
|
timed("texts", () => this.reloadTexts()),
|
|
38177
38653
|
timed("rules", () => this.rules.load()),
|
|
@@ -39471,7 +39947,32 @@ var NotificationCenter = class {
|
|
|
39471
39947
|
return { success: true };
|
|
39472
39948
|
},
|
|
39473
39949
|
setRuleEnabled: async ({ ruleId, enabled }) => {
|
|
39474
|
-
await this.rules.setEnabled(ruleId, enabled);
|
|
39950
|
+
const updated = await this.rules.setEnabled(ruleId, enabled);
|
|
39951
|
+
const bindings = enabled ? updated.actions?.onActivation : updated.actions?.onDeactivation;
|
|
39952
|
+
if (bindings !== void 0 && bindings.length > 0 && this.actionRunner !== null) this.actionRunner.runBindings({
|
|
39953
|
+
ruleId: updated.id,
|
|
39954
|
+
ruleName: updated.name,
|
|
39955
|
+
deviceId: this.deviceIdForRuleActions(updated),
|
|
39956
|
+
bindings,
|
|
39957
|
+
lookup: (id) => this.sequences.get(id)
|
|
39958
|
+
});
|
|
39959
|
+
return { success: true };
|
|
39960
|
+
},
|
|
39961
|
+
listSequences: async () => ({ sequences: [...this.sequences.list()] }),
|
|
39962
|
+
getSequence: async ({ sequenceId }) => ({ sequence: this.sequences.get(sequenceId) }),
|
|
39963
|
+
createSequence: async ({ sequence }) => {
|
|
39964
|
+
const created = await this.sequences.create(sequence);
|
|
39965
|
+
this.logger.info("notification sequence created", { meta: { sequenceId: created.id } });
|
|
39966
|
+
return { sequence: created };
|
|
39967
|
+
},
|
|
39968
|
+
updateSequence: async ({ sequenceId, sequence }) => {
|
|
39969
|
+
const updated = await this.sequences.update(sequenceId, sequence);
|
|
39970
|
+
this.logger.info("notification sequence updated", { meta: { sequenceId } });
|
|
39971
|
+
return { sequence: updated };
|
|
39972
|
+
},
|
|
39973
|
+
deleteSequence: async ({ sequenceId }) => {
|
|
39974
|
+
await this.sequences.delete(sequenceId);
|
|
39975
|
+
this.logger.info("notification sequence deleted", { meta: { sequenceId } });
|
|
39475
39976
|
return { success: true };
|
|
39476
39977
|
},
|
|
39477
39978
|
testRule: async ({ rule, lookbackMinutes }) => ({ results: [...await this.dryRun(rule, lookbackMinutes)] }),
|
|
@@ -40968,7 +41469,7 @@ var NotificationCenter = class {
|
|
|
40968
41469
|
} : {}
|
|
40969
41470
|
}
|
|
40970
41471
|
});
|
|
40971
|
-
const unresolvable = unresolvableButtons(rule.actions);
|
|
41472
|
+
const unresolvable = unresolvableButtons(rule.actions, this.sequences.knownIds());
|
|
40972
41473
|
if (unresolvable.length > 0) this.logger.warn("rule buttons reference sequences that do not exist — not sent", {
|
|
40973
41474
|
tags: { deviceId: subject.deviceId },
|
|
40974
41475
|
meta: {
|
|
@@ -41095,20 +41596,85 @@ var NotificationCenter = class {
|
|
|
41095
41596
|
return this.groupStore.list(query);
|
|
41096
41597
|
}
|
|
41097
41598
|
/**
|
|
41098
|
-
*
|
|
41599
|
+
* Materialise legacy inline `onTrigger` sequences into the shared store and
|
|
41600
|
+
* rewrite each rule's actions to bindings + `buttons.sequenceId`. Runs on RAW
|
|
41601
|
+
* rows before {@link NcRuleStore.load} so Zod never skips them.
|
|
41602
|
+
*/
|
|
41603
|
+
async migrateLegacyRuleActions() {
|
|
41604
|
+
let rows;
|
|
41605
|
+
try {
|
|
41606
|
+
rows = await this.deps.store.query.query({
|
|
41607
|
+
collection: NC_RULES_COLLECTION,
|
|
41608
|
+
filter: { limit: 1e4 }
|
|
41609
|
+
});
|
|
41610
|
+
} catch (err) {
|
|
41611
|
+
this.logger.warn("legacy actions migration skipped — could not read rules", { meta: { error: String(err) } });
|
|
41612
|
+
return;
|
|
41613
|
+
}
|
|
41614
|
+
let migrated = 0;
|
|
41615
|
+
for (const row of rows) {
|
|
41616
|
+
const ruleBlob = row.data["rule"];
|
|
41617
|
+
if (typeof ruleBlob !== "object" || ruleBlob === null || Array.isArray(ruleBlob)) continue;
|
|
41618
|
+
const rule = { ...ruleBlob };
|
|
41619
|
+
const ruleId = typeof rule["id"] === "string" ? rule["id"] : void 0;
|
|
41620
|
+
if (ruleId === void 0) continue;
|
|
41621
|
+
const result = await migrateLegacyRuleActions(rule["actions"], async (input) => {
|
|
41622
|
+
return { id: (await this.sequences.create(input)).id };
|
|
41623
|
+
});
|
|
41624
|
+
if (!result.migrated) continue;
|
|
41625
|
+
if (result.actions !== void 0) rule["actions"] = result.actions;
|
|
41626
|
+
else delete rule["actions"];
|
|
41627
|
+
const { id: _rowId, ...columns } = row.data;
|
|
41628
|
+
await this.deps.store.set.mutate({
|
|
41629
|
+
collection: NC_RULES_COLLECTION,
|
|
41630
|
+
key: ruleId,
|
|
41631
|
+
value: {
|
|
41632
|
+
...columns,
|
|
41633
|
+
rule
|
|
41634
|
+
}
|
|
41635
|
+
});
|
|
41636
|
+
migrated += 1;
|
|
41637
|
+
}
|
|
41638
|
+
if (migrated > 0) this.logger.info("migrated legacy inline rule actions into shared sequences", { meta: {
|
|
41639
|
+
rules: migrated,
|
|
41640
|
+
sequences: this.sequences.list().length
|
|
41641
|
+
} });
|
|
41642
|
+
}
|
|
41643
|
+
/**
|
|
41644
|
+
* True when this match is a clear / leave edge — the subject for
|
|
41645
|
+
* `onTriggerOff`. Raise / enter / ordinary matches use `onTrigger`.
|
|
41646
|
+
*/
|
|
41647
|
+
isTriggerOffSubject(subject) {
|
|
41648
|
+
if (subject.sensorRaised === false) return true;
|
|
41649
|
+
if (subject.occupancy !== void 0 && subject.occupancy.occupied === false) return true;
|
|
41650
|
+
return false;
|
|
41651
|
+
}
|
|
41652
|
+
/** Device id for activation/deactivation log tags — first scoped device, else 0. */
|
|
41653
|
+
deviceIdForRuleActions(rule) {
|
|
41654
|
+
const devices = rule.conditions.devices;
|
|
41655
|
+
if (devices !== void 0 && devices.length > 0) {
|
|
41656
|
+
const first = devices[0];
|
|
41657
|
+
if (first !== void 0) return first;
|
|
41658
|
+
}
|
|
41659
|
+
return 0;
|
|
41660
|
+
}
|
|
41661
|
+
sequenceActionsOf = (sequenceId) => this.sequences.get(sequenceId)?.actions;
|
|
41662
|
+
/**
|
|
41663
|
+
* Run this rule's onTrigger / onTriggerOff bindings for this match.
|
|
41099
41664
|
*
|
|
41100
41665
|
* Called BEFORE the cooldown gate — see the call site for why. Everything
|
|
41101
41666
|
* here is fire-and-forget and nothing may throw into the evaluation.
|
|
41102
41667
|
*/
|
|
41103
41668
|
runRuleActions(rule, subject, kind, controls) {
|
|
41104
|
-
const sequences = rule.actions?.onTrigger;
|
|
41105
|
-
if (sequences === void 0 || sequences.length === 0) return;
|
|
41106
41669
|
if (this.actionRunner === null || controls.runActions === false) return;
|
|
41107
|
-
this.
|
|
41670
|
+
const bindings = this.isTriggerOffSubject(subject) ? rule.actions?.onTriggerOff : rule.actions?.onTrigger;
|
|
41671
|
+
if (bindings === void 0 || bindings.length === 0) return;
|
|
41672
|
+
this.actionRunner.runBindings({
|
|
41108
41673
|
ruleId: rule.id,
|
|
41109
41674
|
ruleName: rule.name,
|
|
41110
41675
|
deviceId: subject.deviceId,
|
|
41111
|
-
|
|
41676
|
+
bindings,
|
|
41677
|
+
lookup: (id) => this.sequences.get(id),
|
|
41112
41678
|
origin: fireRefFor(rule, subject, kind)
|
|
41113
41679
|
});
|
|
41114
41680
|
}
|
|
@@ -42012,7 +42578,8 @@ var NotificationCenter = class {
|
|
|
42012
42578
|
snoozeDecision(input) {
|
|
42013
42579
|
const verdict = snoozeVerdict(input, {
|
|
42014
42580
|
activeFor: (query) => this.snoozes.activeFor(query),
|
|
42015
|
-
ruleFor: (ruleId) => this.rules.get(ruleId)
|
|
42581
|
+
ruleFor: (ruleId) => this.rules.get(ruleId),
|
|
42582
|
+
sequenceActionsOf: this.sequenceActionsOf
|
|
42016
42583
|
});
|
|
42017
42584
|
if (verdict.kind === "exempt") this.logger.info("snooze does not apply — alarm rules are never silenced", {
|
|
42018
42585
|
tags: { deviceId: input.deviceId },
|