@camstack/addon-post-analysis 1.2.327 → 1.2.330

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.
@@ -1,4 +1,4 @@
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-Cn5n0a0Q.mjs";
1
+ import { $ as NC_ALARM_DELAY_MAX_SEC, $n as failureContributionCapability, $r as canonicalHash, $t as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, A as EMBEDDING_REQUIREMENTS_FAILED, Ai as unknown, An as assertTimelapseCadences, At as NcRuleInputSchema, B as ListTrackRelationsInputSchema, Bn as deriveRecordingMode, Br as storageOccupancyCapability, Bt as NcStateSummarySectionIdSchema, C as DEFAULT_EVENT_COLOR, Ci as partialRecord, Cn as addonWidgetsSourceCapability, Cr as readDeviceStateFrom, Ct as NcAlarmSensorsSchema, D as DeclaredDevices, Di as templateLiteral, Dn as applyModeDelaysEdits, Dr as resolveClipSearchMinScore, Dt as NcLegacyRuleActionSequenceSchema, E as DETECTION_PIPELINE_CAP_NAME, Ei as string, En as applyBlockingEdit, Er as renameAlarmProfile, Et as NcConditionsSchema, F as EXPORT_DENSE_MAX_RANGES, Fn as consumableLowPercentOf, Fr as ruleKindOf, Ft as NcScheduleSchema, G as MAX_BIRTH_LATENCY_PROXY_MS, Gn as embeddingRefusalOf, Gr as systemEventFilterApplies, Gt as NcTemplateVarDescriptorSchema, H as LockStateSchema, Hn as describeSystemEventFamilyProblem, Hr as syncActiveAlarmProfileSettings, Ht as NcTaxonomySchema, I as FIRST_LEVEL_MACRO_CLASSES, In as cosineSimilarity$1, Ir as sceneMonitorCapability, It as NcSharedSequenceSchema, J as MOTION_CLOSE_AFTER_MS, Jn as encodeVectorBase64, Jr as templateCarriesSummary, K as MAX_TRACK_PURITIES, Kr as systemEventSummaryScheduleOf, Kt as OPS_LOG_DEFAULT_LIMIT, L as FULL_IMAGE_BBOX, Ln as createAlarmProfile, Lr as secureActuationFor, Lt as NcSnoozeInputSchema, M as EVENT_DENSITY_BATCH_MAX, Mn as audioModeOf, Mr as ruleBelongsToAlarm$1, Mt as NcRuleSchema, N as EVENT_KIND_BY_CAP, Nn as buildEventKindDescriptor, Nr as ruleDeviceCompatCapQueries, Nt as NcRuleTargetSchema, O as DetectionSourceSchema, Oi as tuple, On as applyNonBlockingEdit, Or as resolveLocationMode, Ot as NcRuleActionSchema, P as EVENT_OWNER_TYPES, Pn as cloneAlarmSettings, Pr as ruleGroupingApplies, Pt as NcRuleUpdateInputSchema, Q as NC_ALARM_DELAY_FIELDS, Qn as faceGalleryCapability, Qr as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, Qt as RetrainStatusSchema, R as FailureCounters, Rn as deleteAlarmProfile, Rr as sensorRaisedOf, Rt as NcSnoozeSchema, S as CoverStateSchema, Si as object, Sn as activateAlarmProfile, Sr as plateGalleryCapability, St as NcAlarmSensorsInputSchema, T as DETECTION_MACRO_CLASSES, Ti as strictObject, Tn as alarmPanelCapability, Tr as readTimelapseGeneratedAt, Tt as NcConditionDescriptorSchema, U as MAX_ARCHIVED_DEBUG_NOTES, Ur as systemEventDeliveryProblems, Ut as NcTemplatePreviewInputSchema, V as ListTrackRelationsResultSchema, Vn as describeSystemEventDeliveryProblem, Vr as subKindsOf, Vt as NcSystemEventKindSchema, W as MAX_BIRTH_DECISION_RECORDS, Wr as systemEventFamilyProblems, Wt as NcTemplatePreviewSchema, X as MotionTriggeredBySchema, Xn as eventKindLabel, Xr as vectorDimFromBase64, Xt as RECORDING_SOURCE_CAMSTACK_ADDON, Y as MediaFileKindEnum, Yn as ensureAlarmProfiles, Yr as templateVarsFor, Yt as RECORDING_EXPORT_MAX_READ_BYTES, Z as NC_ALARM_DEFAULT_PROFILE_ID, Zn as evictionPolicyOfLocation, Zr as zoneAnalyticsCapability, Zt as RebuildObjectEmbeddingsRefusalSchema, _ as BaseDevice, _i as boolean, _n as TrackPuritySchema, _r as memoizePinnedFfmpeg, _t as NC_TANK_SYSTEM_EVENT_KINDS, a as CLIP_EMBEDDING_STEP_ID, ai as CamProfileSchema, an as SCENE_DEFAULT_UNCOVERED_POLICY, ar as isDetectionMacroClass, at as NC_CLIP_PRE_ROLL_DEFAULT_SEC, b as ClearTrackRelationInputSchema, bi as looseObject, bn as TrackSideSchema, bt as NcAlarmConfigSchema, c as resolveClusterModelPin, ci as defineCustomActions, cn as SceneMonitorSchema, cr as isKnownTemplateVar, ct as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, d as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, di as nodePin, dn as SetTrackRelationInputSchema, dr as isScheduleActive, dt as NC_GROUPING_DEFAULT_SEC, ei as evaluateSensorEdge, en as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, er as gifDeriveSpeed, et as NC_ALARM_SYSTEM_EVENT_KINDS, f as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, fn as TIMELAPSE_DENSE_FLOOR_SEC, fr as isSystemDelivery, ft as NC_GROUPING_MAX_SEC, g as BIRTH_DECISION_DEFAULT_LIMIT, gi as array, gn as TimelapseRuleSchema, gr as mayWriteToLocation, gt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, h as ArchivedDebugNoteSchema, hi as _enum, hn as TimelapseRulePatchSchema, hr as kebabToCamel, ht as NC_SNOOZE_MAX_MINUTES, ii as BaseAddon, in as SCENE_DEFAULT_QUIET_SECONDS, ir as isDefaultAlarmProfileId, it as NC_CLIP_POST_ROLL_DEFAULT_SEC, j as EMBEDDING_REQUIREMENTS_UNAVAILABLE, ji as EventCategory, jn as audioMetricsCapability, jr as ruleAutoTriggersAlarm, jt as NcRulePatchSchema, k as EMBEDDING_MODEL_FAILED, ki as union, kn as applySecureOnArmEdit, kr as resolveModeDelays, kt as NcRuleActionsSchema, l as APPEARANCE_BIN_COUNT, li as hydrateSchema, ln as SetTrackPurityInputSchema, lr as isMediaOwnerType, lt as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, m as AlarmArmModeSchema, mi as ZodObject, mn as TimelapseRuleInputSchema, mr as isVectorIndexDimConflict, mt as NC_SENSOR_TRANSITION_KINDS, ni as DeviceType, nn as SCENE_DEFAULT_CHECK_INTERVAL_SEC, nr as isAlarmControllableCap, nt as NC_AUDIO_CONFIRM_HITS_DEFAULT, o as FACE_EMBEDDING_STEP_ID, oi as createEvent, on as SCENE_DIVERGED, or as isEventKindLabelKey, ot as NC_CLIP_RETENTION_MAX_SEC, p as AUDIO_MACRO_LABELS, pi as sleep$1, pn as TRACK_RELATIONS_DEFAULT_LIMIT, pr as isSystemEventSummaryRule, pt as NC_GROUPING_MIN_SEC, q as MAX_TRACK_RELATIONS, qr as systemEventSummarySplitOf, qt as OpsLogEntrySchema, ri as errMsg, rn as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, rr as isAudioClassName, rt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, s as readClusterModelPinStrict, si as customAction, sn as SCENE_RESET_RECAPTURES, sr as isEventOwnerType, st as NC_CONDITION_CATALOG, t as BUILTIN_CLIP_MODELS, ti as isSourceCap, tn as SCENE_DEFAULT_ANCHOR_THRESHOLD, tr as gifFromMp4Args, tt as NC_ALARM_TRIGGERED_MAX_SEC, u as APPEARANCE_BLOCKS, ui as isDeviceScopedCap, un as SetTrackPurityResultSchema, ur as isRetiredClearKey, ut as NC_DEFAULT_SNOOZE_MINUTES, v as BirthDecisionRecordSchema, vi as discriminatedUnion, vn as TrackRelationKindSchema, vr as notificationRulesCapability, vt as NC_TAXONOMY, w as DEFAULT_TIMELAPSE_PREVIEW_TEXT, wi as record, wn as alarmDeviceSecureOnArm, wr as readMotionTriggeredBy, wt as NcAlarmSettingsPatchSchema, x as ClearTrackRelationResultSchema, xi as number, xn as TrackSourceSchema, xr as pipelineAnalyticsCapability, xt as NcAlarmPostTriggeredActionSchema, y as COCO_TO_MACRO, yi as literal, yn as TrackRelationSchema, yr as occupancyScope, yt as NC_TEMPLATE_VARS, z as LabelAttributionSchema, zn as derivePostTriggeredAction, zr as servedTemplateVars, zt as NcSnoozeSuppressedSchema } from "../clip-model-registry-CGaaUvAD.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";
@@ -652,7 +652,8 @@ var NcAlarmArmOutcomeSchema = discriminatedUnion("outcome", [
652
652
  object({
653
653
  outcome: literal("rejected"),
654
654
  reason: NcAlarmArmAnywayRejectionSchema
655
- }).readonly()
655
+ }).readonly(),
656
+ object({ outcome: literal("outside-schedule") }).readonly()
656
657
  ]);
657
658
  /**
658
659
  * The devices a mode covers, ascending and de-duplicated.
@@ -1511,7 +1512,25 @@ function modeDelaysFromPersisted(raw) {
1511
1512
  }
1512
1513
  kept[field] = value;
1513
1514
  }
1514
- if (kept.exitDelaySec !== void 0 || kept.entryDelaySec !== void 0 || kept.triggeredDurationSec !== void 0) entries.push(kept);
1515
+ if (item.postTriggeredAction !== void 0) {
1516
+ const action = NcAlarmPostTriggeredActionSchema.safeParse(item.postTriggeredAction);
1517
+ if (action.success) kept.postTriggeredAction = action.data;
1518
+ else dropped.push({
1519
+ mode: item.mode,
1520
+ why: "out-of-bounds",
1521
+ field: "triggeredDurationSec"
1522
+ });
1523
+ }
1524
+ if (item.schedule !== void 0) {
1525
+ const schedule = NcScheduleSchema.safeParse(item.schedule);
1526
+ if (schedule.success) kept.schedule = schedule.data;
1527
+ else dropped.push({
1528
+ mode: item.mode,
1529
+ why: "out-of-bounds",
1530
+ field: "schedule"
1531
+ });
1532
+ }
1533
+ if (kept.exitDelaySec !== void 0 || kept.entryDelaySec !== void 0 || kept.triggeredDurationSec !== void 0 || kept.postTriggeredAction !== void 0 || kept.schedule !== void 0) entries.push(kept);
1515
1534
  }
1516
1535
  return {
1517
1536
  entries,
@@ -1527,16 +1546,15 @@ function modeDelaysFromPersisted(raw) {
1527
1546
  * (`resolveModeDelays`): `home` has no exit delay on purpose — nobody is
1528
1547
  * leaving — and says so with its own entry, not by this shared value.
1529
1548
  *
1530
- * `triggeredDurationSec: 0` = "sound until somebody disarms it". That is what
1531
- * the panel did before the field existed, so an install that never opens the
1532
- * tab keeps behaving exactly as it did — a default that silently re-armed
1533
- * every existing alarm after N seconds would be a behaviour change nobody
1534
- * asked for.
1549
+ * Default post-triggered action is `rearmAfter` 10 s — every mode inherits
1550
+ * that unless the operator overrides. Older blobs that still store the stock
1551
+ * `0` + absent/`doNothing` seed are migrated once at panel boot (D701).
1535
1552
  */
1536
1553
  var DEFAULT_DELAYS = {
1537
1554
  exitDelaySec: 30,
1538
1555
  entryDelaySec: 20,
1539
- triggeredDurationSec: 0
1556
+ triggeredDurationSec: 10,
1557
+ postTriggeredAction: "rearmAfter"
1540
1558
  };
1541
1559
  var DISARMED = {
1542
1560
  target: "disarmed",
@@ -1554,7 +1572,7 @@ function armedState(mode) {
1554
1572
  * answer is still correct the moment anyone asks.
1555
1573
  */
1556
1574
  function stateAt(m, now) {
1557
- if (m.clearAt !== void 0 && now >= m.clearAt) return m.target;
1575
+ if (m.clearAt !== void 0 && now >= m.clearAt) return m.clearToDisarmed === true ? "disarmed" : m.target;
1558
1576
  if (m.fired) return "triggered";
1559
1577
  if (m.triggerAt !== void 0) return now >= m.triggerAt ? "triggered" : "pending";
1560
1578
  if (m.armedAt !== void 0) return now >= m.armedAt ? m.target : "arming";
@@ -1650,10 +1668,18 @@ function trigger(m, delays, now) {
1650
1668
  changedAt: now
1651
1669
  };
1652
1670
  }
1653
- /** `clearAt` iff the operator asked for a bounded siren — see {@link AlarmDelays}. */
1671
+ /**
1672
+ * `clearAt` (+ optional disarm) when the operator asked for a bounded
1673
+ * post-triggered action — see {@link AlarmDelays}.
1674
+ */
1654
1675
  function autoClear(delays, firesAt) {
1655
- if (delays.triggeredDurationSec <= 0) return {};
1656
- return { clearAt: firesAt + delays.triggeredDurationSec * 1e3 };
1676
+ if (delays.postTriggeredAction === "doNothing") return {};
1677
+ const clearAt = firesAt + Math.max(0, delays.triggeredDurationSec) * 1e3;
1678
+ if (delays.postTriggeredAction === "disableAfter") return {
1679
+ clearAt,
1680
+ clearToDisarmed: true
1681
+ };
1682
+ return { clearAt };
1657
1683
  }
1658
1684
  /**
1659
1685
  * Settle the machine so a stored value never depends on when it is read.
@@ -1663,16 +1689,23 @@ function autoClear(delays, firesAt) {
1663
1689
  * than only in the reading of it.
1664
1690
  */
1665
1691
  function settle(m, now) {
1666
- if (m.clearAt !== void 0 && now >= m.clearAt) return {
1667
- target: m.target,
1668
- fired: false,
1669
- changedAt: m.clearAt
1670
- };
1692
+ if (m.clearAt !== void 0 && now >= m.clearAt) {
1693
+ if (m.clearToDisarmed === true) return {
1694
+ ...DISARMED,
1695
+ changedAt: m.clearAt
1696
+ };
1697
+ return {
1698
+ target: m.target,
1699
+ fired: false,
1700
+ changedAt: m.clearAt
1701
+ };
1702
+ }
1671
1703
  if (m.triggerAt !== void 0 && now >= m.triggerAt) return {
1672
1704
  target: m.target,
1673
1705
  fired: true,
1674
1706
  changedAt: m.triggerAt,
1675
- ...m.clearAt !== void 0 ? { clearAt: m.clearAt } : {}
1707
+ ...m.clearAt !== void 0 ? { clearAt: m.clearAt } : {},
1708
+ ...m.clearToDisarmed === true ? { clearToDisarmed: true } : {}
1676
1709
  };
1677
1710
  if (m.armedAt !== void 0 && now >= m.armedAt) return {
1678
1711
  target: m.target,
@@ -1819,7 +1852,8 @@ function alarmTransition(input) {
1819
1852
  kind: "alarm-disarmed",
1820
1853
  state: next,
1821
1854
  previous,
1822
- at
1855
+ at,
1856
+ ...input.autoCleared === true ? { autoCleared: true } : {}
1823
1857
  };
1824
1858
  return null;
1825
1859
  }
@@ -1910,6 +1944,17 @@ var ncAlarmPanelSchema = object({
1910
1944
  entryDelaySec: number().int().min(0).max(600).default(DEFAULT_DELAYS.entryDelaySec),
1911
1945
  triggeredDurationSec: number().int().min(0).max(3600).default(DEFAULT_DELAYS.triggeredDurationSec),
1912
1946
  /**
1947
+ * What happens when the triggered window ends. Absent on older blobs →
1948
+ * derived from {@link triggeredDurationSec} at read time (never defaulted
1949
+ * here: a Zod default would force `doNothing` onto installs that already
1950
+ * had a positive `triggeredDurationSec` and silently stop re-arming).
1951
+ */
1952
+ postTriggeredAction: _enum([
1953
+ "rearmAfter",
1954
+ "disableAfter",
1955
+ "doNothing"
1956
+ ]).optional(),
1957
+ /**
1913
1958
  * VESTIGIAL (D630): nothing reads it to decide anything any more — every
1914
1959
  * transition is announced to `announceTargets`. Kept so a persisted blob
1915
1960
  * and the settings cap still round-trip it, and read once at boot to tell
@@ -1968,7 +2013,14 @@ var ncAlarmPanelSchema = object({
1968
2013
  mode: string(),
1969
2014
  exitDelaySec: number().optional(),
1970
2015
  entryDelaySec: number().optional(),
1971
- triggeredDurationSec: number().optional()
2016
+ triggeredDurationSec: number().optional(),
2017
+ postTriggeredAction: _enum([
2018
+ "rearmAfter",
2019
+ "disableAfter",
2020
+ "doNothing"
2021
+ ]).optional(),
2022
+ /** Loose — re-validated through {@link modeDelaysFromPersisted}. */
2023
+ schedule: unknown().optional()
1972
2024
  })).default([]),
1973
2025
  /**
1974
2026
  * Active alarm configuration profile id. Empty / absent on a blob older than
@@ -2040,12 +2092,18 @@ function settingsBlobFromPersisted(raw) {
2040
2092
  if (typeof rec["exitDelaySec"] === "number") entry.exitDelaySec = rec["exitDelaySec"];
2041
2093
  if (typeof rec["entryDelaySec"] === "number") entry.entryDelaySec = rec["entryDelaySec"];
2042
2094
  if (typeof rec["triggeredDurationSec"] === "number") entry.triggeredDurationSec = rec["triggeredDurationSec"];
2095
+ if (typeof rec["postTriggeredAction"] === "string") entry.postTriggeredAction = rec["postTriggeredAction"];
2096
+ if (rec["schedule"] !== void 0) entry.schedule = rec["schedule"];
2043
2097
  modeDelaysLoose.push(entry);
2044
2098
  }
2099
+ const truncatedTriggered = Math.max(0, Math.min(3600, Math.trunc(triggered)));
2100
+ const rawAction = raw["postTriggeredAction"];
2101
+ const postTriggeredAction = rawAction === "rearmAfter" || rawAction === "disableAfter" || rawAction === "doNothing" ? rawAction : void 0;
2045
2102
  return {
2046
2103
  exitDelaySec: Math.max(0, Math.min(600, Math.trunc(exit))),
2047
2104
  entryDelaySec: Math.max(0, Math.min(600, Math.trunc(entry))),
2048
- triggeredDurationSec: Math.max(0, Math.min(3600, Math.trunc(triggered))),
2105
+ triggeredDurationSec: truncatedTriggered,
2106
+ ...postTriggeredAction !== void 0 ? { postTriggeredAction } : {},
2049
2107
  announceTargets,
2050
2108
  nonBlocking: lists("nonBlocking"),
2051
2109
  blocking: lists("blocking"),
@@ -2058,6 +2116,7 @@ function settingsToPersisted(settings) {
2058
2116
  exitDelaySec: settings.exitDelaySec,
2059
2117
  entryDelaySec: settings.entryDelaySec,
2060
2118
  triggeredDurationSec: settings.triggeredDurationSec,
2119
+ ...settings.postTriggeredAction !== void 0 ? { postTriggeredAction: settings.postTriggeredAction } : {},
2061
2120
  announceTargets: [...settings.announceTargets],
2062
2121
  nonBlocking: settings.nonBlocking.map((l) => ({
2063
2122
  mode: l.mode,
@@ -2142,11 +2201,13 @@ function machineFromPersisted(raw) {
2142
2201
  const armedAt = optionalInstant(rec["armedAt"]);
2143
2202
  const triggerAt = optionalInstant(rec["triggerAt"]);
2144
2203
  const clearAt = optionalInstant(rec["clearAt"]);
2204
+ const clearToDisarmed = rec["clearToDisarmed"] === true;
2145
2205
  return {
2146
2206
  target: rec["target"],
2147
2207
  ...armedAt !== void 0 ? { armedAt } : {},
2148
2208
  ...triggerAt !== void 0 ? { triggerAt } : {},
2149
2209
  ...clearAt !== void 0 ? { clearAt } : {},
2210
+ ...clearToDisarmed ? { clearToDisarmed: true } : {},
2150
2211
  fired: rec["fired"] === true,
2151
2212
  changedAt: rec["changedAt"]
2152
2213
  };
@@ -2186,6 +2247,7 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2186
2247
  super(ctx, ncAlarmPanelSchema, { type: DeviceType.AlarmPanel });
2187
2248
  this.logDroppedModeDelays();
2188
2249
  this.restore();
2250
+ this.migrateStockAfterTriggerDefaults();
2189
2251
  this.registerAlarmCap();
2190
2252
  this.publish();
2191
2253
  if (this.config.get("announceArm") === false && this.config.get("announceTargets").length > 0) this.ctx.logger.info("alarm announcements now cover every transition (D630) — the announceArm switch is gone", { tags: { deviceId: this.id } });
@@ -2197,10 +2259,36 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2197
2259
  this.config.set("announceTargets", []);
2198
2260
  }
2199
2261
  }
2262
+ /**
2263
+ * One-shot: the pre-D701 stock seed was `triggeredDurationSec: 0` + absent /
2264
+ * `doNothing` (stay triggered forever). That left After trigger looking dead
2265
+ * in the UI. Flip only that seed to {@link DEFAULT_DELAYS} — never a custom
2266
+ * duration or an explicit `disableAfter`.
2267
+ */
2268
+ migrateStockAfterTriggerDefaults() {
2269
+ const duration = this.config.get("triggeredDurationSec");
2270
+ const action = this.config.get("postTriggeredAction");
2271
+ if (!(duration === 0 && (action === void 0 || action === "doNothing"))) return;
2272
+ this.config.set("triggeredDurationSec", DEFAULT_DELAYS.triggeredDurationSec);
2273
+ this.config.set("postTriggeredAction", DEFAULT_DELAYS.postTriggeredAction);
2274
+ this.ctx.logger.info("alarm after-trigger defaults migrated to rearmAfter 10s", {
2275
+ tags: { deviceId: this.id },
2276
+ meta: {
2277
+ fromDurationSec: duration,
2278
+ fromAction: action ?? "absent",
2279
+ toDurationSec: DEFAULT_DELAYS.triggeredDurationSec,
2280
+ toAction: DEFAULT_DELAYS.postTriggeredAction
2281
+ }
2282
+ });
2283
+ }
2200
2284
  /** The operator-visible configuration, as the cap serves it. */
2201
2285
  settings() {
2286
+ const storedAction = this.config.get("postTriggeredAction");
2202
2287
  return {
2203
- ...this.readDelays(),
2288
+ exitDelaySec: this.config.get("exitDelaySec"),
2289
+ entryDelaySec: this.config.get("entryDelaySec"),
2290
+ triggeredDurationSec: this.config.get("triggeredDurationSec"),
2291
+ ...storedAction !== void 0 ? { postTriggeredAction: storedAction } : {},
2204
2292
  announceArm: this.config.get("announceArm"),
2205
2293
  announceTargets: [...this.config.get("announceTargets")],
2206
2294
  nonBlocking: modeDeviceListFromPersisted(this.config.get("nonBlocking")),
@@ -2349,6 +2437,7 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2349
2437
  if (patch.exitDelaySec !== void 0) await this.config.set("exitDelaySec", patch.exitDelaySec);
2350
2438
  if (patch.entryDelaySec !== void 0) await this.config.set("entryDelaySec", patch.entryDelaySec);
2351
2439
  if (patch.triggeredDurationSec !== void 0) await this.config.set("triggeredDurationSec", patch.triggeredDurationSec);
2440
+ if (patch.postTriggeredAction !== void 0) await this.config.set("postTriggeredAction", patch.postTriggeredAction);
2352
2441
  if (patch.announceArm !== void 0) await this.config.set("announceArm", patch.announceArm);
2353
2442
  if (patch.announceTargets !== void 0) await this.config.set("announceTargets", [...patch.announceTargets]);
2354
2443
  if (membership !== void 0 && membershipChanged !== null) this.ctx.logger.info("alarm non-blocking list changed", {
@@ -2524,6 +2613,29 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2524
2613
  }
2525
2614
  });
2526
2615
  }
2616
+ if (edit.postTriggeredAction !== void 0) {
2617
+ const from = was?.postTriggeredAction ?? null;
2618
+ const to = now?.postTriggeredAction ?? null;
2619
+ if (from !== to) this.ctx.logger.info("alarm mode delays changed", {
2620
+ tags: { deviceId: this.id },
2621
+ meta: {
2622
+ mode: edit.mode,
2623
+ field: "postTriggeredAction",
2624
+ from,
2625
+ to
2626
+ }
2627
+ });
2628
+ }
2629
+ if (edit.schedule !== void 0) {
2630
+ if ((was?.schedule === void 0 ? null : "set") !== (now?.schedule === void 0 ? null : "set") || edit.schedule !== null) this.ctx.logger.info("alarm mode activation schedule changed", {
2631
+ tags: { deviceId: this.id },
2632
+ meta: {
2633
+ mode: edit.mode,
2634
+ cleared: edit.schedule === null,
2635
+ windows: now?.schedule?.windows.length ?? 0
2636
+ }
2637
+ });
2638
+ }
2527
2639
  }
2528
2640
  }
2529
2641
  /** One INFO line per persisted per-mode delay entry the guard refused to trust. */
@@ -2582,6 +2694,13 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2582
2694
  */
2583
2695
  async armWith(mode) {
2584
2696
  const now = Date.now();
2697
+ if (!this.modeScheduleAllows(mode, now)) {
2698
+ this.ctx.logger.info("alarm arm refused — outside activation schedule", {
2699
+ tags: { deviceId: this.id },
2700
+ meta: { mode }
2701
+ });
2702
+ return { outcome: "outside-schedule" };
2703
+ }
2585
2704
  const read = await this.applyArmGuard(mode);
2586
2705
  if (read.kind === "unguarded") return this.settleArm(mode, null, now);
2587
2706
  if (read.kind === "check-failed") {
@@ -2595,6 +2714,11 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2595
2714
  why: this.exclusionWhyFor(mode, device)
2596
2715
  })), now);
2597
2716
  }
2717
+ /** Absent schedule = always allowed (`isScheduleActive`). */
2718
+ modeScheduleAllows(mode, now) {
2719
+ const schedule = modeDelaysFromPersisted(this.config.get("modeDelays")).entries.find((e) => e.mode === mode)?.schedule;
2720
+ return isScheduleActive(schedule, now);
2721
+ }
2598
2722
  /**
2599
2723
  * The Arm anyway on ONE refusal's notification — the only path that arms
2600
2724
  * around a device that blocks.
@@ -2620,6 +2744,17 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2620
2744
  const reason = refusalId === this.spentRefusalId ? "used" : "unknown-refusal";
2621
2745
  return this.rejectArmAnyway(refusalId, reason);
2622
2746
  }
2747
+ if (!this.modeScheduleAllows(held.mode, now)) {
2748
+ this.refusal = null;
2749
+ this.ctx.logger.info("alarm arm-anyway refused — outside activation schedule", {
2750
+ tags: { deviceId: this.id },
2751
+ meta: {
2752
+ mode: held.mode,
2753
+ refusalId
2754
+ }
2755
+ });
2756
+ return { outcome: "outside-schedule" };
2757
+ }
2623
2758
  if (this.machine.changedAt !== held.panelChangedAt) {
2624
2759
  this.refusal = null;
2625
2760
  return this.rejectArmAnyway(refusalId, "stale");
@@ -3015,11 +3150,12 @@ var NcAlarmPanelDevice = class extends BaseDevice {
3015
3150
  * rather than fired only once somebody looks.
3016
3151
  */
3017
3152
  publish(now = Date.now()) {
3153
+ const autoCleared = this.machine.clearAt !== void 0 && now >= this.machine.clearAt && this.machine.clearToDisarmed === true;
3018
3154
  this.machine = settle(this.machine, now);
3019
3155
  const state = stateAt(this.machine, now);
3020
3156
  this.runtimeState.setCapState("alarm-panel", this.status(now));
3021
3157
  this.persist();
3022
- this.report(state, now);
3158
+ this.report(state, now, autoCleared);
3023
3159
  if (this.machine.armedAt !== void 0 || this.machine.triggerAt !== void 0 || this.machine.clearAt !== void 0) this.startTicking();
3024
3160
  else this.stopTicking();
3025
3161
  }
@@ -3031,7 +3167,7 @@ var NcAlarmPanelDevice = class extends BaseDevice {
3031
3167
  * panel's own state update with it would make a delivery failure look like
3032
3168
  * an alarm failure.
3033
3169
  */
3034
- report(state, now) {
3170
+ report(state, now, autoCleared = false) {
3035
3171
  const previous = this.lastPublished;
3036
3172
  this.lastPublished = state;
3037
3173
  const hook = this.onPublish;
@@ -3046,7 +3182,8 @@ var NcAlarmPanelDevice = class extends BaseDevice {
3046
3182
  at: now,
3047
3183
  ...source !== null ? { source } : {},
3048
3184
  ...arming !== void 0 ? { arming } : {},
3049
- ...pending !== void 0 ? { pending } : {}
3185
+ ...pending !== void 0 ? { pending } : {},
3186
+ ...autoCleared ? { autoCleared: true } : {}
3050
3187
  });
3051
3188
  if (transition?.kind === "alarm-triggered") this.attribution.clear();
3052
3189
  try {
@@ -3132,8 +3269,12 @@ var NcAlarmPanelDevice = class extends BaseDevice {
3132
3269
  }
3133
3270
  /** The global durations plus the per-mode overrides, read from the persisted config NOW. */
3134
3271
  delaySettings() {
3272
+ const storedAction = this.config.get("postTriggeredAction");
3135
3273
  return {
3136
- ...this.readDelays(),
3274
+ exitDelaySec: this.config.get("exitDelaySec"),
3275
+ entryDelaySec: this.config.get("entryDelaySec"),
3276
+ triggeredDurationSec: this.config.get("triggeredDurationSec"),
3277
+ ...storedAction !== void 0 ? { postTriggeredAction: storedAction } : {},
3137
3278
  modeDelays: modeDelaysFromPersisted(this.config.get("modeDelays")).entries
3138
3279
  };
3139
3280
  }
@@ -3143,24 +3284,32 @@ var NcAlarmPanelDevice = class extends BaseDevice {
3143
3284
  */
3144
3285
  delaysFor(mode) {
3145
3286
  const settings = this.delaySettings();
3146
- if (mode === null) return {
3147
- exitDelaySec: settings.exitDelaySec,
3148
- entryDelaySec: settings.entryDelaySec,
3149
- triggeredDurationSec: settings.triggeredDurationSec,
3150
- source: {
3151
- exitDelaySec: "default",
3152
- entryDelaySec: "default",
3153
- triggeredDurationSec: "default"
3154
- }
3155
- };
3287
+ if (mode === null) {
3288
+ const delays = this.readDelays();
3289
+ return {
3290
+ exitDelaySec: delays.exitDelaySec,
3291
+ entryDelaySec: delays.entryDelaySec,
3292
+ triggeredDurationSec: delays.triggeredDurationSec,
3293
+ postTriggeredAction: delays.postTriggeredAction,
3294
+ source: {
3295
+ exitDelaySec: "default",
3296
+ entryDelaySec: "default",
3297
+ triggeredDurationSec: "default",
3298
+ postTriggeredAction: "default"
3299
+ }
3300
+ };
3301
+ }
3156
3302
  return resolveModeDelays(settings, mode);
3157
3303
  }
3158
- /** The three global durations, read from the persisted config. */
3304
+ /** The global durations + post-triggered action, read from the persisted config. */
3159
3305
  readDelays() {
3306
+ const triggeredDurationSec = this.config.get("triggeredDurationSec");
3307
+ const stored = this.config.get("postTriggeredAction");
3160
3308
  return {
3161
3309
  exitDelaySec: this.config.get("exitDelaySec"),
3162
3310
  entryDelaySec: this.config.get("entryDelaySec"),
3163
- triggeredDurationSec: this.config.get("triggeredDurationSec")
3311
+ triggeredDurationSec,
3312
+ postTriggeredAction: derivePostTriggeredAction(stored, triggeredDurationSec)
3164
3313
  };
3165
3314
  }
3166
3315
  startTicking() {
@@ -5140,7 +5289,7 @@ function ncClassNoun(texts, className, count) {
5140
5289
  count,
5141
5290
  vars: { name: className }
5142
5291
  });
5143
- if (isEventKindLabelKey(normalised)) return eventKindLabel(normalised, texts.language);
5292
+ if (isEventKindLabelKey(normalised)) return eventKindLabel(normalised, texts.language).toLocaleLowerCase(texts.language);
5144
5293
  return texts.text({
5145
5294
  key: "class.other",
5146
5295
  count,
@@ -5148,13 +5297,24 @@ function ncClassNoun(texts, className, count) {
5148
5297
  });
5149
5298
  }
5150
5299
  /**
5151
- * The parenthetical half of a labelled subject: a recognised person or plate
5152
- * is printed verbatim; a classification (species, COCO sub, …) goes through
5153
- * the shared event-kind map so "animal (cat)" becomes "animale (Gatto)".
5300
+ * The ONE capitalisation the notification body gets: its first letter, after
5301
+ * every catalog fragment and placeholder has already been rendered lowercase.
5302
+ * Title, fragments and mid-sentence summaries are never passed through here.
5154
5303
  */
5155
- function ncSubjectLabelText(texts, label, labelKind) {
5156
- if (labelKind === "identity" || labelKind === "plate") return label;
5157
- return eventKindLabel(label, texts.language);
5304
+ function ncSentenceCaseBody(body) {
5305
+ if (body.length === 0) return body;
5306
+ const first = body[0];
5307
+ const upper = first.toLocaleUpperCase();
5308
+ if (upper === first) return body;
5309
+ return `${upper}${body.slice(1)}`;
5310
+ }
5311
+ /**
5312
+ * True when `label` is a taxonomy / classification id (species, COCO sub, …)
5313
+ * rather than a person name or plate. Those REPLACE the coarse class in the
5314
+ * subject — "dog" not "animal (dog)", "cane" not "animale (cane)".
5315
+ */
5316
+ function isClassificationLabel(label) {
5317
+ return isEventKindLabelKey(label.trim().toLowerCase());
5158
5318
  }
5159
5319
  /** `person (John)` / `persona (John)` — the thing the notification is about. */
5160
5320
  function ncSubjectText(texts, input) {
@@ -5163,11 +5323,15 @@ function ncSubjectText(texts, input) {
5163
5323
  key: "detection.subject",
5164
5324
  vars: { classNoun }
5165
5325
  });
5326
+ if (input.labelKind !== "identity" && input.labelKind !== "plate" && isClassificationLabel(input.label)) return texts.text({
5327
+ key: "detection.subject",
5328
+ vars: { classNoun: ncClassNoun(texts, input.label, 1) }
5329
+ });
5166
5330
  return texts.text({
5167
5331
  key: subjectLabelKey(input.labelKind),
5168
5332
  vars: {
5169
5333
  classNoun,
5170
- label: ncSubjectLabelText(texts, input.label, input.labelKind)
5334
+ label: input.label
5171
5335
  }
5172
5336
  });
5173
5337
  }
@@ -5757,7 +5921,7 @@ function isAdoptable(record, nowMs) {
5757
5921
  * what `localDayMinute` does for the same input — one behaviour for a bad
5758
5922
  * zone, not two.
5759
5923
  */
5760
- function clockOf$6(atMs, timeZone) {
5924
+ function clockOf$7(atMs, timeZone) {
5761
5925
  if (timeZone !== void 0) try {
5762
5926
  return new Intl.DateTimeFormat("en-GB", {
5763
5927
  timeZone,
@@ -5893,7 +6057,7 @@ var TimelapseRenderer = class {
5893
6057
  includeAudio: false,
5894
6058
  maxLifeMs: await this.ports.exportLifeMs(),
5895
6059
  deleteAfterDownload: false,
5896
- title: `${request.ruleName} ${clockOf$6(window.startMs, request.timezone)}→${clockOf$6(window.endMs, request.timezone)}`,
6060
+ title: `${request.ruleName} ${clockOf$7(window.startMs, request.timezone)}→${clockOf$7(window.endMs, request.timezone)}`,
5897
6061
  timelapse: {
5898
6062
  everyMs,
5899
6063
  outputFps: request.framerate,
@@ -6224,7 +6388,7 @@ var TimelapseRenderer = class {
6224
6388
  ...meta,
6225
6389
  unservedWindowCount: verdict.unservedWindows.length,
6226
6390
  unservedWindows: verdict.unservedWindows.slice(0, 12).map((w) => ({
6227
- at: clockOf$6(request.window.startMs + w.fromSec * 1e3, request.timezone),
6391
+ at: clockOf$7(request.window.startMs + w.fromSec * 1e3, request.timezone),
6228
6392
  fromSec: Math.round(w.fromSec),
6229
6393
  toSec: Math.round(w.toSec),
6230
6394
  expectedDenseFrames: w.expectedDenseFrames,
@@ -8805,6 +8969,7 @@ function buildActionButtons(input) {
8805
8969
  sequenceId: button.sequenceId,
8806
8970
  actionId: button.id
8807
8971
  }),
8972
+ mode: "background",
8808
8973
  ...button.icon !== void 0 ? { icon: button.icon } : {},
8809
8974
  ...button.destructive !== void 0 ? { destructive: button.destructive } : {}
8810
8975
  });
@@ -8920,10 +9085,9 @@ var MAX_ARM_BUTTONS = 3;
8920
9085
  * (`ALARM_DISARM_TTL_MS`). Both exist: the hint is for the person, the TTL is
8921
9086
  * for the forwarded screenshot.
8922
9087
  *
8923
- * Arm buttons are `background` — arming from the lock screen should not open a
8924
- * browser. The disarm is `navigate`: it removes a protection, and the operator
8925
- * doing it deserves to SEE that it happened rather than trust a POST nobody
8926
- * confirmed.
9088
+ * Every alarm button is `background` — Zentik (and ntfy) fire a POST and leave
9089
+ * the lock screen alone. Opening a browser to arm or disarm costs more
9090
+ * attention than the notification did.
8927
9091
  *
8928
9092
  * `panel.availableModes` ARE the enabled modes: CamStack has no per-mode
8929
9093
  * disable setting, so the Arm buttons offer exactly what the panel declares.
@@ -8957,14 +9121,14 @@ function buildAlarmButtons(input) {
8957
9121
  }),
8958
9122
  icon: "disarm",
8959
9123
  destructive: true,
8960
- mode: "navigate"
9124
+ mode: "background"
8961
9125
  }];
8962
9126
  }
8963
9127
  /**
8964
9128
  * The one Arm anyway a refusal notification carries — or none, when the row
8965
- * names no refusal. `navigate` and destructive, like the disarm: it arms
8966
- * AROUND an open door, and the person doing it should see it happen. Its TTL
8967
- * is the refusal's own (`ARM_ANYWAY_TTL_MS`, priced by the minter).
9129
+ * names no refusal. Destructive + background: it arms around an open door via
9130
+ * POST (Zentik BACKGROUND_CALL), same as disarm. Its TTL is the refusal's own
9131
+ * (`ARM_ANYWAY_TTL_MS`, priced by the minter).
8968
9132
  */
8969
9133
  function armAnywayButton(refusalId, input) {
8970
9134
  if (refusalId === void 0) return [];
@@ -8979,7 +9143,7 @@ function armAnywayButton(refusalId, input) {
8979
9143
  }),
8980
9144
  icon: "arm",
8981
9145
  destructive: true,
8982
- mode: "navigate"
9146
+ mode: "background"
8983
9147
  }];
8984
9148
  }
8985
9149
  //#endregion
@@ -12901,6 +13065,10 @@ function alarmTextKeys(transition, body) {
12901
13065
  titleKey,
12902
13066
  bodyKey: `system.${kind}.body.${transition.sensor.event}`
12903
13067
  };
13068
+ if (kind === "alarm-disarmed" && transition.autoCleared === true) return {
13069
+ titleKey,
13070
+ bodyKey: `system.${kind}.body.autoCleared`
13071
+ };
12904
13072
  if (kind === "alarm-disarmed" && (transition.previous === "triggered" || transition.previous === "pending")) return {
12905
13073
  titleKey,
12906
13074
  bodyKey: `system.${kind}.body.defused`
@@ -14620,13 +14788,14 @@ var en_default = {
14620
14788
  "system.alarm-arming.title.away": "Arming away — {{seconds}}s",
14621
14789
  "system.alarm-arming.title.night": "Arming night — {{seconds}}s",
14622
14790
  "system.alarm-arming.body": "Arming",
14623
- "system.alarm-armed.title.home": "Armed: home",
14624
- "system.alarm-armed.title.away": "Armed: away",
14625
- "system.alarm-armed.title.night": "Armed: night",
14791
+ "system.alarm-armed.title.home": "Alarm armed: home",
14792
+ "system.alarm-armed.title.away": "Alarm armed: away",
14793
+ "system.alarm-armed.title.night": "Alarm armed: night",
14626
14794
  "system.alarm-armed.body": "Alarm armed",
14627
14795
  "system.alarm-disarmed.title": "Disarmed",
14628
14796
  "system.alarm-disarmed.body": "Alarm disarmed",
14629
14797
  "system.alarm-disarmed.body.defused": "Alarm disarmed during an alarm",
14798
+ "system.alarm-disarmed.body.autoCleared": "Alarm disarmed automatically after the siren",
14630
14799
  "system.alarm-triggered.title": "ALARM",
14631
14800
  "system.alarm-triggered.body": "Alarm triggered",
14632
14801
  "system.alarm-triggered.body.rule": "Triggered by {{rule}} on {{deviceName}}",
@@ -14948,13 +15117,14 @@ var it_default = {
14948
15117
  "system.alarm-arming.title.away": "Inserimento Fuori — {{seconds}}s",
14949
15118
  "system.alarm-arming.title.night": "Inserimento Notte — {{seconds}}s",
14950
15119
  "system.alarm-arming.body": "Inserimento in corso",
14951
- "system.alarm-armed.title.home": "Inserito: Casa",
14952
- "system.alarm-armed.title.away": "Inserito: Fuori",
14953
- "system.alarm-armed.title.night": "Inserito: Notte",
15120
+ "system.alarm-armed.title.home": "Allarme inserito: Casa",
15121
+ "system.alarm-armed.title.away": "Allarme inserito: Fuori",
15122
+ "system.alarm-armed.title.night": "Allarme inserito: Notte",
14954
15123
  "system.alarm-armed.body": "Allarme inserito",
14955
15124
  "system.alarm-disarmed.title": "Disinserito",
14956
15125
  "system.alarm-disarmed.body": "Allarme disinserito",
14957
15126
  "system.alarm-disarmed.body.defused": "Allarme disinserito durante un allarme",
15127
+ "system.alarm-disarmed.body.autoCleared": "Allarme disinserito automaticamente dopo la sirena",
14958
15128
  "system.alarm-triggered.title": "ALLARME",
14959
15129
  "system.alarm-triggered.body": "Allarme scattato",
14960
15130
  "system.alarm-triggered.body.rule": "Scattato da {{rule}} su {{deviceName}}",
@@ -23942,7 +24112,7 @@ var NcDispatcher = class {
23942
24112
  ...systemCount,
23943
24113
  ...systemEvent?.body !== void 0 ? { frozen: systemEvent.body } : {}
23944
24114
  });
23945
- return composeRuleAiBody({
24115
+ return ncSentenceCaseBody(composeRuleAiBody({
23946
24116
  aiText,
23947
24117
  body: entry.payload.synthetic === true ? this.renderText({
23948
24118
  key: "test.prefix",
@@ -23950,7 +24120,7 @@ var NcDispatcher = class {
23950
24120
  frozen: rendered
23951
24121
  }) : rendered,
23952
24122
  templateNamesAi
23953
- });
24123
+ }));
23954
24124
  };
23955
24125
  const body = renderBody("");
23956
24126
  const resolved = await this.resolveAttachments(entry);
@@ -25130,15 +25300,18 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
25130
25300
  const systemEvent = subject.systemEvent;
25131
25301
  const detection = systemEvent?.alarmSource?.className !== void 0 ? systemEvent.alarmSource : void 0;
25132
25302
  const group = readOutboxGroup(entry.payload.group);
25303
+ const locale = texts.language;
25304
+ const cameraName = deviceName.toLocaleLowerCase(locale);
25305
+ const zonesLower = zoneLabels.map((z) => z.toLocaleLowerCase(locale));
25133
25306
  return {
25134
- camera: deviceName,
25307
+ camera: cameraName,
25135
25308
  class: detection?.className ?? (systemEvent !== void 0 ? "" : subject.className),
25136
25309
  classLabel: (() => {
25137
25310
  const wire = detection?.className ?? (systemEvent !== void 0 ? "" : subject.className);
25138
- return wire.length === 0 ? "" : eventKindLabel(wire, texts.language);
25311
+ return wire.length === 0 ? "" : eventKindLabel(wire, locale).toLocaleLowerCase(locale);
25139
25312
  })(),
25140
25313
  label: detection?.label ?? subject.label ?? "",
25141
- zones: zoneLabels.join(", "),
25314
+ zones: zonesLower.join(", "),
25142
25315
  subject: detection?.className !== void 0 ? ncSubjectText(texts, {
25143
25316
  className: detection.className,
25144
25317
  ...detection.label !== void 0 ? { label: detection.label } : {}
@@ -25148,9 +25321,9 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
25148
25321
  ...subject.labelKind !== void 0 ? { labelKind: subject.labelKind } : {}
25149
25322
  }),
25150
25323
  detectionSummary: group !== void 0 && group.members.length > 1 ? ncDetectionSummary(texts, classCounts(group.members)) : "",
25151
- inZones: ncInZonesText(texts, zoneLabels),
25152
- scope: occupancy !== void 0 ? occupancy.zone ?? deviceName : "",
25153
- zone: occupancy?.zone ?? zoneLabels[0] ?? "",
25324
+ inZones: ncInZonesText(texts, zonesLower),
25325
+ scope: occupancy !== void 0 ? occupancy.zone?.toLocaleLowerCase(locale) ?? cameraName : "",
25326
+ zone: (occupancy?.zone ?? zoneLabels[0] ?? "").toLocaleLowerCase(locale),
25154
25327
  confidence: percentOf(detection?.confidence ?? subject.confidence),
25155
25328
  time: ncClockText(texts.language, subject.timestamp, timeZone),
25156
25329
  sounds: subject.audio?.labels.join(", ") ?? "",
@@ -29648,7 +29821,7 @@ function buildRuleAiSystemPrompt(shape, language, maxWords, grouped = false) {
29648
29821
  ].join("");
29649
29822
  }
29650
29823
  /** `HH:MM` in the host timezone. */
29651
- function clockOf$5(atMs) {
29824
+ function clockOf$6(atMs) {
29652
29825
  const d = new Date(atMs);
29653
29826
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
29654
29827
  }
@@ -29686,7 +29859,7 @@ function buildRuleAiPrompt(input) {
29686
29859
  if (shape === "detection-track") {
29687
29860
  lines.push(`Camera: ${camera}.${located} Subject: ${subject}. The stills, in order:`);
29688
29861
  for (const [i, frame] of input.frames.entries()) {
29689
- const when = frame.atMs !== void 0 ? ` · ${clockOf$5(frame.atMs)}` : "";
29862
+ const when = frame.atMs !== void 0 ? ` · ${clockOf$6(frame.atMs)}` : "";
29690
29863
  lines.push(`${String(i + 1)}.${when}`);
29691
29864
  }
29692
29865
  return lines.join("\n");
@@ -31457,7 +31630,7 @@ var DEFAULT_PROMPT_EN = "What happened during this period? Summarise it for a ho
31457
31630
  */
31458
31631
  var SummaryAnswerSchema = object({ summary: string() });
31459
31632
  /** `HH:MM` in the host timezone — the two numbers a frame label needs. */
31460
- function clockOf$4(atMs) {
31633
+ function clockOf$5(atMs) {
31461
31634
  const d = new Date(atMs);
31462
31635
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
31463
31636
  }
@@ -31480,7 +31653,7 @@ function buildSummaryAiPrompt(input, frames) {
31480
31653
  const roster = frames.map((f, i) => {
31481
31654
  const location = plainLabel(f.location ?? "");
31482
31655
  const where = location.length > 0 ? ` (${location})` : "";
31483
- return `${String(i + 1)}. ${plainLabel(f.deviceName)}${where} · ${clockOf$4(f.atMs)} · ${plainVocabulary(f.className) || "subject"}`;
31656
+ return `${String(i + 1)}. ${plainLabel(f.deviceName)}${where} · ${clockOf$5(f.atMs)} · ${plainVocabulary(f.className) || "subject"}`;
31484
31657
  }).join("\n");
31485
31658
  const described = /* @__PURE__ */ new Map();
31486
31659
  for (const f of frames) {
@@ -32083,7 +32256,7 @@ var NO_DETECTIONS$1 = {
32083
32256
  * has to check is a key somebody can get wrong. */
32084
32257
  var BODY_SEPARATOR$1 = " · ";
32085
32258
  /** `HH:MM` in the host timezone. */
32086
- function clockOf$3(atMs) {
32259
+ function clockOf$4(atMs) {
32087
32260
  const d = new Date(atMs);
32088
32261
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
32089
32262
  }
@@ -32097,8 +32270,8 @@ function clockOf$3(atMs) {
32097
32270
  function summaryTemplateVars(input) {
32098
32271
  return {
32099
32272
  rule: input.ruleName,
32100
- from: clockOf$3(input.window.startMs),
32101
- to: clockOf$3(input.window.endMs),
32273
+ from: clockOf$4(input.window.startMs),
32274
+ to: clockOf$4(input.window.endMs),
32102
32275
  time: ncClockText(input.texts.language, input.generatedAt),
32103
32276
  events: String(input.tileCount),
32104
32277
  cameras: String(input.cameraCount),
@@ -32193,11 +32366,11 @@ function summaryRecordId(ruleId, windowEndMs) {
32193
32366
  /** The caption burned onto the mosaic's title band, with the same `{{var}}`
32194
32367
  * vocabulary the title and body use. `''` is a real answer ("no caption"). */
32195
32368
  function summaryCaptionText(input) {
32196
- if (input.captionText === void 0) return `${input.ruleName} — ${clockOf$3(input.window.startMs)}→${clockOf$3(input.window.endMs)}`;
32369
+ if (input.captionText === void 0) return `${input.ruleName} — ${clockOf$4(input.window.startMs)}→${clockOf$4(input.window.endMs)}`;
32197
32370
  return renderTemplate(input.captionText, {
32198
32371
  rule: input.ruleName,
32199
- from: clockOf$3(input.window.startMs),
32200
- to: clockOf$3(input.window.endMs)
32372
+ from: clockOf$4(input.window.startMs),
32373
+ to: clockOf$4(input.window.endMs)
32201
32374
  }) ?? "";
32202
32375
  }
32203
32376
  /**
@@ -33129,7 +33302,7 @@ var NcSummaryProducer = class {
33129
33302
  purpose: "summary",
33130
33303
  tiles: tiles.map(({ candidate, jpeg }) => ({
33131
33304
  jpeg,
33132
- ...plan.tileLabels ? { label: `${names.get(candidate.deviceId) ?? candidate.deviceId} · ${clockOf$2(candidate.firstSeen)} · ${candidate.className}` } : {},
33305
+ ...plan.tileLabels ? { label: `${names.get(candidate.deviceId) ?? candidate.deviceId} · ${clockOf$3(candidate.firstSeen)} · ${candidate.className}` } : {},
33133
33306
  deviceId: candidate.deviceId,
33134
33307
  sourceId: candidate.trackId
33135
33308
  })),
@@ -33352,7 +33525,7 @@ var NcSummaryProducer = class {
33352
33525
  }
33353
33526
  };
33354
33527
  /** `HH:MM` in the host timezone — the two numbers a tile label needs. */
33355
- function clockOf$2(atMs) {
33528
+ function clockOf$3(atMs) {
33356
33529
  const d = new Date(atMs);
33357
33530
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
33358
33531
  }
@@ -33852,6 +34025,99 @@ var NcCalendarEventsStore = class {
33852
34025
  }
33853
34026
  };
33854
34027
  //#endregion
34028
+ //#region src/notification-center/calendar-events/calendar-events-body.ts
34029
+ /** Visual weight on the tomorrow header — Telegram/Zentik plain text. */
34030
+ var CALENDAR_EVENTS_TOMORROW_MARK = "★";
34031
+ /** Separates the tomorrow block from the rest of the horizon. */
34032
+ var CALENDAR_EVENTS_DAY_DELIMITER = "────────";
34033
+ var MAX_EVENT_LINES = 40;
34034
+ function pad2(n) {
34035
+ return String(n).padStart(2, "0");
34036
+ }
34037
+ /** Local calendar day key `YYYY-MM-DD`. */
34038
+ function localDayKey(atMs) {
34039
+ const d = new Date(atMs);
34040
+ return `${String(d.getFullYear())}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
34041
+ }
34042
+ function startOfLocalDay(atMs) {
34043
+ const d = new Date(atMs);
34044
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
34045
+ }
34046
+ function addLocalDays(atMs, days) {
34047
+ const d = new Date(atMs);
34048
+ d.setDate(d.getDate() + days);
34049
+ return d.getTime();
34050
+ }
34051
+ function dayHeadingFor(dayStartMs, todayKey, tomorrowKey) {
34052
+ const key = localDayKey(dayStartMs);
34053
+ const label = new Date(dayStartMs).toLocaleDateString(void 0, {
34054
+ weekday: "short",
34055
+ month: "short",
34056
+ day: "numeric"
34057
+ });
34058
+ if (key === todayKey) return `Today — ${label}`;
34059
+ if (key === tomorrowKey) return `${CALENDAR_EVENTS_TOMORROW_MARK} Tomorrow — ${label}`;
34060
+ return label;
34061
+ }
34062
+ /** `HH:MM` local, or `YYYY-MM-DD HH:MM` when the end falls on another day. */
34063
+ function clockOf$2(atMs) {
34064
+ const d = new Date(atMs);
34065
+ return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
34066
+ }
34067
+ function formatEventLine(event) {
34068
+ const when = event.allDay === true ? "All day" : `${clockOf$2(event.startMs)}–${clockOf$2(event.endMs)}`;
34069
+ const where = event.location !== void 0 && event.location.length > 0 ? ` @ ${event.location}` : "";
34070
+ return `• ${when} ${event.title}${where}`;
34071
+ }
34072
+ function groupByLocalDay(events) {
34073
+ const map = /* @__PURE__ */ new Map();
34074
+ for (const ev of events) {
34075
+ const dayKey = localDayKey(ev.startMs);
34076
+ const existing = map.get(dayKey);
34077
+ if (existing !== void 0) {
34078
+ existing.events.push(ev);
34079
+ continue;
34080
+ }
34081
+ map.set(dayKey, {
34082
+ dayKey,
34083
+ dayStartMs: startOfLocalDay(ev.startMs),
34084
+ events: [ev]
34085
+ });
34086
+ }
34087
+ return [...map.values()].toSorted((a, b) => a.dayStartMs - b.dayStartMs);
34088
+ }
34089
+ /**
34090
+ * Build the digest body.
34091
+ *
34092
+ * @param nowMs — fire time; tomorrow is the local calendar day after this.
34093
+ */
34094
+ function formatCalendarEventsBody(events, nowMs, horizonDays) {
34095
+ if (events.length === 0) return `No events in the next ${String(horizonDays)} day(s).`;
34096
+ const todayKey = localDayKey(nowMs);
34097
+ const tomorrowKey = localDayKey(addLocalDays(startOfLocalDay(nowMs), 1));
34098
+ const buckets = groupByLocalDay(events);
34099
+ const today = buckets.filter((b) => b.dayKey === todayKey);
34100
+ const tomorrow = buckets.filter((b) => b.dayKey === tomorrowKey);
34101
+ const later = buckets.filter((b) => b.dayKey !== todayKey && b.dayKey !== tomorrowKey);
34102
+ const lead = [...today, ...tomorrow];
34103
+ const lines = [];
34104
+ let emitted = 0;
34105
+ const pushBucket = (bucket) => {
34106
+ if (emitted >= MAX_EVENT_LINES) return;
34107
+ lines.push(dayHeadingFor(bucket.dayStartMs, todayKey, tomorrowKey));
34108
+ for (const ev of bucket.events) {
34109
+ if (emitted >= MAX_EVENT_LINES) break;
34110
+ lines.push(formatEventLine(ev));
34111
+ emitted += 1;
34112
+ }
34113
+ };
34114
+ for (const bucket of lead) pushBucket(bucket);
34115
+ if (lead.length > 0 && later.length > 0) lines.push(CALENDAR_EVENTS_DAY_DELIMITER);
34116
+ for (const bucket of later) pushBucket(bucket);
34117
+ if (events.length > MAX_EVENT_LINES) lines.push(`…and ${String(events.length - MAX_EVENT_LINES)} more`);
34118
+ return lines.join("\n");
34119
+ }
34120
+ //#endregion
33855
34121
  //#region src/notification-center/calendar-events/calendar-events-delivery.ts
33856
34122
  function calendarEventsRecordId(ruleId, windowEndMs) {
33857
34123
  return `calendar-events:${ruleId}:${windowEndMs}`;
@@ -33861,16 +34127,8 @@ function clockOf$1(atMs) {
33861
34127
  const d = new Date(atMs);
33862
34128
  return `${`${String(d.getFullYear())}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`} ${`${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`}`;
33863
34129
  }
33864
- function formatEventLine(event) {
33865
- const when = event.allDay ? clockOf$1(event.startMs).slice(0, 10) : `${clockOf$1(event.startMs)}–${clockOf$1(event.endMs)}`;
33866
- const where = event.location !== void 0 && event.location.length > 0 ? ` @ ${event.location}` : "";
33867
- return `• ${when} ${event.title}${where}`;
33868
- }
33869
34130
  function derivedBody$1(input) {
33870
- if (input.events.length === 0) return `No events in the next ${String(input.horizonDays)} day(s).`;
33871
- const lines = input.events.slice(0, 40).map(formatEventLine);
33872
- const extra = input.events.length > 40 ? `\n…and ${String(input.events.length - 40)} more` : "";
33873
- return lines.join("\n") + extra;
34131
+ return formatCalendarEventsBody(input.events, input.eventWindow.startMs, input.horizonDays);
33874
34132
  }
33875
34133
  function templateVars(input) {
33876
34134
  return {
@@ -39316,19 +39574,21 @@ var NotificationCenter = class {
39316
39574
  }
39317
39575
  /**
39318
39576
  * Start photographing every camera that links `sensorId`, and return the
39319
- * owners the sensor-level evaluation names — or `undefined` when there is no
39320
- * set (≤1 linked camera: today's marker is the picture), nothing to show
39321
- * (every linked camera muted), the event will be dropped as muted (the
39322
- * SENSOR is muted, or there were row cameras and every one is muted — see
39323
- * the evaluation's mute gate), or no enabled device-event rule that could
39324
- * fire on this sensor KIND wants a picture (the gate is the RULE's media
39325
- * policy, scope and kind conditions, read from the in-memory mirror — D49,
39326
- * as `audioStillOwner`; an occupancy rule, which rides the same delivery,
39327
- * never fires on a sensor). Coarse on purpose, in one direction only: a
39328
- * capture for a rule whose other conditions then fail is one wasted
39329
- * snapshot, a missing capture is a notification without its picture. The
39330
- * kind check is what keeps a chatty multi-linked motion sensor from
39331
- * composing a mosaic on every edge for a doorbell rule.
39577
+ * owners the sensor-level evaluation names — or `undefined` when there is
39578
+ * nothing to photograph (`cameraIds` empty), nothing to show (every linked
39579
+ * camera muted), the event will be dropped as muted (the SENSOR is muted,
39580
+ * or there were row cameras and every one is muted — see the evaluation's
39581
+ * mute gate), or no enabled device-event rule that could fire on this
39582
+ * sensor KIND wants a picture (the gate is the RULE's media policy, scope
39583
+ * and kind conditions, read from the in-memory mirror — D49, as
39584
+ * `audioStillOwner`; an occupancy rule, which rides the same delivery,
39585
+ * never fires on a sensor). One linked camera is enough: link alone owns
39586
+ * notification media; tracked rows are a separate timeline question. Coarse
39587
+ * on purpose, in one direction only: a capture for a rule whose other
39588
+ * conditions then fail is one wasted snapshot, a missing capture is a
39589
+ * notification without its picture. The kind check is what keeps a chatty
39590
+ * multi-linked motion sensor from composing a mosaic on every edge for a
39591
+ * doorbell rule.
39332
39592
  *
39333
39593
  * A MUTED camera (D62) is never dialled: its tile is left out, said on that
39334
39594
  * camera (once per window), and the primary is the first linked camera that
@@ -39341,7 +39601,7 @@ var NotificationCenter = class {
39341
39601
  beginLinkedStills(request) {
39342
39602
  const { sensorId, sensorKind, cameraIds, atMs, rowCameraIds, sensorRaised } = request;
39343
39603
  const shelf = this.linkedStills;
39344
- if (shelf === null || !this.evaluationActive || cameraIds.length < 2) return void 0;
39604
+ if (shelf === null || !this.evaluationActive || cameraIds.length === 0) return void 0;
39345
39605
  if (this.deviceMutes.isMuted(sensorId)) return void 0;
39346
39606
  const rowShown = this.unmutedCameras(rowCameraIds);
39347
39607
  if (rowCameraIds.length > 0 && rowShown.length === 0) return void 0;
@@ -41698,14 +41958,19 @@ var NotificationCenter = class {
41698
41958
  }
41699
41959
  sequenceActionsOf = (sequenceId) => this.sequences.get(sequenceId)?.actions;
41700
41960
  /**
41701
- * Run this rule's onTrigger / onTriggerOff bindings for this match.
41961
+ * Run this rule's onTrigger / onTriggerOff bindings for this match, and —
41962
+ * when the rule is gated on `armed_*` — auto-trigger the CamStack
41963
+ * alarm-panel (never sirens).
41702
41964
  *
41703
41965
  * Called BEFORE the cooldown gate — see the call site for why. Everything
41704
41966
  * here is fire-and-forget and nothing may throw into the evaluation.
41705
41967
  */
41706
41968
  runRuleActions(rule, subject, kind, controls) {
41707
- if (this.actionRunner === null || controls.runActions === false) return;
41708
- const bindings = this.isTriggerOffSubject(subject) ? rule.actions?.onTriggerOff : rule.actions?.onTrigger;
41969
+ if (controls.runActions === false) return;
41970
+ const triggerOff = this.isTriggerOffSubject(subject);
41971
+ if (!triggerOff) this.autoTriggerAlarmPanel(rule, subject, kind);
41972
+ if (this.actionRunner === null) return;
41973
+ const bindings = triggerOff ? rule.actions?.onTriggerOff : rule.actions?.onTrigger;
41709
41974
  if (bindings === void 0 || bindings.length === 0) return;
41710
41975
  this.actionRunner.runBindings({
41711
41976
  ruleId: rule.id,
@@ -41717,6 +41982,41 @@ var NotificationCenter = class {
41717
41982
  });
41718
41983
  }
41719
41984
  /**
41985
+ * A rule gated on `armed_*` fires the CamStack panel on match — no
41986
+ * `onTrigger` sequence required. Sirens are never actuated here.
41987
+ */
41988
+ autoTriggerAlarmPanel(rule, subject, kind) {
41989
+ if (!ruleAutoTriggersAlarm(rule)) return;
41990
+ const panel = this.alarmPanel;
41991
+ if (panel === null) {
41992
+ this.logger.warn("alarm rule matched but no alarm panel is wired — panel not triggered", {
41993
+ tags: { deviceId: subject.deviceId },
41994
+ meta: {
41995
+ ruleId: rule.id,
41996
+ rule: rule.name
41997
+ }
41998
+ });
41999
+ return;
42000
+ }
42001
+ this.actuateWithAttribution({
42002
+ deviceId: panel.deviceId,
42003
+ cap: ALARM_PANEL_CAP,
42004
+ method: "trigger",
42005
+ args: {},
42006
+ origin: fireRefFor(rule, subject, kind)
42007
+ }).catch((err) => {
42008
+ this.logger.warn("alarm panel auto-trigger failed", {
42009
+ tags: { deviceId: subject.deviceId },
42010
+ meta: {
42011
+ ruleId: rule.id,
42012
+ rule: rule.name,
42013
+ panelId: panel.deviceId,
42014
+ error: err instanceof Error ? err.message : String(err)
42015
+ }
42016
+ });
42017
+ });
42018
+ }
42019
+ /**
41720
42020
  * The muted device that drops this subject, or `undefined` when none does.
41721
42021
  * A per-camera subject: its camera. A linked set: the sensor; else, when
41722
42022
  * the change took rows and every row camera is muted, the sensor too — the
@@ -104042,8 +104342,22 @@ var NO_PUBLICATION = {
104042
104342
  url: null,
104043
104343
  artifactId: null
104044
104344
  };
104045
- function isCapRecord(value) {
104046
- return typeof value === "object" && value !== null;
104345
+ /**
104346
+ * A tRPC-shaped router or procedure reached by DYNAMIC name off `ctx.api`.
104347
+ *
104348
+ * A rule names its cap and method as strings, so the generated client cannot be
104349
+ * indexed statically. The walk down `api → router → procedure → mutate|query`
104350
+ * is therefore guarded hop by hop rather than asserted: every hop that is not
104351
+ * what it must be raises the named error the rule author needs to see.
104352
+ *
104353
+ * Forked-addon `ctx.api` namespaces are tRPC v11 Proxies over a noop function
104354
+ * (`typeof === 'function'`). Treating only plain objects as routers made every
104355
+ * sequence step fail with `no router for capability "alarm-panel"` while the
104356
+ * same method answered fine over HTTP — the panel never left `armed_*`, so
104357
+ * HomeKit stayed quiet.
104358
+ */
104359
+ function isCapNode(value) {
104360
+ return typeof value === "object" && value !== null || typeof value === "function";
104047
104361
  }
104048
104362
  function isCapCall(value) {
104049
104363
  return typeof value === "function";
@@ -107162,10 +107476,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
107162
107476
  },
107163
107477
  actuate: async ({ deviceId, cap, method, args }) => {
107164
107478
  const router = Reflect.get(api, kebabToCamel(cap));
107165
- if (!isCapRecord(router)) throw new Error(`rule action: no router for capability "${cap}"`);
107166
- const proc = router[method];
107167
- const fn = isCapRecord(proc) ? proc["mutate"] ?? proc["query"] : void 0;
107168
- if (!isCapCall(fn)) throw new Error(`rule action: "${cap}.${method}" is not a callable cap method`);
107479
+ if (!isCapNode(router)) throw new Error(`rule action: no router for capability "${cap}"`);
107480
+ const proc = Reflect.get(router, method);
107481
+ const mutate = isCapNode(proc) ? Reflect.get(proc, "mutate") : void 0;
107482
+ const query = isCapNode(proc) ? Reflect.get(proc, "query") : void 0;
107483
+ const fn = isCapCall(mutate) ? mutate : isCapCall(query) ? query : void 0;
107484
+ if (fn === void 0) throw new Error(`rule action: "${cap}.${method}" is not a callable cap method`);
107169
107485
  await fn({
107170
107486
  deviceId,
107171
107487
  ...args
@@ -113418,7 +113734,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
113418
113734
  });
113419
113735
  return NO_SENSOR_NOTIFICATION_CAMERAS;
113420
113736
  });
113421
- const linkedSet = cameras.mediaCameraIds.length > 1;
113737
+ const linkedSet = cameras.mediaCameraIds.length > 1 || cameras.mediaCameraIds.length === 1 && cameras.rowCameraIds.length === 0;
113422
113738
  const linkedStills = linkedSet ? this.startLinkedStills(data, timestamp, cameras) : void 0;
113423
113739
  const persisted = [];
113424
113740
  const unowned = [];