@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
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_clip_model_registry = require("../clip-model-registry-BFLeI0dr.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-DQmmIYXS.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_clip_model_registry.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -659,7 +659,8 @@ var NcAlarmArmOutcomeSchema = require_clip_model_registry.discriminatedUnion("ou
659
659
  require_clip_model_registry.object({
660
660
  outcome: require_clip_model_registry.literal("rejected"),
661
661
  reason: NcAlarmArmAnywayRejectionSchema
662
- }).readonly()
662
+ }).readonly(),
663
+ require_clip_model_registry.object({ outcome: require_clip_model_registry.literal("outside-schedule") }).readonly()
663
664
  ]);
664
665
  /**
665
666
  * The devices a mode covers, ascending and de-duplicated.
@@ -1518,7 +1519,25 @@ function modeDelaysFromPersisted(raw) {
1518
1519
  }
1519
1520
  kept[field] = value;
1520
1521
  }
1521
- if (kept.exitDelaySec !== void 0 || kept.entryDelaySec !== void 0 || kept.triggeredDurationSec !== void 0) entries.push(kept);
1522
+ if (item.postTriggeredAction !== void 0) {
1523
+ const action = require_clip_model_registry.NcAlarmPostTriggeredActionSchema.safeParse(item.postTriggeredAction);
1524
+ if (action.success) kept.postTriggeredAction = action.data;
1525
+ else dropped.push({
1526
+ mode: item.mode,
1527
+ why: "out-of-bounds",
1528
+ field: "triggeredDurationSec"
1529
+ });
1530
+ }
1531
+ if (item.schedule !== void 0) {
1532
+ const schedule = require_clip_model_registry.NcScheduleSchema.safeParse(item.schedule);
1533
+ if (schedule.success) kept.schedule = schedule.data;
1534
+ else dropped.push({
1535
+ mode: item.mode,
1536
+ why: "out-of-bounds",
1537
+ field: "schedule"
1538
+ });
1539
+ }
1540
+ if (kept.exitDelaySec !== void 0 || kept.entryDelaySec !== void 0 || kept.triggeredDurationSec !== void 0 || kept.postTriggeredAction !== void 0 || kept.schedule !== void 0) entries.push(kept);
1522
1541
  }
1523
1542
  return {
1524
1543
  entries,
@@ -1534,16 +1553,15 @@ function modeDelaysFromPersisted(raw) {
1534
1553
  * (`resolveModeDelays`): `home` has no exit delay on purpose — nobody is
1535
1554
  * leaving — and says so with its own entry, not by this shared value.
1536
1555
  *
1537
- * `triggeredDurationSec: 0` = "sound until somebody disarms it". That is what
1538
- * the panel did before the field existed, so an install that never opens the
1539
- * tab keeps behaving exactly as it did — a default that silently re-armed
1540
- * every existing alarm after N seconds would be a behaviour change nobody
1541
- * asked for.
1556
+ * Default post-triggered action is `rearmAfter` 10 s — every mode inherits
1557
+ * that unless the operator overrides. Older blobs that still store the stock
1558
+ * `0` + absent/`doNothing` seed are migrated once at panel boot (D701).
1542
1559
  */
1543
1560
  var DEFAULT_DELAYS = {
1544
1561
  exitDelaySec: 30,
1545
1562
  entryDelaySec: 20,
1546
- triggeredDurationSec: 0
1563
+ triggeredDurationSec: 10,
1564
+ postTriggeredAction: "rearmAfter"
1547
1565
  };
1548
1566
  var DISARMED = {
1549
1567
  target: "disarmed",
@@ -1561,7 +1579,7 @@ function armedState(mode) {
1561
1579
  * answer is still correct the moment anyone asks.
1562
1580
  */
1563
1581
  function stateAt(m, now) {
1564
- if (m.clearAt !== void 0 && now >= m.clearAt) return m.target;
1582
+ if (m.clearAt !== void 0 && now >= m.clearAt) return m.clearToDisarmed === true ? "disarmed" : m.target;
1565
1583
  if (m.fired) return "triggered";
1566
1584
  if (m.triggerAt !== void 0) return now >= m.triggerAt ? "triggered" : "pending";
1567
1585
  if (m.armedAt !== void 0) return now >= m.armedAt ? m.target : "arming";
@@ -1657,10 +1675,18 @@ function trigger(m, delays, now) {
1657
1675
  changedAt: now
1658
1676
  };
1659
1677
  }
1660
- /** `clearAt` iff the operator asked for a bounded siren — see {@link AlarmDelays}. */
1678
+ /**
1679
+ * `clearAt` (+ optional disarm) when the operator asked for a bounded
1680
+ * post-triggered action — see {@link AlarmDelays}.
1681
+ */
1661
1682
  function autoClear(delays, firesAt) {
1662
- if (delays.triggeredDurationSec <= 0) return {};
1663
- return { clearAt: firesAt + delays.triggeredDurationSec * 1e3 };
1683
+ if (delays.postTriggeredAction === "doNothing") return {};
1684
+ const clearAt = firesAt + Math.max(0, delays.triggeredDurationSec) * 1e3;
1685
+ if (delays.postTriggeredAction === "disableAfter") return {
1686
+ clearAt,
1687
+ clearToDisarmed: true
1688
+ };
1689
+ return { clearAt };
1664
1690
  }
1665
1691
  /**
1666
1692
  * Settle the machine so a stored value never depends on when it is read.
@@ -1670,16 +1696,23 @@ function autoClear(delays, firesAt) {
1670
1696
  * than only in the reading of it.
1671
1697
  */
1672
1698
  function settle(m, now) {
1673
- if (m.clearAt !== void 0 && now >= m.clearAt) return {
1674
- target: m.target,
1675
- fired: false,
1676
- changedAt: m.clearAt
1677
- };
1699
+ if (m.clearAt !== void 0 && now >= m.clearAt) {
1700
+ if (m.clearToDisarmed === true) return {
1701
+ ...DISARMED,
1702
+ changedAt: m.clearAt
1703
+ };
1704
+ return {
1705
+ target: m.target,
1706
+ fired: false,
1707
+ changedAt: m.clearAt
1708
+ };
1709
+ }
1678
1710
  if (m.triggerAt !== void 0 && now >= m.triggerAt) return {
1679
1711
  target: m.target,
1680
1712
  fired: true,
1681
1713
  changedAt: m.triggerAt,
1682
- ...m.clearAt !== void 0 ? { clearAt: m.clearAt } : {}
1714
+ ...m.clearAt !== void 0 ? { clearAt: m.clearAt } : {},
1715
+ ...m.clearToDisarmed === true ? { clearToDisarmed: true } : {}
1683
1716
  };
1684
1717
  if (m.armedAt !== void 0 && now >= m.armedAt) return {
1685
1718
  target: m.target,
@@ -1826,7 +1859,8 @@ function alarmTransition(input) {
1826
1859
  kind: "alarm-disarmed",
1827
1860
  state: next,
1828
1861
  previous,
1829
- at
1862
+ at,
1863
+ ...input.autoCleared === true ? { autoCleared: true } : {}
1830
1864
  };
1831
1865
  return null;
1832
1866
  }
@@ -1917,6 +1951,17 @@ var ncAlarmPanelSchema = require_clip_model_registry.object({
1917
1951
  entryDelaySec: require_clip_model_registry.number().int().min(0).max(600).default(DEFAULT_DELAYS.entryDelaySec),
1918
1952
  triggeredDurationSec: require_clip_model_registry.number().int().min(0).max(3600).default(DEFAULT_DELAYS.triggeredDurationSec),
1919
1953
  /**
1954
+ * What happens when the triggered window ends. Absent on older blobs →
1955
+ * derived from {@link triggeredDurationSec} at read time (never defaulted
1956
+ * here: a Zod default would force `doNothing` onto installs that already
1957
+ * had a positive `triggeredDurationSec` and silently stop re-arming).
1958
+ */
1959
+ postTriggeredAction: require_clip_model_registry._enum([
1960
+ "rearmAfter",
1961
+ "disableAfter",
1962
+ "doNothing"
1963
+ ]).optional(),
1964
+ /**
1920
1965
  * VESTIGIAL (D630): nothing reads it to decide anything any more — every
1921
1966
  * transition is announced to `announceTargets`. Kept so a persisted blob
1922
1967
  * and the settings cap still round-trip it, and read once at boot to tell
@@ -1975,7 +2020,14 @@ var ncAlarmPanelSchema = require_clip_model_registry.object({
1975
2020
  mode: require_clip_model_registry.string(),
1976
2021
  exitDelaySec: require_clip_model_registry.number().optional(),
1977
2022
  entryDelaySec: require_clip_model_registry.number().optional(),
1978
- triggeredDurationSec: require_clip_model_registry.number().optional()
2023
+ triggeredDurationSec: require_clip_model_registry.number().optional(),
2024
+ postTriggeredAction: require_clip_model_registry._enum([
2025
+ "rearmAfter",
2026
+ "disableAfter",
2027
+ "doNothing"
2028
+ ]).optional(),
2029
+ /** Loose — re-validated through {@link modeDelaysFromPersisted}. */
2030
+ schedule: require_clip_model_registry.unknown().optional()
1979
2031
  })).default([]),
1980
2032
  /**
1981
2033
  * Active alarm configuration profile id. Empty / absent on a blob older than
@@ -2047,12 +2099,18 @@ function settingsBlobFromPersisted(raw) {
2047
2099
  if (typeof rec["exitDelaySec"] === "number") entry.exitDelaySec = rec["exitDelaySec"];
2048
2100
  if (typeof rec["entryDelaySec"] === "number") entry.entryDelaySec = rec["entryDelaySec"];
2049
2101
  if (typeof rec["triggeredDurationSec"] === "number") entry.triggeredDurationSec = rec["triggeredDurationSec"];
2102
+ if (typeof rec["postTriggeredAction"] === "string") entry.postTriggeredAction = rec["postTriggeredAction"];
2103
+ if (rec["schedule"] !== void 0) entry.schedule = rec["schedule"];
2050
2104
  modeDelaysLoose.push(entry);
2051
2105
  }
2106
+ const truncatedTriggered = Math.max(0, Math.min(3600, Math.trunc(triggered)));
2107
+ const rawAction = raw["postTriggeredAction"];
2108
+ const postTriggeredAction = rawAction === "rearmAfter" || rawAction === "disableAfter" || rawAction === "doNothing" ? rawAction : void 0;
2052
2109
  return {
2053
2110
  exitDelaySec: Math.max(0, Math.min(600, Math.trunc(exit))),
2054
2111
  entryDelaySec: Math.max(0, Math.min(600, Math.trunc(entry))),
2055
- triggeredDurationSec: Math.max(0, Math.min(3600, Math.trunc(triggered))),
2112
+ triggeredDurationSec: truncatedTriggered,
2113
+ ...postTriggeredAction !== void 0 ? { postTriggeredAction } : {},
2056
2114
  announceTargets,
2057
2115
  nonBlocking: lists("nonBlocking"),
2058
2116
  blocking: lists("blocking"),
@@ -2065,6 +2123,7 @@ function settingsToPersisted(settings) {
2065
2123
  exitDelaySec: settings.exitDelaySec,
2066
2124
  entryDelaySec: settings.entryDelaySec,
2067
2125
  triggeredDurationSec: settings.triggeredDurationSec,
2126
+ ...settings.postTriggeredAction !== void 0 ? { postTriggeredAction: settings.postTriggeredAction } : {},
2068
2127
  announceTargets: [...settings.announceTargets],
2069
2128
  nonBlocking: settings.nonBlocking.map((l) => ({
2070
2129
  mode: l.mode,
@@ -2149,11 +2208,13 @@ function machineFromPersisted(raw) {
2149
2208
  const armedAt = optionalInstant(rec["armedAt"]);
2150
2209
  const triggerAt = optionalInstant(rec["triggerAt"]);
2151
2210
  const clearAt = optionalInstant(rec["clearAt"]);
2211
+ const clearToDisarmed = rec["clearToDisarmed"] === true;
2152
2212
  return {
2153
2213
  target: rec["target"],
2154
2214
  ...armedAt !== void 0 ? { armedAt } : {},
2155
2215
  ...triggerAt !== void 0 ? { triggerAt } : {},
2156
2216
  ...clearAt !== void 0 ? { clearAt } : {},
2217
+ ...clearToDisarmed ? { clearToDisarmed: true } : {},
2157
2218
  fired: rec["fired"] === true,
2158
2219
  changedAt: rec["changedAt"]
2159
2220
  };
@@ -2193,14 +2254,48 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2193
2254
  super(ctx, ncAlarmPanelSchema, { type: require_clip_model_registry.DeviceType.AlarmPanel });
2194
2255
  this.logDroppedModeDelays();
2195
2256
  this.restore();
2257
+ this.migrateStockAfterTriggerDefaults();
2196
2258
  this.registerAlarmCap();
2197
2259
  this.publish();
2198
2260
  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 } });
2261
+ if (this.config.get("announceTargets").length > 0) {
2262
+ this.ctx.logger.info("alarm announceTargets cleared — deliver via alarm rules only", {
2263
+ tags: { deviceId: this.id },
2264
+ meta: { cleared: this.config.get("announceTargets").length }
2265
+ });
2266
+ this.config.set("announceTargets", []);
2267
+ }
2268
+ }
2269
+ /**
2270
+ * One-shot: the pre-D701 stock seed was `triggeredDurationSec: 0` + absent /
2271
+ * `doNothing` (stay triggered forever). That left After trigger looking dead
2272
+ * in the UI. Flip only that seed to {@link DEFAULT_DELAYS} — never a custom
2273
+ * duration or an explicit `disableAfter`.
2274
+ */
2275
+ migrateStockAfterTriggerDefaults() {
2276
+ const duration = this.config.get("triggeredDurationSec");
2277
+ const action = this.config.get("postTriggeredAction");
2278
+ if (!(duration === 0 && (action === void 0 || action === "doNothing"))) return;
2279
+ this.config.set("triggeredDurationSec", DEFAULT_DELAYS.triggeredDurationSec);
2280
+ this.config.set("postTriggeredAction", DEFAULT_DELAYS.postTriggeredAction);
2281
+ this.ctx.logger.info("alarm after-trigger defaults migrated to rearmAfter 10s", {
2282
+ tags: { deviceId: this.id },
2283
+ meta: {
2284
+ fromDurationSec: duration,
2285
+ fromAction: action ?? "absent",
2286
+ toDurationSec: DEFAULT_DELAYS.triggeredDurationSec,
2287
+ toAction: DEFAULT_DELAYS.postTriggeredAction
2288
+ }
2289
+ });
2199
2290
  }
2200
2291
  /** The operator-visible configuration, as the cap serves it. */
2201
2292
  settings() {
2293
+ const storedAction = this.config.get("postTriggeredAction");
2202
2294
  return {
2203
- ...this.readDelays(),
2295
+ exitDelaySec: this.config.get("exitDelaySec"),
2296
+ entryDelaySec: this.config.get("entryDelaySec"),
2297
+ triggeredDurationSec: this.config.get("triggeredDurationSec"),
2298
+ ...storedAction !== void 0 ? { postTriggeredAction: storedAction } : {},
2204
2299
  announceArm: this.config.get("announceArm"),
2205
2300
  announceTargets: [...this.config.get("announceTargets")],
2206
2301
  nonBlocking: modeDeviceListFromPersisted(this.config.get("nonBlocking")),
@@ -2349,6 +2444,7 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2349
2444
  if (patch.exitDelaySec !== void 0) await this.config.set("exitDelaySec", patch.exitDelaySec);
2350
2445
  if (patch.entryDelaySec !== void 0) await this.config.set("entryDelaySec", patch.entryDelaySec);
2351
2446
  if (patch.triggeredDurationSec !== void 0) await this.config.set("triggeredDurationSec", patch.triggeredDurationSec);
2447
+ if (patch.postTriggeredAction !== void 0) await this.config.set("postTriggeredAction", patch.postTriggeredAction);
2352
2448
  if (patch.announceArm !== void 0) await this.config.set("announceArm", patch.announceArm);
2353
2449
  if (patch.announceTargets !== void 0) await this.config.set("announceTargets", [...patch.announceTargets]);
2354
2450
  if (membership !== void 0 && membershipChanged !== null) this.ctx.logger.info("alarm non-blocking list changed", {
@@ -2524,6 +2620,29 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2524
2620
  }
2525
2621
  });
2526
2622
  }
2623
+ if (edit.postTriggeredAction !== void 0) {
2624
+ const from = was?.postTriggeredAction ?? null;
2625
+ const to = now?.postTriggeredAction ?? null;
2626
+ if (from !== to) this.ctx.logger.info("alarm mode delays changed", {
2627
+ tags: { deviceId: this.id },
2628
+ meta: {
2629
+ mode: edit.mode,
2630
+ field: "postTriggeredAction",
2631
+ from,
2632
+ to
2633
+ }
2634
+ });
2635
+ }
2636
+ if (edit.schedule !== void 0) {
2637
+ 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", {
2638
+ tags: { deviceId: this.id },
2639
+ meta: {
2640
+ mode: edit.mode,
2641
+ cleared: edit.schedule === null,
2642
+ windows: now?.schedule?.windows.length ?? 0
2643
+ }
2644
+ });
2645
+ }
2527
2646
  }
2528
2647
  }
2529
2648
  /** One INFO line per persisted per-mode delay entry the guard refused to trust. */
@@ -2582,6 +2701,13 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2582
2701
  */
2583
2702
  async armWith(mode) {
2584
2703
  const now = Date.now();
2704
+ if (!this.modeScheduleAllows(mode, now)) {
2705
+ this.ctx.logger.info("alarm arm refused — outside activation schedule", {
2706
+ tags: { deviceId: this.id },
2707
+ meta: { mode }
2708
+ });
2709
+ return { outcome: "outside-schedule" };
2710
+ }
2585
2711
  const read = await this.applyArmGuard(mode);
2586
2712
  if (read.kind === "unguarded") return this.settleArm(mode, null, now);
2587
2713
  if (read.kind === "check-failed") {
@@ -2595,6 +2721,11 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2595
2721
  why: this.exclusionWhyFor(mode, device)
2596
2722
  })), now);
2597
2723
  }
2724
+ /** Absent schedule = always allowed (`isScheduleActive`). */
2725
+ modeScheduleAllows(mode, now) {
2726
+ const schedule = modeDelaysFromPersisted(this.config.get("modeDelays")).entries.find((e) => e.mode === mode)?.schedule;
2727
+ return require_clip_model_registry.isScheduleActive(schedule, now);
2728
+ }
2598
2729
  /**
2599
2730
  * The Arm anyway on ONE refusal's notification — the only path that arms
2600
2731
  * around a device that blocks.
@@ -2620,6 +2751,17 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2620
2751
  const reason = refusalId === this.spentRefusalId ? "used" : "unknown-refusal";
2621
2752
  return this.rejectArmAnyway(refusalId, reason);
2622
2753
  }
2754
+ if (!this.modeScheduleAllows(held.mode, now)) {
2755
+ this.refusal = null;
2756
+ this.ctx.logger.info("alarm arm-anyway refused — outside activation schedule", {
2757
+ tags: { deviceId: this.id },
2758
+ meta: {
2759
+ mode: held.mode,
2760
+ refusalId
2761
+ }
2762
+ });
2763
+ return { outcome: "outside-schedule" };
2764
+ }
2623
2765
  if (this.machine.changedAt !== held.panelChangedAt) {
2624
2766
  this.refusal = null;
2625
2767
  return this.rejectArmAnyway(refusalId, "stale");
@@ -3015,11 +3157,12 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
3015
3157
  * rather than fired only once somebody looks.
3016
3158
  */
3017
3159
  publish(now = Date.now()) {
3160
+ const autoCleared = this.machine.clearAt !== void 0 && now >= this.machine.clearAt && this.machine.clearToDisarmed === true;
3018
3161
  this.machine = settle(this.machine, now);
3019
3162
  const state = stateAt(this.machine, now);
3020
3163
  this.runtimeState.setCapState("alarm-panel", this.status(now));
3021
3164
  this.persist();
3022
- this.report(state, now);
3165
+ this.report(state, now, autoCleared);
3023
3166
  if (this.machine.armedAt !== void 0 || this.machine.triggerAt !== void 0 || this.machine.clearAt !== void 0) this.startTicking();
3024
3167
  else this.stopTicking();
3025
3168
  }
@@ -3031,7 +3174,7 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
3031
3174
  * panel's own state update with it would make a delivery failure look like
3032
3175
  * an alarm failure.
3033
3176
  */
3034
- report(state, now) {
3177
+ report(state, now, autoCleared = false) {
3035
3178
  const previous = this.lastPublished;
3036
3179
  this.lastPublished = state;
3037
3180
  const hook = this.onPublish;
@@ -3046,7 +3189,8 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
3046
3189
  at: now,
3047
3190
  ...source !== null ? { source } : {},
3048
3191
  ...arming !== void 0 ? { arming } : {},
3049
- ...pending !== void 0 ? { pending } : {}
3192
+ ...pending !== void 0 ? { pending } : {},
3193
+ ...autoCleared ? { autoCleared: true } : {}
3050
3194
  });
3051
3195
  if (transition?.kind === "alarm-triggered") this.attribution.clear();
3052
3196
  try {
@@ -3132,8 +3276,12 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
3132
3276
  }
3133
3277
  /** The global durations plus the per-mode overrides, read from the persisted config NOW. */
3134
3278
  delaySettings() {
3279
+ const storedAction = this.config.get("postTriggeredAction");
3135
3280
  return {
3136
- ...this.readDelays(),
3281
+ exitDelaySec: this.config.get("exitDelaySec"),
3282
+ entryDelaySec: this.config.get("entryDelaySec"),
3283
+ triggeredDurationSec: this.config.get("triggeredDurationSec"),
3284
+ ...storedAction !== void 0 ? { postTriggeredAction: storedAction } : {},
3137
3285
  modeDelays: modeDelaysFromPersisted(this.config.get("modeDelays")).entries
3138
3286
  };
3139
3287
  }
@@ -3143,24 +3291,32 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
3143
3291
  */
3144
3292
  delaysFor(mode) {
3145
3293
  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
- };
3294
+ if (mode === null) {
3295
+ const delays = this.readDelays();
3296
+ return {
3297
+ exitDelaySec: delays.exitDelaySec,
3298
+ entryDelaySec: delays.entryDelaySec,
3299
+ triggeredDurationSec: delays.triggeredDurationSec,
3300
+ postTriggeredAction: delays.postTriggeredAction,
3301
+ source: {
3302
+ exitDelaySec: "default",
3303
+ entryDelaySec: "default",
3304
+ triggeredDurationSec: "default",
3305
+ postTriggeredAction: "default"
3306
+ }
3307
+ };
3308
+ }
3156
3309
  return require_clip_model_registry.resolveModeDelays(settings, mode);
3157
3310
  }
3158
- /** The three global durations, read from the persisted config. */
3311
+ /** The global durations + post-triggered action, read from the persisted config. */
3159
3312
  readDelays() {
3313
+ const triggeredDurationSec = this.config.get("triggeredDurationSec");
3314
+ const stored = this.config.get("postTriggeredAction");
3160
3315
  return {
3161
3316
  exitDelaySec: this.config.get("exitDelaySec"),
3162
3317
  entryDelaySec: this.config.get("entryDelaySec"),
3163
- triggeredDurationSec: this.config.get("triggeredDurationSec")
3318
+ triggeredDurationSec,
3319
+ postTriggeredAction: require_clip_model_registry.derivePostTriggeredAction(stored, triggeredDurationSec)
3164
3320
  };
3165
3321
  }
3166
3322
  startTicking() {
@@ -5141,7 +5297,7 @@ function ncClassNoun(texts, className, count) {
5141
5297
  count,
5142
5298
  vars: { name: className }
5143
5299
  });
5144
- if (require_clip_model_registry.isEventKindLabelKey(normalised)) return require_clip_model_registry.eventKindLabel(normalised, texts.language);
5300
+ if (require_clip_model_registry.isEventKindLabelKey(normalised)) return require_clip_model_registry.eventKindLabel(normalised, texts.language).toLocaleLowerCase(texts.language);
5145
5301
  return texts.text({
5146
5302
  key: "class.other",
5147
5303
  count,
@@ -5149,13 +5305,24 @@ function ncClassNoun(texts, className, count) {
5149
5305
  });
5150
5306
  }
5151
5307
  /**
5152
- * The parenthetical half of a labelled subject: a recognised person or plate
5153
- * is printed verbatim; a classification (species, COCO sub, …) goes through
5154
- * the shared event-kind map so "animal (cat)" becomes "animale (Gatto)".
5308
+ * The ONE capitalisation the notification body gets: its first letter, after
5309
+ * every catalog fragment and placeholder has already been rendered lowercase.
5310
+ * Title, fragments and mid-sentence summaries are never passed through here.
5155
5311
  */
5156
- function ncSubjectLabelText(texts, label, labelKind) {
5157
- if (labelKind === "identity" || labelKind === "plate") return label;
5158
- return require_clip_model_registry.eventKindLabel(label, texts.language);
5312
+ function ncSentenceCaseBody(body) {
5313
+ if (body.length === 0) return body;
5314
+ const first = body[0];
5315
+ const upper = first.toLocaleUpperCase();
5316
+ if (upper === first) return body;
5317
+ return `${upper}${body.slice(1)}`;
5318
+ }
5319
+ /**
5320
+ * True when `label` is a taxonomy / classification id (species, COCO sub, …)
5321
+ * rather than a person name or plate. Those REPLACE the coarse class in the
5322
+ * subject — "dog" not "animal (dog)", "cane" not "animale (cane)".
5323
+ */
5324
+ function isClassificationLabel(label) {
5325
+ return require_clip_model_registry.isEventKindLabelKey(label.trim().toLowerCase());
5159
5326
  }
5160
5327
  /** `person (John)` / `persona (John)` — the thing the notification is about. */
5161
5328
  function ncSubjectText(texts, input) {
@@ -5164,11 +5331,15 @@ function ncSubjectText(texts, input) {
5164
5331
  key: "detection.subject",
5165
5332
  vars: { classNoun }
5166
5333
  });
5334
+ if (input.labelKind !== "identity" && input.labelKind !== "plate" && isClassificationLabel(input.label)) return texts.text({
5335
+ key: "detection.subject",
5336
+ vars: { classNoun: ncClassNoun(texts, input.label, 1) }
5337
+ });
5167
5338
  return texts.text({
5168
5339
  key: subjectLabelKey(input.labelKind),
5169
5340
  vars: {
5170
5341
  classNoun,
5171
- label: ncSubjectLabelText(texts, input.label, input.labelKind)
5342
+ label: input.label
5172
5343
  }
5173
5344
  });
5174
5345
  }
@@ -5758,7 +5929,7 @@ function isAdoptable(record, nowMs) {
5758
5929
  * what `localDayMinute` does for the same input — one behaviour for a bad
5759
5930
  * zone, not two.
5760
5931
  */
5761
- function clockOf$6(atMs, timeZone) {
5932
+ function clockOf$7(atMs, timeZone) {
5762
5933
  if (timeZone !== void 0) try {
5763
5934
  return new Intl.DateTimeFormat("en-GB", {
5764
5935
  timeZone,
@@ -5894,7 +6065,7 @@ var TimelapseRenderer = class {
5894
6065
  includeAudio: false,
5895
6066
  maxLifeMs: await this.ports.exportLifeMs(),
5896
6067
  deleteAfterDownload: false,
5897
- title: `${request.ruleName} ${clockOf$6(window.startMs, request.timezone)}→${clockOf$6(window.endMs, request.timezone)}`,
6068
+ title: `${request.ruleName} ${clockOf$7(window.startMs, request.timezone)}→${clockOf$7(window.endMs, request.timezone)}`,
5898
6069
  timelapse: {
5899
6070
  everyMs,
5900
6071
  outputFps: request.framerate,
@@ -6225,7 +6396,7 @@ var TimelapseRenderer = class {
6225
6396
  ...meta,
6226
6397
  unservedWindowCount: verdict.unservedWindows.length,
6227
6398
  unservedWindows: verdict.unservedWindows.slice(0, 12).map((w) => ({
6228
- at: clockOf$6(request.window.startMs + w.fromSec * 1e3, request.timezone),
6399
+ at: clockOf$7(request.window.startMs + w.fromSec * 1e3, request.timezone),
6229
6400
  fromSec: Math.round(w.fromSec),
6230
6401
  toSec: Math.round(w.toSec),
6231
6402
  expectedDenseFrames: w.expectedDenseFrames,
@@ -8806,6 +8977,7 @@ function buildActionButtons(input) {
8806
8977
  sequenceId: button.sequenceId,
8807
8978
  actionId: button.id
8808
8979
  }),
8980
+ mode: "background",
8809
8981
  ...button.icon !== void 0 ? { icon: button.icon } : {},
8810
8982
  ...button.destructive !== void 0 ? { destructive: button.destructive } : {}
8811
8983
  });
@@ -8921,10 +9093,9 @@ var MAX_ARM_BUTTONS = 3;
8921
9093
  * (`ALARM_DISARM_TTL_MS`). Both exist: the hint is for the person, the TTL is
8922
9094
  * for the forwarded screenshot.
8923
9095
  *
8924
- * Arm buttons are `background` — arming from the lock screen should not open a
8925
- * browser. The disarm is `navigate`: it removes a protection, and the operator
8926
- * doing it deserves to SEE that it happened rather than trust a POST nobody
8927
- * confirmed.
9096
+ * Every alarm button is `background` — Zentik (and ntfy) fire a POST and leave
9097
+ * the lock screen alone. Opening a browser to arm or disarm costs more
9098
+ * attention than the notification did.
8928
9099
  *
8929
9100
  * `panel.availableModes` ARE the enabled modes: CamStack has no per-mode
8930
9101
  * disable setting, so the Arm buttons offer exactly what the panel declares.
@@ -8958,14 +9129,14 @@ function buildAlarmButtons(input) {
8958
9129
  }),
8959
9130
  icon: "disarm",
8960
9131
  destructive: true,
8961
- mode: "navigate"
9132
+ mode: "background"
8962
9133
  }];
8963
9134
  }
8964
9135
  /**
8965
9136
  * The one Arm anyway a refusal notification carries — or none, when the row
8966
- * names no refusal. `navigate` and destructive, like the disarm: it arms
8967
- * AROUND an open door, and the person doing it should see it happen. Its TTL
8968
- * is the refusal's own (`ARM_ANYWAY_TTL_MS`, priced by the minter).
9137
+ * names no refusal. Destructive + background: it arms around an open door via
9138
+ * POST (Zentik BACKGROUND_CALL), same as disarm. Its TTL is the refusal's own
9139
+ * (`ARM_ANYWAY_TTL_MS`, priced by the minter).
8969
9140
  */
8970
9141
  function armAnywayButton(refusalId, input) {
8971
9142
  if (refusalId === void 0) return [];
@@ -8980,7 +9151,7 @@ function armAnywayButton(refusalId, input) {
8980
9151
  }),
8981
9152
  icon: "arm",
8982
9153
  destructive: true,
8983
- mode: "navigate"
9154
+ mode: "background"
8984
9155
  }];
8985
9156
  }
8986
9157
  //#endregion
@@ -12902,6 +13073,10 @@ function alarmTextKeys(transition, body) {
12902
13073
  titleKey,
12903
13074
  bodyKey: `system.${kind}.body.${transition.sensor.event}`
12904
13075
  };
13076
+ if (kind === "alarm-disarmed" && transition.autoCleared === true) return {
13077
+ titleKey,
13078
+ bodyKey: `system.${kind}.body.autoCleared`
13079
+ };
12905
13080
  if (kind === "alarm-disarmed" && (transition.previous === "triggered" || transition.previous === "pending")) return {
12906
13081
  titleKey,
12907
13082
  bodyKey: `system.${kind}.body.defused`
@@ -14621,13 +14796,14 @@ var en_default = {
14621
14796
  "system.alarm-arming.title.away": "Arming away — {{seconds}}s",
14622
14797
  "system.alarm-arming.title.night": "Arming night — {{seconds}}s",
14623
14798
  "system.alarm-arming.body": "Arming",
14624
- "system.alarm-armed.title.home": "Armed: home",
14625
- "system.alarm-armed.title.away": "Armed: away",
14626
- "system.alarm-armed.title.night": "Armed: night",
14799
+ "system.alarm-armed.title.home": "Alarm armed: home",
14800
+ "system.alarm-armed.title.away": "Alarm armed: away",
14801
+ "system.alarm-armed.title.night": "Alarm armed: night",
14627
14802
  "system.alarm-armed.body": "Alarm armed",
14628
14803
  "system.alarm-disarmed.title": "Disarmed",
14629
14804
  "system.alarm-disarmed.body": "Alarm disarmed",
14630
14805
  "system.alarm-disarmed.body.defused": "Alarm disarmed during an alarm",
14806
+ "system.alarm-disarmed.body.autoCleared": "Alarm disarmed automatically after the siren",
14631
14807
  "system.alarm-triggered.title": "ALARM",
14632
14808
  "system.alarm-triggered.body": "Alarm triggered",
14633
14809
  "system.alarm-triggered.body.rule": "Triggered by {{rule}} on {{deviceName}}",
@@ -14949,13 +15125,14 @@ var it_default = {
14949
15125
  "system.alarm-arming.title.away": "Inserimento Fuori — {{seconds}}s",
14950
15126
  "system.alarm-arming.title.night": "Inserimento Notte — {{seconds}}s",
14951
15127
  "system.alarm-arming.body": "Inserimento in corso",
14952
- "system.alarm-armed.title.home": "Inserito: Casa",
14953
- "system.alarm-armed.title.away": "Inserito: Fuori",
14954
- "system.alarm-armed.title.night": "Inserito: Notte",
15128
+ "system.alarm-armed.title.home": "Allarme inserito: Casa",
15129
+ "system.alarm-armed.title.away": "Allarme inserito: Fuori",
15130
+ "system.alarm-armed.title.night": "Allarme inserito: Notte",
14955
15131
  "system.alarm-armed.body": "Allarme inserito",
14956
15132
  "system.alarm-disarmed.title": "Disinserito",
14957
15133
  "system.alarm-disarmed.body": "Allarme disinserito",
14958
15134
  "system.alarm-disarmed.body.defused": "Allarme disinserito durante un allarme",
15135
+ "system.alarm-disarmed.body.autoCleared": "Allarme disinserito automaticamente dopo la sirena",
14959
15136
  "system.alarm-triggered.title": "ALLARME",
14960
15137
  "system.alarm-triggered.body": "Allarme scattato",
14961
15138
  "system.alarm-triggered.body.rule": "Scattato da {{rule}} su {{deviceName}}",
@@ -23958,7 +24135,7 @@ var NcDispatcher = class {
23958
24135
  ...systemCount,
23959
24136
  ...systemEvent?.body !== void 0 ? { frozen: systemEvent.body } : {}
23960
24137
  });
23961
- return composeRuleAiBody({
24138
+ return ncSentenceCaseBody(composeRuleAiBody({
23962
24139
  aiText,
23963
24140
  body: entry.payload.synthetic === true ? this.renderText({
23964
24141
  key: "test.prefix",
@@ -23966,7 +24143,7 @@ var NcDispatcher = class {
23966
24143
  frozen: rendered
23967
24144
  }) : rendered,
23968
24145
  templateNamesAi
23969
- });
24146
+ }));
23970
24147
  };
23971
24148
  const body = renderBody("");
23972
24149
  const resolved = await this.resolveAttachments(entry);
@@ -25146,15 +25323,18 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
25146
25323
  const systemEvent = subject.systemEvent;
25147
25324
  const detection = systemEvent?.alarmSource?.className !== void 0 ? systemEvent.alarmSource : void 0;
25148
25325
  const group = readOutboxGroup(entry.payload.group);
25326
+ const locale = texts.language;
25327
+ const cameraName = deviceName.toLocaleLowerCase(locale);
25328
+ const zonesLower = zoneLabels.map((z) => z.toLocaleLowerCase(locale));
25149
25329
  return {
25150
- camera: deviceName,
25330
+ camera: cameraName,
25151
25331
  class: detection?.className ?? (systemEvent !== void 0 ? "" : subject.className),
25152
25332
  classLabel: (() => {
25153
25333
  const wire = detection?.className ?? (systemEvent !== void 0 ? "" : subject.className);
25154
- return wire.length === 0 ? "" : require_clip_model_registry.eventKindLabel(wire, texts.language);
25334
+ return wire.length === 0 ? "" : require_clip_model_registry.eventKindLabel(wire, locale).toLocaleLowerCase(locale);
25155
25335
  })(),
25156
25336
  label: detection?.label ?? subject.label ?? "",
25157
- zones: zoneLabels.join(", "),
25337
+ zones: zonesLower.join(", "),
25158
25338
  subject: detection?.className !== void 0 ? ncSubjectText(texts, {
25159
25339
  className: detection.className,
25160
25340
  ...detection.label !== void 0 ? { label: detection.label } : {}
@@ -25164,9 +25344,9 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
25164
25344
  ...subject.labelKind !== void 0 ? { labelKind: subject.labelKind } : {}
25165
25345
  }),
25166
25346
  detectionSummary: group !== void 0 && group.members.length > 1 ? ncDetectionSummary(texts, classCounts(group.members)) : "",
25167
- inZones: ncInZonesText(texts, zoneLabels),
25168
- scope: occupancy !== void 0 ? occupancy.zone ?? deviceName : "",
25169
- zone: occupancy?.zone ?? zoneLabels[0] ?? "",
25347
+ inZones: ncInZonesText(texts, zonesLower),
25348
+ scope: occupancy !== void 0 ? occupancy.zone?.toLocaleLowerCase(locale) ?? cameraName : "",
25349
+ zone: (occupancy?.zone ?? zoneLabels[0] ?? "").toLocaleLowerCase(locale),
25170
25350
  confidence: percentOf(detection?.confidence ?? subject.confidence),
25171
25351
  time: ncClockText(texts.language, subject.timestamp, timeZone),
25172
25352
  sounds: subject.audio?.labels.join(", ") ?? "",
@@ -29664,7 +29844,7 @@ function buildRuleAiSystemPrompt(shape, language, maxWords, grouped = false) {
29664
29844
  ].join("");
29665
29845
  }
29666
29846
  /** `HH:MM` in the host timezone. */
29667
- function clockOf$5(atMs) {
29847
+ function clockOf$6(atMs) {
29668
29848
  const d = new Date(atMs);
29669
29849
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
29670
29850
  }
@@ -29702,7 +29882,7 @@ function buildRuleAiPrompt(input) {
29702
29882
  if (shape === "detection-track") {
29703
29883
  lines.push(`Camera: ${camera}.${located} Subject: ${subject}. The stills, in order:`);
29704
29884
  for (const [i, frame] of input.frames.entries()) {
29705
- const when = frame.atMs !== void 0 ? ` · ${clockOf$5(frame.atMs)}` : "";
29885
+ const when = frame.atMs !== void 0 ? ` · ${clockOf$6(frame.atMs)}` : "";
29706
29886
  lines.push(`${String(i + 1)}.${when}`);
29707
29887
  }
29708
29888
  return lines.join("\n");
@@ -31473,7 +31653,7 @@ var DEFAULT_PROMPT_EN = "What happened during this period? Summarise it for a ho
31473
31653
  */
31474
31654
  var SummaryAnswerSchema = require_clip_model_registry.object({ summary: require_clip_model_registry.string() });
31475
31655
  /** `HH:MM` in the host timezone — the two numbers a frame label needs. */
31476
- function clockOf$4(atMs) {
31656
+ function clockOf$5(atMs) {
31477
31657
  const d = new Date(atMs);
31478
31658
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
31479
31659
  }
@@ -31496,7 +31676,7 @@ function buildSummaryAiPrompt(input, frames) {
31496
31676
  const roster = frames.map((f, i) => {
31497
31677
  const location = plainLabel(f.location ?? "");
31498
31678
  const where = location.length > 0 ? ` (${location})` : "";
31499
- return `${String(i + 1)}. ${plainLabel(f.deviceName)}${where} · ${clockOf$4(f.atMs)} · ${plainVocabulary(f.className) || "subject"}`;
31679
+ return `${String(i + 1)}. ${plainLabel(f.deviceName)}${where} · ${clockOf$5(f.atMs)} · ${plainVocabulary(f.className) || "subject"}`;
31500
31680
  }).join("\n");
31501
31681
  const described = /* @__PURE__ */ new Map();
31502
31682
  for (const f of frames) {
@@ -32099,7 +32279,7 @@ var NO_DETECTIONS$1 = {
32099
32279
  * has to check is a key somebody can get wrong. */
32100
32280
  var BODY_SEPARATOR$1 = " · ";
32101
32281
  /** `HH:MM` in the host timezone. */
32102
- function clockOf$3(atMs) {
32282
+ function clockOf$4(atMs) {
32103
32283
  const d = new Date(atMs);
32104
32284
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
32105
32285
  }
@@ -32113,8 +32293,8 @@ function clockOf$3(atMs) {
32113
32293
  function summaryTemplateVars(input) {
32114
32294
  return {
32115
32295
  rule: input.ruleName,
32116
- from: clockOf$3(input.window.startMs),
32117
- to: clockOf$3(input.window.endMs),
32296
+ from: clockOf$4(input.window.startMs),
32297
+ to: clockOf$4(input.window.endMs),
32118
32298
  time: ncClockText(input.texts.language, input.generatedAt),
32119
32299
  events: String(input.tileCount),
32120
32300
  cameras: String(input.cameraCount),
@@ -32209,11 +32389,11 @@ function summaryRecordId(ruleId, windowEndMs) {
32209
32389
  /** The caption burned onto the mosaic's title band, with the same `{{var}}`
32210
32390
  * vocabulary the title and body use. `''` is a real answer ("no caption"). */
32211
32391
  function summaryCaptionText(input) {
32212
- if (input.captionText === void 0) return `${input.ruleName} — ${clockOf$3(input.window.startMs)}→${clockOf$3(input.window.endMs)}`;
32392
+ if (input.captionText === void 0) return `${input.ruleName} — ${clockOf$4(input.window.startMs)}→${clockOf$4(input.window.endMs)}`;
32213
32393
  return renderTemplate(input.captionText, {
32214
32394
  rule: input.ruleName,
32215
- from: clockOf$3(input.window.startMs),
32216
- to: clockOf$3(input.window.endMs)
32395
+ from: clockOf$4(input.window.startMs),
32396
+ to: clockOf$4(input.window.endMs)
32217
32397
  }) ?? "";
32218
32398
  }
32219
32399
  /**
@@ -33145,7 +33325,7 @@ var NcSummaryProducer = class {
33145
33325
  purpose: "summary",
33146
33326
  tiles: tiles.map(({ candidate, jpeg }) => ({
33147
33327
  jpeg,
33148
- ...plan.tileLabels ? { label: `${names.get(candidate.deviceId) ?? candidate.deviceId} · ${clockOf$2(candidate.firstSeen)} · ${candidate.className}` } : {},
33328
+ ...plan.tileLabels ? { label: `${names.get(candidate.deviceId) ?? candidate.deviceId} · ${clockOf$3(candidate.firstSeen)} · ${candidate.className}` } : {},
33149
33329
  deviceId: candidate.deviceId,
33150
33330
  sourceId: candidate.trackId
33151
33331
  })),
@@ -33368,7 +33548,7 @@ var NcSummaryProducer = class {
33368
33548
  }
33369
33549
  };
33370
33550
  /** `HH:MM` in the host timezone — the two numbers a tile label needs. */
33371
- function clockOf$2(atMs) {
33551
+ function clockOf$3(atMs) {
33372
33552
  const d = new Date(atMs);
33373
33553
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
33374
33554
  }
@@ -33868,6 +34048,99 @@ var NcCalendarEventsStore = class {
33868
34048
  }
33869
34049
  };
33870
34050
  //#endregion
34051
+ //#region src/notification-center/calendar-events/calendar-events-body.ts
34052
+ /** Visual weight on the tomorrow header — Telegram/Zentik plain text. */
34053
+ var CALENDAR_EVENTS_TOMORROW_MARK = "★";
34054
+ /** Separates the tomorrow block from the rest of the horizon. */
34055
+ var CALENDAR_EVENTS_DAY_DELIMITER = "────────";
34056
+ var MAX_EVENT_LINES = 40;
34057
+ function pad2(n) {
34058
+ return String(n).padStart(2, "0");
34059
+ }
34060
+ /** Local calendar day key `YYYY-MM-DD`. */
34061
+ function localDayKey(atMs) {
34062
+ const d = new Date(atMs);
34063
+ return `${String(d.getFullYear())}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
34064
+ }
34065
+ function startOfLocalDay(atMs) {
34066
+ const d = new Date(atMs);
34067
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
34068
+ }
34069
+ function addLocalDays(atMs, days) {
34070
+ const d = new Date(atMs);
34071
+ d.setDate(d.getDate() + days);
34072
+ return d.getTime();
34073
+ }
34074
+ function dayHeadingFor(dayStartMs, todayKey, tomorrowKey) {
34075
+ const key = localDayKey(dayStartMs);
34076
+ const label = new Date(dayStartMs).toLocaleDateString(void 0, {
34077
+ weekday: "short",
34078
+ month: "short",
34079
+ day: "numeric"
34080
+ });
34081
+ if (key === todayKey) return `Today — ${label}`;
34082
+ if (key === tomorrowKey) return `${CALENDAR_EVENTS_TOMORROW_MARK} Tomorrow — ${label}`;
34083
+ return label;
34084
+ }
34085
+ /** `HH:MM` local, or `YYYY-MM-DD HH:MM` when the end falls on another day. */
34086
+ function clockOf$2(atMs) {
34087
+ const d = new Date(atMs);
34088
+ return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
34089
+ }
34090
+ function formatEventLine(event) {
34091
+ const when = event.allDay === true ? "All day" : `${clockOf$2(event.startMs)}–${clockOf$2(event.endMs)}`;
34092
+ const where = event.location !== void 0 && event.location.length > 0 ? ` @ ${event.location}` : "";
34093
+ return `• ${when} ${event.title}${where}`;
34094
+ }
34095
+ function groupByLocalDay(events) {
34096
+ const map = /* @__PURE__ */ new Map();
34097
+ for (const ev of events) {
34098
+ const dayKey = localDayKey(ev.startMs);
34099
+ const existing = map.get(dayKey);
34100
+ if (existing !== void 0) {
34101
+ existing.events.push(ev);
34102
+ continue;
34103
+ }
34104
+ map.set(dayKey, {
34105
+ dayKey,
34106
+ dayStartMs: startOfLocalDay(ev.startMs),
34107
+ events: [ev]
34108
+ });
34109
+ }
34110
+ return [...map.values()].toSorted((a, b) => a.dayStartMs - b.dayStartMs);
34111
+ }
34112
+ /**
34113
+ * Build the digest body.
34114
+ *
34115
+ * @param nowMs — fire time; tomorrow is the local calendar day after this.
34116
+ */
34117
+ function formatCalendarEventsBody(events, nowMs, horizonDays) {
34118
+ if (events.length === 0) return `No events in the next ${String(horizonDays)} day(s).`;
34119
+ const todayKey = localDayKey(nowMs);
34120
+ const tomorrowKey = localDayKey(addLocalDays(startOfLocalDay(nowMs), 1));
34121
+ const buckets = groupByLocalDay(events);
34122
+ const today = buckets.filter((b) => b.dayKey === todayKey);
34123
+ const tomorrow = buckets.filter((b) => b.dayKey === tomorrowKey);
34124
+ const later = buckets.filter((b) => b.dayKey !== todayKey && b.dayKey !== tomorrowKey);
34125
+ const lead = [...today, ...tomorrow];
34126
+ const lines = [];
34127
+ let emitted = 0;
34128
+ const pushBucket = (bucket) => {
34129
+ if (emitted >= MAX_EVENT_LINES) return;
34130
+ lines.push(dayHeadingFor(bucket.dayStartMs, todayKey, tomorrowKey));
34131
+ for (const ev of bucket.events) {
34132
+ if (emitted >= MAX_EVENT_LINES) break;
34133
+ lines.push(formatEventLine(ev));
34134
+ emitted += 1;
34135
+ }
34136
+ };
34137
+ for (const bucket of lead) pushBucket(bucket);
34138
+ if (lead.length > 0 && later.length > 0) lines.push(CALENDAR_EVENTS_DAY_DELIMITER);
34139
+ for (const bucket of later) pushBucket(bucket);
34140
+ if (events.length > MAX_EVENT_LINES) lines.push(`…and ${String(events.length - MAX_EVENT_LINES)} more`);
34141
+ return lines.join("\n");
34142
+ }
34143
+ //#endregion
33871
34144
  //#region src/notification-center/calendar-events/calendar-events-delivery.ts
33872
34145
  function calendarEventsRecordId(ruleId, windowEndMs) {
33873
34146
  return `calendar-events:${ruleId}:${windowEndMs}`;
@@ -33877,16 +34150,8 @@ function clockOf$1(atMs) {
33877
34150
  const d = new Date(atMs);
33878
34151
  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")}`}`;
33879
34152
  }
33880
- function formatEventLine(event) {
33881
- const when = event.allDay ? clockOf$1(event.startMs).slice(0, 10) : `${clockOf$1(event.startMs)}–${clockOf$1(event.endMs)}`;
33882
- const where = event.location !== void 0 && event.location.length > 0 ? ` @ ${event.location}` : "";
33883
- return `• ${when} ${event.title}${where}`;
33884
- }
33885
34153
  function derivedBody$1(input) {
33886
- if (input.events.length === 0) return `No events in the next ${String(input.horizonDays)} day(s).`;
33887
- const lines = input.events.slice(0, 40).map(formatEventLine);
33888
- const extra = input.events.length > 40 ? `\n…and ${String(input.events.length - 40)} more` : "";
33889
- return lines.join("\n") + extra;
34154
+ return formatCalendarEventsBody(input.events, input.eventWindow.startMs, input.horizonDays);
33890
34155
  }
33891
34156
  function templateVars(input) {
33892
34157
  return {
@@ -39332,19 +39597,21 @@ var NotificationCenter = class {
39332
39597
  }
39333
39598
  /**
39334
39599
  * Start photographing every camera that links `sensorId`, and return the
39335
- * owners the sensor-level evaluation names — or `undefined` when there is no
39336
- * set (≤1 linked camera: today's marker is the picture), nothing to show
39337
- * (every linked camera muted), the event will be dropped as muted (the
39338
- * SENSOR is muted, or there were row cameras and every one is muted — see
39339
- * the evaluation's mute gate), or no enabled device-event rule that could
39340
- * fire on this sensor KIND wants a picture (the gate is the RULE's media
39341
- * policy, scope and kind conditions, read from the in-memory mirror — D49,
39342
- * as `audioStillOwner`; an occupancy rule, which rides the same delivery,
39343
- * never fires on a sensor). Coarse on purpose, in one direction only: a
39344
- * capture for a rule whose other conditions then fail is one wasted
39345
- * snapshot, a missing capture is a notification without its picture. The
39346
- * kind check is what keeps a chatty multi-linked motion sensor from
39347
- * composing a mosaic on every edge for a doorbell rule.
39600
+ * owners the sensor-level evaluation names — or `undefined` when there is
39601
+ * nothing to photograph (`cameraIds` empty), nothing to show (every linked
39602
+ * camera muted), the event will be dropped as muted (the SENSOR is muted,
39603
+ * or there were row cameras and every one is muted — see the evaluation's
39604
+ * mute gate), or no enabled device-event rule that could fire on this
39605
+ * sensor KIND wants a picture (the gate is the RULE's media policy, scope
39606
+ * and kind conditions, read from the in-memory mirror — D49, as
39607
+ * `audioStillOwner`; an occupancy rule, which rides the same delivery,
39608
+ * never fires on a sensor). One linked camera is enough: link alone owns
39609
+ * notification media; tracked rows are a separate timeline question. Coarse
39610
+ * on purpose, in one direction only: a capture for a rule whose other
39611
+ * conditions then fail is one wasted snapshot, a missing capture is a
39612
+ * notification without its picture. The kind check is what keeps a chatty
39613
+ * multi-linked motion sensor from composing a mosaic on every edge for a
39614
+ * doorbell rule.
39348
39615
  *
39349
39616
  * A MUTED camera (D62) is never dialled: its tile is left out, said on that
39350
39617
  * camera (once per window), and the primary is the first linked camera that
@@ -39357,7 +39624,7 @@ var NotificationCenter = class {
39357
39624
  beginLinkedStills(request) {
39358
39625
  const { sensorId, sensorKind, cameraIds, atMs, rowCameraIds, sensorRaised } = request;
39359
39626
  const shelf = this.linkedStills;
39360
- if (shelf === null || !this.evaluationActive || cameraIds.length < 2) return void 0;
39627
+ if (shelf === null || !this.evaluationActive || cameraIds.length === 0) return void 0;
39361
39628
  if (this.deviceMutes.isMuted(sensorId)) return void 0;
39362
39629
  const rowShown = this.unmutedCameras(rowCameraIds);
39363
39630
  if (rowCameraIds.length > 0 && rowShown.length === 0) return void 0;
@@ -40656,10 +40923,8 @@ var NotificationCenter = class {
40656
40923
  * On the SAME serialized chain as every other evaluation, because both of
40657
40924
  * those follow-ups read what the evaluation just enqueued:
40658
40925
  *
40659
- * - EVERY alarm transition is also announced to the panel's own
40660
- * `announceTargets`, which no rule addresses (D630) — but only where a
40661
- * rule has not already delivered it, or a target that is both would get
40662
- * the same sentence twice;
40926
+ * - Panel `announceTargets` are NOT addressed (retired 2026-10-03): only
40927
+ * alarm-family rules deliver. The announce enqueue is a no-op.
40663
40928
  * - a TRIGGERED with an attribution supersedes the triggering rule's own
40664
40929
  * still-pending rows, for exactly the targets the alarm reached. That is
40665
40930
  * the combined notification: one row carrying the alarm's words and that
@@ -40692,11 +40957,11 @@ var NotificationCenter = class {
40692
40957
  });
40693
40958
  }
40694
40959
  /**
40695
- * The sentence an alarm transition is said with, and the targets it goes to
40696
- * — for EVERY alarm kind, always the panel's `announceTargets` (D630: the
40697
- * `announceArm` switch is gone; rules may add more recipients, never fewer).
40698
- *
40699
- * `text` is a composed body only where the words depend on the rule set:
40960
+ * The composed body for an alarm transition (when the words depend on the
40961
+ * rule set). Delivery targets are ALWAYS empty here: the panel no longer
40962
+ * fans out to `announceTargets` — an alarm-family rule is the only notifier
40963
+ * (operator ruling 2026-10-03). `announceTargets` remains on the settings
40964
+ * blob for schema compatibility and is ignored at send time.
40700
40965
  *
40701
40966
  * - ARM and ARMING compose from the SAME coverage — "what does this mode
40702
40967
  * actually watch" — because they are the same answer given thirty seconds
@@ -40707,14 +40972,9 @@ var NotificationCenter = class {
40707
40972
  * Every other kind is `null`: the intake picks its catalog body and params.
40708
40973
  */
40709
40974
  alarmAnnouncement(panel, transition) {
40710
- const targetIds = panel.settings().announceTargets;
40711
- if (targetIds.length === 0) this.logger.info("alarm transition has no announcement target", {
40712
- tags: { deviceId: panel.deviceId },
40713
- meta: { kind: transition.kind }
40714
- });
40715
40975
  return {
40716
40976
  text: this.alarmAnnouncementText(panel, transition),
40717
- targetIds
40977
+ targetIds: []
40718
40978
  };
40719
40979
  }
40720
40980
  /** The composed body of {@link alarmAnnouncement}, or null for a catalog body. */
@@ -41721,14 +41981,19 @@ var NotificationCenter = class {
41721
41981
  }
41722
41982
  sequenceActionsOf = (sequenceId) => this.sequences.get(sequenceId)?.actions;
41723
41983
  /**
41724
- * Run this rule's onTrigger / onTriggerOff bindings for this match.
41984
+ * Run this rule's onTrigger / onTriggerOff bindings for this match, and —
41985
+ * when the rule is gated on `armed_*` — auto-trigger the CamStack
41986
+ * alarm-panel (never sirens).
41725
41987
  *
41726
41988
  * Called BEFORE the cooldown gate — see the call site for why. Everything
41727
41989
  * here is fire-and-forget and nothing may throw into the evaluation.
41728
41990
  */
41729
41991
  runRuleActions(rule, subject, kind, controls) {
41730
- if (this.actionRunner === null || controls.runActions === false) return;
41731
- const bindings = this.isTriggerOffSubject(subject) ? rule.actions?.onTriggerOff : rule.actions?.onTrigger;
41992
+ if (controls.runActions === false) return;
41993
+ const triggerOff = this.isTriggerOffSubject(subject);
41994
+ if (!triggerOff) this.autoTriggerAlarmPanel(rule, subject, kind);
41995
+ if (this.actionRunner === null) return;
41996
+ const bindings = triggerOff ? rule.actions?.onTriggerOff : rule.actions?.onTrigger;
41732
41997
  if (bindings === void 0 || bindings.length === 0) return;
41733
41998
  this.actionRunner.runBindings({
41734
41999
  ruleId: rule.id,
@@ -41740,6 +42005,41 @@ var NotificationCenter = class {
41740
42005
  });
41741
42006
  }
41742
42007
  /**
42008
+ * A rule gated on `armed_*` fires the CamStack panel on match — no
42009
+ * `onTrigger` sequence required. Sirens are never actuated here.
42010
+ */
42011
+ autoTriggerAlarmPanel(rule, subject, kind) {
42012
+ if (!require_clip_model_registry.ruleAutoTriggersAlarm(rule)) return;
42013
+ const panel = this.alarmPanel;
42014
+ if (panel === null) {
42015
+ this.logger.warn("alarm rule matched but no alarm panel is wired — panel not triggered", {
42016
+ tags: { deviceId: subject.deviceId },
42017
+ meta: {
42018
+ ruleId: rule.id,
42019
+ rule: rule.name
42020
+ }
42021
+ });
42022
+ return;
42023
+ }
42024
+ this.actuateWithAttribution({
42025
+ deviceId: panel.deviceId,
42026
+ cap: ALARM_PANEL_CAP,
42027
+ method: "trigger",
42028
+ args: {},
42029
+ origin: fireRefFor(rule, subject, kind)
42030
+ }).catch((err) => {
42031
+ this.logger.warn("alarm panel auto-trigger failed", {
42032
+ tags: { deviceId: subject.deviceId },
42033
+ meta: {
42034
+ ruleId: rule.id,
42035
+ rule: rule.name,
42036
+ panelId: panel.deviceId,
42037
+ error: err instanceof Error ? err.message : String(err)
42038
+ }
42039
+ });
42040
+ });
42041
+ }
42042
+ /**
41743
42043
  * The muted device that drops this subject, or `undefined` when none does.
41744
42044
  * A per-camera subject: its camera. A linked set: the sensor; else, when
41745
42045
  * the change took rows and every row camera is muted, the sensor too — the
@@ -104137,8 +104437,22 @@ var NO_PUBLICATION = {
104137
104437
  url: null,
104138
104438
  artifactId: null
104139
104439
  };
104140
- function isCapRecord(value) {
104141
- return typeof value === "object" && value !== null;
104440
+ /**
104441
+ * A tRPC-shaped router or procedure reached by DYNAMIC name off `ctx.api`.
104442
+ *
104443
+ * A rule names its cap and method as strings, so the generated client cannot be
104444
+ * indexed statically. The walk down `api → router → procedure → mutate|query`
104445
+ * is therefore guarded hop by hop rather than asserted: every hop that is not
104446
+ * what it must be raises the named error the rule author needs to see.
104447
+ *
104448
+ * Forked-addon `ctx.api` namespaces are tRPC v11 Proxies over a noop function
104449
+ * (`typeof === 'function'`). Treating only plain objects as routers made every
104450
+ * sequence step fail with `no router for capability "alarm-panel"` while the
104451
+ * same method answered fine over HTTP — the panel never left `armed_*`, so
104452
+ * HomeKit stayed quiet.
104453
+ */
104454
+ function isCapNode(value) {
104455
+ return typeof value === "object" && value !== null || typeof value === "function";
104142
104456
  }
104143
104457
  function isCapCall(value) {
104144
104458
  return typeof value === "function";
@@ -107257,10 +107571,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
107257
107571
  },
107258
107572
  actuate: async ({ deviceId, cap, method, args }) => {
107259
107573
  const router = Reflect.get(api, require_clip_model_registry.kebabToCamel(cap));
107260
- if (!isCapRecord(router)) throw new Error(`rule action: no router for capability "${cap}"`);
107261
- const proc = router[method];
107262
- const fn = isCapRecord(proc) ? proc["mutate"] ?? proc["query"] : void 0;
107263
- if (!isCapCall(fn)) throw new Error(`rule action: "${cap}.${method}" is not a callable cap method`);
107574
+ if (!isCapNode(router)) throw new Error(`rule action: no router for capability "${cap}"`);
107575
+ const proc = Reflect.get(router, method);
107576
+ const mutate = isCapNode(proc) ? Reflect.get(proc, "mutate") : void 0;
107577
+ const query = isCapNode(proc) ? Reflect.get(proc, "query") : void 0;
107578
+ const fn = isCapCall(mutate) ? mutate : isCapCall(query) ? query : void 0;
107579
+ if (fn === void 0) throw new Error(`rule action: "${cap}.${method}" is not a callable cap method`);
107264
107580
  await fn({
107265
107581
  deviceId,
107266
107582
  ...args
@@ -113513,7 +113829,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
113513
113829
  });
113514
113830
  return NO_SENSOR_NOTIFICATION_CAMERAS;
113515
113831
  });
113516
- const linkedSet = cameras.mediaCameraIds.length > 1;
113832
+ const linkedSet = cameras.mediaCameraIds.length > 1 || cameras.mediaCameraIds.length === 1 && cameras.rowCameraIds.length === 0;
113517
113833
  const linkedStills = linkedSet ? this.startLinkedStills(data, timestamp, cameras) : void 0;
113518
113834
  const persisted = [];
113519
113835
  const unowned = [];
@@ -113693,14 +114009,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
113693
114009
  * which verdict swallowed it.
113694
114010
  */
113695
114011
  flushSensorTransitionDrops() {
113696
- for (const drop of this.sensorTransitions.takeDropSummary(Date.now())) this.ctx.logger.info("sensor state changes dropped — no transition", {
113697
- tags: { deviceId: drop.deviceId },
113698
- meta: {
113699
- capName: drop.capName,
113700
- reason: drop.reason,
113701
- count: drop.count
113702
- }
113703
- });
114012
+ for (const drop of this.sensorTransitions.takeDropSummary(Date.now())) {
114013
+ const level = drop.reason === "baseline-seeded-inactive" || drop.reason === "no-change" ? "debug" : "info";
114014
+ this.ctx.logger[level]("sensor state changes dropped — no transition", {
114015
+ tags: { deviceId: drop.deviceId },
114016
+ meta: {
114017
+ capName: drop.capName,
114018
+ reason: drop.reason,
114019
+ count: drop.count
114020
+ }
114021
+ });
114022
+ }
113704
114023
  }
113705
114024
  /**
113706
114025
  * One `info` line per (device, cap) per window for the evaluations refused