@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.
Files changed (15) hide show
  1. package/dist/{clip-model-registry-BFbP9U7z.js → clip-model-registry-8IMzj7jJ.js} +5674 -5032
  2. package/dist/{clip-model-registry-CwK2SLQD.mjs → clip-model-registry-BENGs4rw.mjs} +5441 -4835
  3. package/dist/embedding-encoder/index.js +1 -1
  4. package/dist/embedding-encoder/index.mjs +1 -1
  5. package/dist/pipeline-analytics/_stub.js +2 -2
  6. 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
  7. 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
  8. 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
  9. package/dist/pipeline-analytics/{hostInit-DtKO_7UT.mjs → hostInit-D5UUhDKa.mjs} +2 -2
  10. package/dist/pipeline-analytics/index.js +673 -106
  11. package/dist/pipeline-analytics/index.mjs +673 -106
  12. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  13. package/package.json +1 -1
  14. 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
  15. 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 isEventOwnerType, $r as defineCustomActions, $t as SCENE_DEFAULT_QUIET_SECONDS, A as EMBEDDING_REQUIREMENTS_FAILED, An as consumableLowPercentOf, Ar as servedTemplateVars, At as NcRuleUpdateInputSchema, B as ListTrackRelationsInputSchema, Br as templateVarsFor, Bt as NcTemplateVarDescriptorSchema, C as DEFAULT_EVENT_COLOR, Cn as applyNonBlockingEdit, Cr as ruleBelongsToAlarm$1, Ct as NcAlarmSettingsPatchSchema, D as DeclaredDevices, Dn as audioModeOf, Dr as sceneMonitorCapability, Dt as NcRulePatchSchema, E as DETECTION_PIPELINE_CAP_NAME, En as audioMetricsCapability, Er as ruleKindOf, Et as NcRuleInputSchema, F as EXPORT_DENSE_MAX_RANGES, Fn as describeSystemEventDeliveryProblem, Fr as systemEventFamilyProblems, Ft as NcStateSummarySectionIdSchema, G as MAX_BIRTH_LATENCY_PROXY_MS, Gn as faceGalleryCapability, Gr as evaluateSensorEdge, Gt as RECORDING_SOURCE_CAMSTACK_ADDON, H as LockStateSchema, Hn as encodeVectorBase64, Hr as zoneAnalyticsCapability, Ht as OpsLogEntrySchema, I as FIRST_LEVEL_MACRO_CLASSES, In as describeSystemEventFamilyProblem, Ir as systemEventFilterApplies, It as NcSystemEventKindSchema, J as MOTION_CLOSE_AFTER_MS, Jn as gifFromMp4Args, Jr as errMsg, Jt as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, K as MAX_TRACK_PURITIES, Kn as failureContributionCapability, Kr as isSourceCap, Kt as RebuildObjectEmbeddingsRefusalSchema, L as FULL_IMAGE_BBOX, Lr as systemEventSummaryScheduleOf, Lt as NcTaxonomySchema, M as EVENT_DENSITY_BATCH_MAX, Mn as createAlarmProfile, Mr as subKindsOf, Mt as NcSnoozeInputSchema, N as EVENT_KIND_BY_CAP, Nn as deleteAlarmProfile, Nr as syncActiveAlarmProfileSettings, Nt as NcSnoozeSchema, O as DetectionSourceSchema, On as buildEventKindDescriptor, Or as secureActuationFor, Ot as NcRuleSchema, P as EVENT_OWNER_TYPES, Pn as deriveRecordingMode, Pr as systemEventDeliveryProblems, Pt as NcSnoozeSuppressedSchema, Q as NC_ALARM_DELAY_FIELDS, Qn as isDetectionMacroClass, Qr as customAction, Qt as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, R as FailureCounters, Rr as systemEventSummarySplitOf, Rt as NcTemplatePreviewInputSchema, S as CoverStateSchema, Si as EventCategory, Sn as applyModeDelaysEdits, St as NcAlarmSensorsSchema, T as DETECTION_MACRO_CLASSES, Tn as assertTimelapseCadences, Tr as ruleGroupingApplies, Tt as NcConditionsSchema, U as MAX_ARCHIVED_DEBUG_NOTES, Un as ensureAlarmProfiles, Ur as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, V as ListTrackRelationsResultSchema, Vr as vectorDimFromBase64, Vt as OPS_LOG_DEFAULT_LIMIT, W as MAX_BIRTH_DECISION_RECORDS, Wn as evictionPolicyOfLocation, Wr as canonicalHash, Wt as RECORDING_EXPORT_MAX_READ_BYTES, X as MotionTriggeredBySchema, Xn as isAudioClassName, Xr as CamProfileSchema, Xt as SCENE_DEFAULT_ANCHOR_THRESHOLD, Y as MediaFileKindEnum, Yn as isAlarmControllableCap, Yr as BaseAddon, Yt as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, Z as NC_ALARM_DEFAULT_PROFILE_ID, Zn as isDefaultAlarmProfileId, Zr as createEvent, Zt as SCENE_DEFAULT_CHECK_INTERVAL_SEC, _ as BaseDevice, _i as string, _n as activateAlarmProfile, _r as readTimelapseGeneratedAt, _t as NC_TANK_SYSTEM_EVENT_KINDS, a as CLIP_EMBEDDING_STEP_ID, ai as ZodObject, an as SetTrackPurityResultSchema, ar as isSystemEventSummaryRule, at as NC_CLIP_PRE_ROLL_DEFAULT_SEC, b as ClearTrackRelationInputSchema, bi as union, bn as alarmPanelCapability, br as resolveLocationMode, bt as NcAlarmConfigSchema, c as resolveClusterModelPin, ci as boolean, cn as TRACK_RELATIONS_DEFAULT_LIMIT, cr as mayWriteToLocation, ct as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, d as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, di as looseObject, dn as TimelapseRuleSchema, dr as occupancyScope, dt as NC_GROUPING_DEFAULT_SEC, ei as hydrateSchema, en as SCENE_DEFAULT_UNCOVERED_POLICY, er as isKnownTemplateVar, et as NC_ALARM_SYSTEM_EVENT_KINDS, f as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, fi as number, fn as TrackPuritySchema, ft as NC_GROUPING_MAX_SEC, g as BIRTH_DECISION_DEFAULT_LIMIT, gi as strictObject, gn as TrackSourceSchema, gr as readMotionTriggeredBy, gt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, h as ArchivedDebugNoteSchema, hi as record, hn as TrackSideSchema, hr as readDeviceStateFrom, ht as NC_SNOOZE_MAX_MINUTES, ii as sleep$1, in as SetTrackPurityInputSchema, ir as isSystemDelivery, it as NC_CLIP_POST_ROLL_DEFAULT_SEC, j as EMBEDDING_REQUIREMENTS_UNAVAILABLE, jn as cosineSimilarity$1, jr as storageOccupancyCapability, jt as NcScheduleSchema, k as EMBEDDING_MODEL_FAILED, kn as cloneAlarmSettings, kr as sensorRaisedOf, kt as NcRuleTargetSchema, l as APPEARANCE_BIN_COUNT, li as discriminatedUnion, ln as TimelapseRuleInputSchema, lr as memoizePinnedFfmpeg, lt as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, m as AlarmArmModeSchema, mi as partialRecord, mn as TrackRelationSchema, mr as plateGalleryCapability, mt as NC_SENSOR_TRANSITION_KINDS, ni as nodePin, nn as SCENE_RESET_RECAPTURES, nr as isRetiredClearKey, nt as NC_AUDIO_CONFIRM_HITS_DEFAULT, o as FACE_EMBEDDING_STEP_ID, oi as _enum, on as SetTrackRelationInputSchema, or as isVectorIndexDimConflict, ot as NC_CLIP_RETENTION_MAX_SEC, p as AUDIO_MACRO_LABELS, pi as object, pn as TrackRelationKindSchema, pr as pipelineAnalyticsCapability, pt as NC_GROUPING_MIN_SEC, q as MAX_TRACK_RELATIONS, qn as gifDeriveSpeed, qr as DeviceType, qt as RetrainStatusSchema, rn as SceneMonitorSchema, rr as isScheduleActive, rt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, s as readClusterModelPinStrict, si as array, sn as TIMELAPSE_DENSE_FLOOR_SEC, sr as kebabToCamel, st as NC_CONDITION_CATALOG, t as BUILTIN_CLIP_MODELS, ti as isDeviceScopedCap, tn as SCENE_DIVERGED, tr as isMediaOwnerType, tt as NC_ALARM_TRIGGERED_MAX_SEC, u as APPEARANCE_BLOCKS, ui as literal, un as TimelapseRulePatchSchema, ur as notificationRulesCapability, ut as NC_DEFAULT_SNOOZE_MINUTES, v as BirthDecisionRecordSchema, vi as templateLiteral, vn as addonWidgetsSourceCapability, vr as renameAlarmProfile, vt as NC_TAXONOMY, w as DEFAULT_TIMELAPSE_PREVIEW_TEXT, wn as applySecureOnArmEdit, wr as ruleDeviceCompatCapQueries, wt as NcConditionDescriptorSchema, x as ClearTrackRelationResultSchema, xi as unknown, xn as applyBlockingEdit, xr as resolveModeDelays, xt as NcAlarmSensorsInputSchema, y as COCO_TO_MACRO, yi as tuple, yn as alarmDeviceSecureOnArm, yr as resolveClipSearchMinScore, yt as NC_TEMPLATE_VARS, z as LabelAttributionSchema, zn as embeddingRefusalOf, zr as templateCarriesSummary, zt as NcTemplatePreviewSchema } from "../clip-model-registry-CwK2SLQD.mjs";
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
- sequence: grant.sequence
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
- * A class no locale names falls through to `class.other`, which prints the RAW
5072
- * name: a custom model's class must appear in the sentence rather than vanish
5073
- * out of it, and a missing translation is a smaller defect than a missing
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 key = `class.${normaliseClass(className)}`;
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 actuatesAlarmPanel(sequences) {
8671
- for (const sequence of sequences) for (const step of sequence.actions) if (step.kind === "cap" && step.cap === ALARM_PANEL_CAP$1) return true;
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: the gate reads it
8678
- * on the delivery path, where a fallible lookup would be a read that can
8679
- * DESTROY the exemption (D49) — and the failure direction of this one is
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 actuatesAlarmPanel(rule.actions?.onTrigger ?? []);
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 = declaredSequences(actions);
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.sequence)) continue;
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
- sequence: button.sequence,
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 it does not have.
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
- const known = declaredSequences(actions);
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?.onTrigger !== void 0 ? { onTrigger: rule.actions.onTrigger } : {}
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$7(value) {
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$7(slice)) return null;
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$7(raw)) continue;
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 sequence's device steps, dead ones dropped. `null` when none was dead. */
20427
- function pruneSequence(sequence, isDead) {
20428
- const dead = [];
20429
- const kept = sequence.actions.filter((step) => {
20430
- if (step.kind !== "cap" || !isDead(step.deviceId)) return true;
20431
- dead.push(step.deviceId);
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 removedNames = /* @__PURE__ */ new Set();
20455
- const nextSequences = [];
20456
- for (const sequence of sequences) {
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
- changed = true;
20463
- const field = `actions.onTrigger.${sequence.name}`;
20464
- if (pruned.next === "removed") {
20465
- removedNames.add(sequence.name);
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 null;
20473
- const buttons = actions.buttons;
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
- ...actions,
20476
- onTrigger: nextSequences,
20477
- ...buttons !== void 0 ? { buttons: buttons.filter((button) => !removedNames.has(button.sequence)) } : {}
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 `${ruleId}:${sequenceName}` — the throttle's memory. */
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
- * Run every enabled sequence, in order.
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. The operator finds out from the log and from the
28869
- * device not having moved — not from silence on both.
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(key);
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(key, now);
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
- mintUrl: ({ sequence, actionId }) => {
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
- sequence,
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 = (rule.actions?.onTrigger ?? []).find((s) => s.name === grant.sequence);
37721
- if (sequence === void 0) throw new Error(`sequence "${grant.sequence}" no longer exists on rule ${rule.name}`);
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
- * Run this rule's `onTrigger` sequences for this match.
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.actionRunner.run({
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
- sequences,
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 },