@camstack/addon-post-analysis 1.2.325 → 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.
Files changed (15) hide show
  1. package/dist/{clip-model-registry-CshjlVTF.mjs → clip-model-registry-CGaaUvAD.mjs} +198 -20
  2. package/dist/{clip-model-registry-BFLeI0dr.js → clip-model-registry-DQmmIYXS.js} +215 -19
  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-BiD7k7AM.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BNJdXVq8.mjs} +3 -3
  7. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BdFzCOK8.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-n9xCtjjZ.mjs +26 -0
  9. package/dist/pipeline-analytics/{hostInit-D0L1yv82.mjs → hostInit-Bj7szOak.mjs} +3 -3
  10. package/dist/pipeline-analytics/index.js +460 -141
  11. package/dist/pipeline-analytics/index.mjs +460 -141
  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-CBG_lEGV.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-Bnb-yu_h.mjs +0 -26
@@ -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-CshjlVTF.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,14 +2247,48 @@ 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 } });
2254
+ if (this.config.get("announceTargets").length > 0) {
2255
+ this.ctx.logger.info("alarm announceTargets cleared — deliver via alarm rules only", {
2256
+ tags: { deviceId: this.id },
2257
+ meta: { cleared: this.config.get("announceTargets").length }
2258
+ });
2259
+ this.config.set("announceTargets", []);
2260
+ }
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
+ });
2192
2283
  }
2193
2284
  /** The operator-visible configuration, as the cap serves it. */
2194
2285
  settings() {
2286
+ const storedAction = this.config.get("postTriggeredAction");
2195
2287
  return {
2196
- ...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 } : {},
2197
2292
  announceArm: this.config.get("announceArm"),
2198
2293
  announceTargets: [...this.config.get("announceTargets")],
2199
2294
  nonBlocking: modeDeviceListFromPersisted(this.config.get("nonBlocking")),
@@ -2342,6 +2437,7 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2342
2437
  if (patch.exitDelaySec !== void 0) await this.config.set("exitDelaySec", patch.exitDelaySec);
2343
2438
  if (patch.entryDelaySec !== void 0) await this.config.set("entryDelaySec", patch.entryDelaySec);
2344
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);
2345
2441
  if (patch.announceArm !== void 0) await this.config.set("announceArm", patch.announceArm);
2346
2442
  if (patch.announceTargets !== void 0) await this.config.set("announceTargets", [...patch.announceTargets]);
2347
2443
  if (membership !== void 0 && membershipChanged !== null) this.ctx.logger.info("alarm non-blocking list changed", {
@@ -2517,6 +2613,29 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2517
2613
  }
2518
2614
  });
2519
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
+ }
2520
2639
  }
2521
2640
  }
2522
2641
  /** One INFO line per persisted per-mode delay entry the guard refused to trust. */
@@ -2575,6 +2694,13 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2575
2694
  */
2576
2695
  async armWith(mode) {
2577
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
+ }
2578
2704
  const read = await this.applyArmGuard(mode);
2579
2705
  if (read.kind === "unguarded") return this.settleArm(mode, null, now);
2580
2706
  if (read.kind === "check-failed") {
@@ -2588,6 +2714,11 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2588
2714
  why: this.exclusionWhyFor(mode, device)
2589
2715
  })), now);
2590
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
+ }
2591
2722
  /**
2592
2723
  * The Arm anyway on ONE refusal's notification — the only path that arms
2593
2724
  * around a device that blocks.
@@ -2613,6 +2744,17 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2613
2744
  const reason = refusalId === this.spentRefusalId ? "used" : "unknown-refusal";
2614
2745
  return this.rejectArmAnyway(refusalId, reason);
2615
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
+ }
2616
2758
  if (this.machine.changedAt !== held.panelChangedAt) {
2617
2759
  this.refusal = null;
2618
2760
  return this.rejectArmAnyway(refusalId, "stale");
@@ -3008,11 +3150,12 @@ var NcAlarmPanelDevice = class extends BaseDevice {
3008
3150
  * rather than fired only once somebody looks.
3009
3151
  */
3010
3152
  publish(now = Date.now()) {
3153
+ const autoCleared = this.machine.clearAt !== void 0 && now >= this.machine.clearAt && this.machine.clearToDisarmed === true;
3011
3154
  this.machine = settle(this.machine, now);
3012
3155
  const state = stateAt(this.machine, now);
3013
3156
  this.runtimeState.setCapState("alarm-panel", this.status(now));
3014
3157
  this.persist();
3015
- this.report(state, now);
3158
+ this.report(state, now, autoCleared);
3016
3159
  if (this.machine.armedAt !== void 0 || this.machine.triggerAt !== void 0 || this.machine.clearAt !== void 0) this.startTicking();
3017
3160
  else this.stopTicking();
3018
3161
  }
@@ -3024,7 +3167,7 @@ var NcAlarmPanelDevice = class extends BaseDevice {
3024
3167
  * panel's own state update with it would make a delivery failure look like
3025
3168
  * an alarm failure.
3026
3169
  */
3027
- report(state, now) {
3170
+ report(state, now, autoCleared = false) {
3028
3171
  const previous = this.lastPublished;
3029
3172
  this.lastPublished = state;
3030
3173
  const hook = this.onPublish;
@@ -3039,7 +3182,8 @@ var NcAlarmPanelDevice = class extends BaseDevice {
3039
3182
  at: now,
3040
3183
  ...source !== null ? { source } : {},
3041
3184
  ...arming !== void 0 ? { arming } : {},
3042
- ...pending !== void 0 ? { pending } : {}
3185
+ ...pending !== void 0 ? { pending } : {},
3186
+ ...autoCleared ? { autoCleared: true } : {}
3043
3187
  });
3044
3188
  if (transition?.kind === "alarm-triggered") this.attribution.clear();
3045
3189
  try {
@@ -3125,8 +3269,12 @@ var NcAlarmPanelDevice = class extends BaseDevice {
3125
3269
  }
3126
3270
  /** The global durations plus the per-mode overrides, read from the persisted config NOW. */
3127
3271
  delaySettings() {
3272
+ const storedAction = this.config.get("postTriggeredAction");
3128
3273
  return {
3129
- ...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 } : {},
3130
3278
  modeDelays: modeDelaysFromPersisted(this.config.get("modeDelays")).entries
3131
3279
  };
3132
3280
  }
@@ -3136,24 +3284,32 @@ var NcAlarmPanelDevice = class extends BaseDevice {
3136
3284
  */
3137
3285
  delaysFor(mode) {
3138
3286
  const settings = this.delaySettings();
3139
- if (mode === null) return {
3140
- exitDelaySec: settings.exitDelaySec,
3141
- entryDelaySec: settings.entryDelaySec,
3142
- triggeredDurationSec: settings.triggeredDurationSec,
3143
- source: {
3144
- exitDelaySec: "default",
3145
- entryDelaySec: "default",
3146
- triggeredDurationSec: "default"
3147
- }
3148
- };
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
+ }
3149
3302
  return resolveModeDelays(settings, mode);
3150
3303
  }
3151
- /** The three global durations, read from the persisted config. */
3304
+ /** The global durations + post-triggered action, read from the persisted config. */
3152
3305
  readDelays() {
3306
+ const triggeredDurationSec = this.config.get("triggeredDurationSec");
3307
+ const stored = this.config.get("postTriggeredAction");
3153
3308
  return {
3154
3309
  exitDelaySec: this.config.get("exitDelaySec"),
3155
3310
  entryDelaySec: this.config.get("entryDelaySec"),
3156
- triggeredDurationSec: this.config.get("triggeredDurationSec")
3311
+ triggeredDurationSec,
3312
+ postTriggeredAction: derivePostTriggeredAction(stored, triggeredDurationSec)
3157
3313
  };
3158
3314
  }
3159
3315
  startTicking() {
@@ -5133,7 +5289,7 @@ function ncClassNoun(texts, className, count) {
5133
5289
  count,
5134
5290
  vars: { name: className }
5135
5291
  });
5136
- if (isEventKindLabelKey(normalised)) return eventKindLabel(normalised, texts.language);
5292
+ if (isEventKindLabelKey(normalised)) return eventKindLabel(normalised, texts.language).toLocaleLowerCase(texts.language);
5137
5293
  return texts.text({
5138
5294
  key: "class.other",
5139
5295
  count,
@@ -5141,13 +5297,24 @@ function ncClassNoun(texts, className, count) {
5141
5297
  });
5142
5298
  }
5143
5299
  /**
5144
- * The parenthetical half of a labelled subject: a recognised person or plate
5145
- * is printed verbatim; a classification (species, COCO sub, …) goes through
5146
- * 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.
5147
5303
  */
5148
- function ncSubjectLabelText(texts, label, labelKind) {
5149
- if (labelKind === "identity" || labelKind === "plate") return label;
5150
- 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());
5151
5318
  }
5152
5319
  /** `person (John)` / `persona (John)` — the thing the notification is about. */
5153
5320
  function ncSubjectText(texts, input) {
@@ -5156,11 +5323,15 @@ function ncSubjectText(texts, input) {
5156
5323
  key: "detection.subject",
5157
5324
  vars: { classNoun }
5158
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
+ });
5159
5330
  return texts.text({
5160
5331
  key: subjectLabelKey(input.labelKind),
5161
5332
  vars: {
5162
5333
  classNoun,
5163
- label: ncSubjectLabelText(texts, input.label, input.labelKind)
5334
+ label: input.label
5164
5335
  }
5165
5336
  });
5166
5337
  }
@@ -5750,7 +5921,7 @@ function isAdoptable(record, nowMs) {
5750
5921
  * what `localDayMinute` does for the same input — one behaviour for a bad
5751
5922
  * zone, not two.
5752
5923
  */
5753
- function clockOf$6(atMs, timeZone) {
5924
+ function clockOf$7(atMs, timeZone) {
5754
5925
  if (timeZone !== void 0) try {
5755
5926
  return new Intl.DateTimeFormat("en-GB", {
5756
5927
  timeZone,
@@ -5886,7 +6057,7 @@ var TimelapseRenderer = class {
5886
6057
  includeAudio: false,
5887
6058
  maxLifeMs: await this.ports.exportLifeMs(),
5888
6059
  deleteAfterDownload: false,
5889
- 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)}`,
5890
6061
  timelapse: {
5891
6062
  everyMs,
5892
6063
  outputFps: request.framerate,
@@ -6217,7 +6388,7 @@ var TimelapseRenderer = class {
6217
6388
  ...meta,
6218
6389
  unservedWindowCount: verdict.unservedWindows.length,
6219
6390
  unservedWindows: verdict.unservedWindows.slice(0, 12).map((w) => ({
6220
- at: clockOf$6(request.window.startMs + w.fromSec * 1e3, request.timezone),
6391
+ at: clockOf$7(request.window.startMs + w.fromSec * 1e3, request.timezone),
6221
6392
  fromSec: Math.round(w.fromSec),
6222
6393
  toSec: Math.round(w.toSec),
6223
6394
  expectedDenseFrames: w.expectedDenseFrames,
@@ -8798,6 +8969,7 @@ function buildActionButtons(input) {
8798
8969
  sequenceId: button.sequenceId,
8799
8970
  actionId: button.id
8800
8971
  }),
8972
+ mode: "background",
8801
8973
  ...button.icon !== void 0 ? { icon: button.icon } : {},
8802
8974
  ...button.destructive !== void 0 ? { destructive: button.destructive } : {}
8803
8975
  });
@@ -8913,10 +9085,9 @@ var MAX_ARM_BUTTONS = 3;
8913
9085
  * (`ALARM_DISARM_TTL_MS`). Both exist: the hint is for the person, the TTL is
8914
9086
  * for the forwarded screenshot.
8915
9087
  *
8916
- * Arm buttons are `background` — arming from the lock screen should not open a
8917
- * browser. The disarm is `navigate`: it removes a protection, and the operator
8918
- * doing it deserves to SEE that it happened rather than trust a POST nobody
8919
- * 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.
8920
9091
  *
8921
9092
  * `panel.availableModes` ARE the enabled modes: CamStack has no per-mode
8922
9093
  * disable setting, so the Arm buttons offer exactly what the panel declares.
@@ -8950,14 +9121,14 @@ function buildAlarmButtons(input) {
8950
9121
  }),
8951
9122
  icon: "disarm",
8952
9123
  destructive: true,
8953
- mode: "navigate"
9124
+ mode: "background"
8954
9125
  }];
8955
9126
  }
8956
9127
  /**
8957
9128
  * The one Arm anyway a refusal notification carries — or none, when the row
8958
- * names no refusal. `navigate` and destructive, like the disarm: it arms
8959
- * AROUND an open door, and the person doing it should see it happen. Its TTL
8960
- * 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).
8961
9132
  */
8962
9133
  function armAnywayButton(refusalId, input) {
8963
9134
  if (refusalId === void 0) return [];
@@ -8972,7 +9143,7 @@ function armAnywayButton(refusalId, input) {
8972
9143
  }),
8973
9144
  icon: "arm",
8974
9145
  destructive: true,
8975
- mode: "navigate"
9146
+ mode: "background"
8976
9147
  }];
8977
9148
  }
8978
9149
  //#endregion
@@ -12894,6 +13065,10 @@ function alarmTextKeys(transition, body) {
12894
13065
  titleKey,
12895
13066
  bodyKey: `system.${kind}.body.${transition.sensor.event}`
12896
13067
  };
13068
+ if (kind === "alarm-disarmed" && transition.autoCleared === true) return {
13069
+ titleKey,
13070
+ bodyKey: `system.${kind}.body.autoCleared`
13071
+ };
12897
13072
  if (kind === "alarm-disarmed" && (transition.previous === "triggered" || transition.previous === "pending")) return {
12898
13073
  titleKey,
12899
13074
  bodyKey: `system.${kind}.body.defused`
@@ -14613,13 +14788,14 @@ var en_default = {
14613
14788
  "system.alarm-arming.title.away": "Arming away — {{seconds}}s",
14614
14789
  "system.alarm-arming.title.night": "Arming night — {{seconds}}s",
14615
14790
  "system.alarm-arming.body": "Arming",
14616
- "system.alarm-armed.title.home": "Armed: home",
14617
- "system.alarm-armed.title.away": "Armed: away",
14618
- "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",
14619
14794
  "system.alarm-armed.body": "Alarm armed",
14620
14795
  "system.alarm-disarmed.title": "Disarmed",
14621
14796
  "system.alarm-disarmed.body": "Alarm disarmed",
14622
14797
  "system.alarm-disarmed.body.defused": "Alarm disarmed during an alarm",
14798
+ "system.alarm-disarmed.body.autoCleared": "Alarm disarmed automatically after the siren",
14623
14799
  "system.alarm-triggered.title": "ALARM",
14624
14800
  "system.alarm-triggered.body": "Alarm triggered",
14625
14801
  "system.alarm-triggered.body.rule": "Triggered by {{rule}} on {{deviceName}}",
@@ -14941,13 +15117,14 @@ var it_default = {
14941
15117
  "system.alarm-arming.title.away": "Inserimento Fuori — {{seconds}}s",
14942
15118
  "system.alarm-arming.title.night": "Inserimento Notte — {{seconds}}s",
14943
15119
  "system.alarm-arming.body": "Inserimento in corso",
14944
- "system.alarm-armed.title.home": "Inserito: Casa",
14945
- "system.alarm-armed.title.away": "Inserito: Fuori",
14946
- "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",
14947
15123
  "system.alarm-armed.body": "Allarme inserito",
14948
15124
  "system.alarm-disarmed.title": "Disinserito",
14949
15125
  "system.alarm-disarmed.body": "Allarme disinserito",
14950
15126
  "system.alarm-disarmed.body.defused": "Allarme disinserito durante un allarme",
15127
+ "system.alarm-disarmed.body.autoCleared": "Allarme disinserito automaticamente dopo la sirena",
14951
15128
  "system.alarm-triggered.title": "ALLARME",
14952
15129
  "system.alarm-triggered.body": "Allarme scattato",
14953
15130
  "system.alarm-triggered.body.rule": "Scattato da {{rule}} su {{deviceName}}",
@@ -23935,7 +24112,7 @@ var NcDispatcher = class {
23935
24112
  ...systemCount,
23936
24113
  ...systemEvent?.body !== void 0 ? { frozen: systemEvent.body } : {}
23937
24114
  });
23938
- return composeRuleAiBody({
24115
+ return ncSentenceCaseBody(composeRuleAiBody({
23939
24116
  aiText,
23940
24117
  body: entry.payload.synthetic === true ? this.renderText({
23941
24118
  key: "test.prefix",
@@ -23943,7 +24120,7 @@ var NcDispatcher = class {
23943
24120
  frozen: rendered
23944
24121
  }) : rendered,
23945
24122
  templateNamesAi
23946
- });
24123
+ }));
23947
24124
  };
23948
24125
  const body = renderBody("");
23949
24126
  const resolved = await this.resolveAttachments(entry);
@@ -25123,15 +25300,18 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
25123
25300
  const systemEvent = subject.systemEvent;
25124
25301
  const detection = systemEvent?.alarmSource?.className !== void 0 ? systemEvent.alarmSource : void 0;
25125
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));
25126
25306
  return {
25127
- camera: deviceName,
25307
+ camera: cameraName,
25128
25308
  class: detection?.className ?? (systemEvent !== void 0 ? "" : subject.className),
25129
25309
  classLabel: (() => {
25130
25310
  const wire = detection?.className ?? (systemEvent !== void 0 ? "" : subject.className);
25131
- return wire.length === 0 ? "" : eventKindLabel(wire, texts.language);
25311
+ return wire.length === 0 ? "" : eventKindLabel(wire, locale).toLocaleLowerCase(locale);
25132
25312
  })(),
25133
25313
  label: detection?.label ?? subject.label ?? "",
25134
- zones: zoneLabels.join(", "),
25314
+ zones: zonesLower.join(", "),
25135
25315
  subject: detection?.className !== void 0 ? ncSubjectText(texts, {
25136
25316
  className: detection.className,
25137
25317
  ...detection.label !== void 0 ? { label: detection.label } : {}
@@ -25141,9 +25321,9 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
25141
25321
  ...subject.labelKind !== void 0 ? { labelKind: subject.labelKind } : {}
25142
25322
  }),
25143
25323
  detectionSummary: group !== void 0 && group.members.length > 1 ? ncDetectionSummary(texts, classCounts(group.members)) : "",
25144
- inZones: ncInZonesText(texts, zoneLabels),
25145
- scope: occupancy !== void 0 ? occupancy.zone ?? deviceName : "",
25146
- 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),
25147
25327
  confidence: percentOf(detection?.confidence ?? subject.confidence),
25148
25328
  time: ncClockText(texts.language, subject.timestamp, timeZone),
25149
25329
  sounds: subject.audio?.labels.join(", ") ?? "",
@@ -29641,7 +29821,7 @@ function buildRuleAiSystemPrompt(shape, language, maxWords, grouped = false) {
29641
29821
  ].join("");
29642
29822
  }
29643
29823
  /** `HH:MM` in the host timezone. */
29644
- function clockOf$5(atMs) {
29824
+ function clockOf$6(atMs) {
29645
29825
  const d = new Date(atMs);
29646
29826
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
29647
29827
  }
@@ -29679,7 +29859,7 @@ function buildRuleAiPrompt(input) {
29679
29859
  if (shape === "detection-track") {
29680
29860
  lines.push(`Camera: ${camera}.${located} Subject: ${subject}. The stills, in order:`);
29681
29861
  for (const [i, frame] of input.frames.entries()) {
29682
- const when = frame.atMs !== void 0 ? ` · ${clockOf$5(frame.atMs)}` : "";
29862
+ const when = frame.atMs !== void 0 ? ` · ${clockOf$6(frame.atMs)}` : "";
29683
29863
  lines.push(`${String(i + 1)}.${when}`);
29684
29864
  }
29685
29865
  return lines.join("\n");
@@ -31450,7 +31630,7 @@ var DEFAULT_PROMPT_EN = "What happened during this period? Summarise it for a ho
31450
31630
  */
31451
31631
  var SummaryAnswerSchema = object({ summary: string() });
31452
31632
  /** `HH:MM` in the host timezone — the two numbers a frame label needs. */
31453
- function clockOf$4(atMs) {
31633
+ function clockOf$5(atMs) {
31454
31634
  const d = new Date(atMs);
31455
31635
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
31456
31636
  }
@@ -31473,7 +31653,7 @@ function buildSummaryAiPrompt(input, frames) {
31473
31653
  const roster = frames.map((f, i) => {
31474
31654
  const location = plainLabel(f.location ?? "");
31475
31655
  const where = location.length > 0 ? ` (${location})` : "";
31476
- 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"}`;
31477
31657
  }).join("\n");
31478
31658
  const described = /* @__PURE__ */ new Map();
31479
31659
  for (const f of frames) {
@@ -32076,7 +32256,7 @@ var NO_DETECTIONS$1 = {
32076
32256
  * has to check is a key somebody can get wrong. */
32077
32257
  var BODY_SEPARATOR$1 = " · ";
32078
32258
  /** `HH:MM` in the host timezone. */
32079
- function clockOf$3(atMs) {
32259
+ function clockOf$4(atMs) {
32080
32260
  const d = new Date(atMs);
32081
32261
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
32082
32262
  }
@@ -32090,8 +32270,8 @@ function clockOf$3(atMs) {
32090
32270
  function summaryTemplateVars(input) {
32091
32271
  return {
32092
32272
  rule: input.ruleName,
32093
- from: clockOf$3(input.window.startMs),
32094
- to: clockOf$3(input.window.endMs),
32273
+ from: clockOf$4(input.window.startMs),
32274
+ to: clockOf$4(input.window.endMs),
32095
32275
  time: ncClockText(input.texts.language, input.generatedAt),
32096
32276
  events: String(input.tileCount),
32097
32277
  cameras: String(input.cameraCount),
@@ -32186,11 +32366,11 @@ function summaryRecordId(ruleId, windowEndMs) {
32186
32366
  /** The caption burned onto the mosaic's title band, with the same `{{var}}`
32187
32367
  * vocabulary the title and body use. `''` is a real answer ("no caption"). */
32188
32368
  function summaryCaptionText(input) {
32189
- 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)}`;
32190
32370
  return renderTemplate(input.captionText, {
32191
32371
  rule: input.ruleName,
32192
- from: clockOf$3(input.window.startMs),
32193
- to: clockOf$3(input.window.endMs)
32372
+ from: clockOf$4(input.window.startMs),
32373
+ to: clockOf$4(input.window.endMs)
32194
32374
  }) ?? "";
32195
32375
  }
32196
32376
  /**
@@ -33122,7 +33302,7 @@ var NcSummaryProducer = class {
33122
33302
  purpose: "summary",
33123
33303
  tiles: tiles.map(({ candidate, jpeg }) => ({
33124
33304
  jpeg,
33125
- ...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}` } : {},
33126
33306
  deviceId: candidate.deviceId,
33127
33307
  sourceId: candidate.trackId
33128
33308
  })),
@@ -33345,7 +33525,7 @@ var NcSummaryProducer = class {
33345
33525
  }
33346
33526
  };
33347
33527
  /** `HH:MM` in the host timezone — the two numbers a tile label needs. */
33348
- function clockOf$2(atMs) {
33528
+ function clockOf$3(atMs) {
33349
33529
  const d = new Date(atMs);
33350
33530
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
33351
33531
  }
@@ -33845,6 +34025,99 @@ var NcCalendarEventsStore = class {
33845
34025
  }
33846
34026
  };
33847
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
33848
34121
  //#region src/notification-center/calendar-events/calendar-events-delivery.ts
33849
34122
  function calendarEventsRecordId(ruleId, windowEndMs) {
33850
34123
  return `calendar-events:${ruleId}:${windowEndMs}`;
@@ -33854,16 +34127,8 @@ function clockOf$1(atMs) {
33854
34127
  const d = new Date(atMs);
33855
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")}`}`;
33856
34129
  }
33857
- function formatEventLine(event) {
33858
- const when = event.allDay ? clockOf$1(event.startMs).slice(0, 10) : `${clockOf$1(event.startMs)}–${clockOf$1(event.endMs)}`;
33859
- const where = event.location !== void 0 && event.location.length > 0 ? ` @ ${event.location}` : "";
33860
- return `• ${when} ${event.title}${where}`;
33861
- }
33862
34130
  function derivedBody$1(input) {
33863
- if (input.events.length === 0) return `No events in the next ${String(input.horizonDays)} day(s).`;
33864
- const lines = input.events.slice(0, 40).map(formatEventLine);
33865
- const extra = input.events.length > 40 ? `\n…and ${String(input.events.length - 40)} more` : "";
33866
- return lines.join("\n") + extra;
34131
+ return formatCalendarEventsBody(input.events, input.eventWindow.startMs, input.horizonDays);
33867
34132
  }
33868
34133
  function templateVars(input) {
33869
34134
  return {
@@ -39309,19 +39574,21 @@ var NotificationCenter = class {
39309
39574
  }
39310
39575
  /**
39311
39576
  * Start photographing every camera that links `sensorId`, and return the
39312
- * owners the sensor-level evaluation names — or `undefined` when there is no
39313
- * set (≤1 linked camera: today's marker is the picture), nothing to show
39314
- * (every linked camera muted), the event will be dropped as muted (the
39315
- * SENSOR is muted, or there were row cameras and every one is muted — see
39316
- * the evaluation's mute gate), or no enabled device-event rule that could
39317
- * fire on this sensor KIND wants a picture (the gate is the RULE's media
39318
- * policy, scope and kind conditions, read from the in-memory mirror — D49,
39319
- * as `audioStillOwner`; an occupancy rule, which rides the same delivery,
39320
- * never fires on a sensor). Coarse on purpose, in one direction only: a
39321
- * capture for a rule whose other conditions then fail is one wasted
39322
- * snapshot, a missing capture is a notification without its picture. The
39323
- * kind check is what keeps a chatty multi-linked motion sensor from
39324
- * 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.
39325
39592
  *
39326
39593
  * A MUTED camera (D62) is never dialled: its tile is left out, said on that
39327
39594
  * camera (once per window), and the primary is the first linked camera that
@@ -39334,7 +39601,7 @@ var NotificationCenter = class {
39334
39601
  beginLinkedStills(request) {
39335
39602
  const { sensorId, sensorKind, cameraIds, atMs, rowCameraIds, sensorRaised } = request;
39336
39603
  const shelf = this.linkedStills;
39337
- if (shelf === null || !this.evaluationActive || cameraIds.length < 2) return void 0;
39604
+ if (shelf === null || !this.evaluationActive || cameraIds.length === 0) return void 0;
39338
39605
  if (this.deviceMutes.isMuted(sensorId)) return void 0;
39339
39606
  const rowShown = this.unmutedCameras(rowCameraIds);
39340
39607
  if (rowCameraIds.length > 0 && rowShown.length === 0) return void 0;
@@ -40633,10 +40900,8 @@ var NotificationCenter = class {
40633
40900
  * On the SAME serialized chain as every other evaluation, because both of
40634
40901
  * those follow-ups read what the evaluation just enqueued:
40635
40902
  *
40636
- * - EVERY alarm transition is also announced to the panel's own
40637
- * `announceTargets`, which no rule addresses (D630) — but only where a
40638
- * rule has not already delivered it, or a target that is both would get
40639
- * the same sentence twice;
40903
+ * - Panel `announceTargets` are NOT addressed (retired 2026-10-03): only
40904
+ * alarm-family rules deliver. The announce enqueue is a no-op.
40640
40905
  * - a TRIGGERED with an attribution supersedes the triggering rule's own
40641
40906
  * still-pending rows, for exactly the targets the alarm reached. That is
40642
40907
  * the combined notification: one row carrying the alarm's words and that
@@ -40669,11 +40934,11 @@ var NotificationCenter = class {
40669
40934
  });
40670
40935
  }
40671
40936
  /**
40672
- * The sentence an alarm transition is said with, and the targets it goes to
40673
- * — for EVERY alarm kind, always the panel's `announceTargets` (D630: the
40674
- * `announceArm` switch is gone; rules may add more recipients, never fewer).
40675
- *
40676
- * `text` is a composed body only where the words depend on the rule set:
40937
+ * The composed body for an alarm transition (when the words depend on the
40938
+ * rule set). Delivery targets are ALWAYS empty here: the panel no longer
40939
+ * fans out to `announceTargets` — an alarm-family rule is the only notifier
40940
+ * (operator ruling 2026-10-03). `announceTargets` remains on the settings
40941
+ * blob for schema compatibility and is ignored at send time.
40677
40942
  *
40678
40943
  * - ARM and ARMING compose from the SAME coverage — "what does this mode
40679
40944
  * actually watch" — because they are the same answer given thirty seconds
@@ -40684,14 +40949,9 @@ var NotificationCenter = class {
40684
40949
  * Every other kind is `null`: the intake picks its catalog body and params.
40685
40950
  */
40686
40951
  alarmAnnouncement(panel, transition) {
40687
- const targetIds = panel.settings().announceTargets;
40688
- if (targetIds.length === 0) this.logger.info("alarm transition has no announcement target", {
40689
- tags: { deviceId: panel.deviceId },
40690
- meta: { kind: transition.kind }
40691
- });
40692
40952
  return {
40693
40953
  text: this.alarmAnnouncementText(panel, transition),
40694
- targetIds
40954
+ targetIds: []
40695
40955
  };
40696
40956
  }
40697
40957
  /** The composed body of {@link alarmAnnouncement}, or null for a catalog body. */
@@ -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 = [];
@@ -113598,14 +113914,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
113598
113914
  * which verdict swallowed it.
113599
113915
  */
113600
113916
  flushSensorTransitionDrops() {
113601
- for (const drop of this.sensorTransitions.takeDropSummary(Date.now())) this.ctx.logger.info("sensor state changes dropped — no transition", {
113602
- tags: { deviceId: drop.deviceId },
113603
- meta: {
113604
- capName: drop.capName,
113605
- reason: drop.reason,
113606
- count: drop.count
113607
- }
113608
- });
113917
+ for (const drop of this.sensorTransitions.takeDropSummary(Date.now())) {
113918
+ const level = drop.reason === "baseline-seeded-inactive" || drop.reason === "no-change" ? "debug" : "info";
113919
+ this.ctx.logger[level]("sensor state changes dropped — no transition", {
113920
+ tags: { deviceId: drop.deviceId },
113921
+ meta: {
113922
+ capName: drop.capName,
113923
+ reason: drop.reason,
113924
+ count: drop.count
113925
+ }
113926
+ });
113927
+ }
113609
113928
  }
113610
113929
  /**
113611
113930
  * One `info` line per (device, cap) per window for the evaluations refused