@camstack/addon-post-analysis 1.2.310 → 1.2.312

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 (14) hide show
  1. package/dist/{clip-model-registry-xYrJkXdS.js → clip-model-registry-CRkjjUyj.js} +257 -18
  2. package/dist/{clip-model-registry-BROqj9pS.mjs → clip-model-registry-ivwvxxex.mjs} +210 -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-KF5Fk8sD.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BocOElW2.mjs} +2 -2
  7. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Ds6vT2FJ.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-B8qeS8Da.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B_KS96RA.mjs} +1 -1
  9. package/dist/pipeline-analytics/{hostInit-B3xcTHUM.mjs → hostInit-CRloCh1h.mjs} +2 -2
  10. package/dist/pipeline-analytics/index.js +413 -31
  11. package/dist/pipeline-analytics/index.mjs +413 -31
  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-DytyGNZ-.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-xYrJkXdS.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-CRkjjUyj.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");
@@ -1396,11 +1396,85 @@ var NcAlarmExclusions = class {
1396
1396
  }
1397
1397
  };
1398
1398
  //#endregion
1399
+ //#region src/notification-center/alarm/alarm-mode-delays.ts
1400
+ /**
1401
+ * The persisted per-mode delay overrides, read through a guard (D697).
1402
+ *
1403
+ * The panel's config schema keeps `mode` a plain string so a blob written by a later release
1404
+ * with a mode this one does not know cannot fail the whole config. This is where that string is
1405
+ * narrowed: an entry that cannot be trusted is dropped and REPORTED, never repaired. An entry of an unknown
1406
+ * mode is also handed back raw (`foreign`) so a write can keep it.
1407
+ */
1408
+ function inBounds(value, max) {
1409
+ return Number.isInteger(value) && value >= 0 && value <= max;
1410
+ }
1411
+ /** Read the persisted overrides through a guard, never a cast (like `nonBlockingFromPersisted`). */
1412
+ function modeDelaysFromPersisted(raw) {
1413
+ const seen = /* @__PURE__ */ new Set();
1414
+ const entries = [];
1415
+ const dropped = [];
1416
+ const foreign = [];
1417
+ for (const item of raw) {
1418
+ const mode = require_clip_model_registry.AlarmArmModeSchema.safeParse(item.mode);
1419
+ if (!mode.success) {
1420
+ dropped.push({
1421
+ mode: item.mode,
1422
+ why: "unknown-mode"
1423
+ });
1424
+ foreign.push(item);
1425
+ continue;
1426
+ }
1427
+ if (seen.has(mode.data)) {
1428
+ dropped.push({
1429
+ mode: item.mode,
1430
+ why: "duplicate-mode"
1431
+ });
1432
+ continue;
1433
+ }
1434
+ seen.add(mode.data);
1435
+ const kept = { mode: mode.data };
1436
+ const fields = [
1437
+ {
1438
+ field: "exitDelaySec",
1439
+ max: require_clip_model_registry.NC_ALARM_DELAY_MAX_SEC
1440
+ },
1441
+ {
1442
+ field: "entryDelaySec",
1443
+ max: require_clip_model_registry.NC_ALARM_DELAY_MAX_SEC
1444
+ },
1445
+ {
1446
+ field: "triggeredDurationSec",
1447
+ max: require_clip_model_registry.NC_ALARM_TRIGGERED_MAX_SEC
1448
+ }
1449
+ ];
1450
+ for (const { field, max } of fields) {
1451
+ const value = item[field];
1452
+ if (value === void 0) continue;
1453
+ if (!inBounds(value, max)) {
1454
+ dropped.push({
1455
+ mode: item.mode,
1456
+ why: "out-of-bounds",
1457
+ field
1458
+ });
1459
+ continue;
1460
+ }
1461
+ kept[field] = value;
1462
+ }
1463
+ if (kept.exitDelaySec !== void 0 || kept.entryDelaySec !== void 0 || kept.triggeredDurationSec !== void 0) entries.push(kept);
1464
+ }
1465
+ return {
1466
+ entries,
1467
+ dropped,
1468
+ foreign
1469
+ };
1470
+ }
1471
+ //#endregion
1399
1472
  //#region src/notification-center/alarm/alarm-state-machine.ts
1400
1473
  /**
1401
- * Defaults an operator can change per panel. `home` has no exit delay on
1402
- * purpose — nobody is leaving — but the shared value is applied to every mode
1403
- * until per-mode delays are asked for.
1474
+ * The GLOBAL defaults an operator can change per panel — what a mode inherits
1475
+ * when it overrides nothing. A mode can override each of the three since D697
1476
+ * (`resolveModeDelays`): `home` has no exit delay on purpose — nobody is
1477
+ * leaving — and says so with its own entry, not by this shared value.
1404
1478
  *
1405
1479
  * `triggeredDurationSec: 0` = "sound until somebody disarms it". That is what
1406
1480
  * the panel did before the field existed, so an install that never opens the
@@ -1815,6 +1889,19 @@ var ncAlarmPanelSchema = require_clip_model_registry.object({
1815
1889
  nonBlocking: require_clip_model_registry.array(require_clip_model_registry.object({
1816
1890
  mode: require_clip_model_registry.string(),
1817
1891
  deviceIds: require_clip_model_registry.array(require_clip_model_registry.number().int())
1892
+ })).default([]),
1893
+ /**
1894
+ * Per arm mode, the delays that override the global ones (D697). Loose on purpose — `mode` is
1895
+ * a string and the numbers are unbounded here — and read back through
1896
+ * {@link modeDelaysFromPersisted}, never a cast: an entry this release cannot trust is dropped
1897
+ * and logged at boot, never the whole config. A blob written before D697 has no key and
1898
+ * parses to no overrides.
1899
+ */
1900
+ modeDelays: require_clip_model_registry.array(require_clip_model_registry.object({
1901
+ mode: require_clip_model_registry.string(),
1902
+ exitDelaySec: require_clip_model_registry.number().optional(),
1903
+ entryDelaySec: require_clip_model_registry.number().optional(),
1904
+ triggeredDurationSec: require_clip_model_registry.number().optional()
1818
1905
  })).default([])
1819
1906
  });
1820
1907
  /**
@@ -1885,7 +1972,6 @@ function machineFromPersisted(raw) {
1885
1972
  var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
1886
1973
  features = [];
1887
1974
  machine = DISARMED;
1888
- delays = DEFAULT_DELAYS;
1889
1975
  ticker = null;
1890
1976
  onPublish = null;
1891
1977
  /**
@@ -1916,7 +2002,7 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
1916
2002
  refusalSeq = 0;
1917
2003
  constructor(ctx) {
1918
2004
  super(ctx, ncAlarmPanelSchema, { type: require_clip_model_registry.DeviceType.AlarmPanel });
1919
- this.delays = this.readDelays();
2005
+ this.logDroppedModeDelays();
1920
2006
  this.restore();
1921
2007
  this.registerAlarmCap();
1922
2008
  this.publish();
@@ -1928,30 +2014,148 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
1928
2014
  ...this.readDelays(),
1929
2015
  announceArm: this.config.get("announceArm"),
1930
2016
  announceTargets: [...this.config.get("announceTargets")],
1931
- nonBlocking: nonBlockingFromPersisted(this.config.get("nonBlocking"))
2017
+ nonBlocking: nonBlockingFromPersisted(this.config.get("nonBlocking")),
2018
+ modeDelays: modeDelaysFromPersisted(this.config.get("modeDelays")).entries
1932
2019
  };
1933
2020
  }
1934
2021
  /**
1935
2022
  * Apply an editor's patch and return what the panel settled on.
1936
2023
  *
1937
2024
  * The new delays take effect on the NEXT transition, never retroactively: a
1938
- * machine already counting down keeps the instants it was given. Re-deriving
1939
- * `armedAt` from a delay the operator changed mid-exit would move the arm
1940
- * instant under somebody who is walking out of the door.
2025
+ * machine already counting down keeps the instants it was given (the exit
2026
+ * delay is read at arm, the entry delay and siren duration at trigger, and
2027
+ * the machine stores the instants, not the durations). Re-deriving `armedAt`
2028
+ * from a delay the operator changed mid-exit would move the arm instant under
2029
+ * somebody who is walking out of the door.
2030
+ *
2031
+ * Two phases, so a refused patch writes NOTHING:
2032
+ * 1. every refusal is checked first (a mode this panel does not offer, an
2033
+ * add to a full non-blocking list) — it logs why and throws;
2034
+ * 2. every LIST write is CALLED before the first `await`. `DeviceConfig.set`
2035
+ * replaces its in-memory value synchronously, so a list computed after an
2036
+ * await could be based on a list another call has since replaced — and a
2037
+ * whole-list write (the deleted-device prune sends one, computed
2038
+ * synchronously before calling: `device-prune-runner.ts` `applyAlarm`)
2039
+ * would erase the membership edit that landed in between.
1941
2040
  */
1942
2041
  async applySettings(patch) {
2042
+ const edits = patch.modeDelays ?? [];
2043
+ const membership = patch.nonBlockingEdit;
2044
+ for (const edit of edits) this.refuseUnofferedMode(edit.mode);
2045
+ if (membership !== void 0) {
2046
+ this.refuseUnofferedMode(membership.mode, membership.deviceId);
2047
+ const verdict = require_clip_model_registry.applyNonBlockingEdit(patch.nonBlocking !== void 0 ? patch.nonBlocking : nonBlockingFromPersisted(this.config.get("nonBlocking")), membership);
2048
+ if (verdict.kind === "refused") {
2049
+ this.ctx.logger.info("alarm settings refused", {
2050
+ tags: { deviceId: membership.deviceId },
2051
+ meta: {
2052
+ panelId: this.id,
2053
+ reason: "list-full",
2054
+ mode: verdict.mode
2055
+ }
2056
+ });
2057
+ throw new Error(`the ${verdict.mode} non-blocking list already holds ${verdict.max} devices — remove one first`);
2058
+ }
2059
+ }
2060
+ const listWrites = [];
2061
+ if (patch.nonBlocking !== void 0) listWrites.push(this.config.set("nonBlocking", patch.nonBlocking.map((l) => ({
2062
+ mode: l.mode,
2063
+ deviceIds: [...l.deviceIds]
2064
+ }))));
2065
+ let membershipChanged = null;
2066
+ if (membership !== void 0) {
2067
+ const result = require_clip_model_registry.applyNonBlockingEdit(nonBlockingFromPersisted(this.config.get("nonBlocking")), membership);
2068
+ if (result.kind === "applied") {
2069
+ membershipChanged = result.changed;
2070
+ if (result.changed) listWrites.push(this.config.set("nonBlocking", result.lists.map((l) => ({
2071
+ mode: l.mode,
2072
+ deviceIds: [...l.deviceIds]
2073
+ }))));
2074
+ }
2075
+ }
2076
+ const persistedDelays = modeDelaysFromPersisted(this.config.get("modeDelays"));
2077
+ const beforeDelays = persistedDelays.entries;
2078
+ const afterDelays = edits.length > 0 ? require_clip_model_registry.applyModeDelaysEdits(beforeDelays, edits) : beforeDelays;
2079
+ if (edits.length > 0) {
2080
+ for (const drop of persistedDelays.dropped) {
2081
+ if (drop.why === "unknown-mode") continue;
2082
+ this.ctx.logger.info("alarm per-mode delay discarded on write", {
2083
+ tags: { deviceId: this.id },
2084
+ meta: {
2085
+ mode: drop.mode,
2086
+ why: drop.why,
2087
+ field: drop.field
2088
+ }
2089
+ });
2090
+ }
2091
+ listWrites.push(this.config.set("modeDelays", [...afterDelays, ...persistedDelays.foreign]));
2092
+ }
2093
+ await Promise.all(listWrites);
2094
+ this.logModeDelayChanges(beforeDelays, afterDelays, edits);
1943
2095
  if (patch.exitDelaySec !== void 0) await this.config.set("exitDelaySec", patch.exitDelaySec);
1944
2096
  if (patch.entryDelaySec !== void 0) await this.config.set("entryDelaySec", patch.entryDelaySec);
1945
2097
  if (patch.triggeredDurationSec !== void 0) await this.config.set("triggeredDurationSec", patch.triggeredDurationSec);
1946
2098
  if (patch.announceArm !== void 0) await this.config.set("announceArm", patch.announceArm);
1947
2099
  if (patch.announceTargets !== void 0) await this.config.set("announceTargets", [...patch.announceTargets]);
1948
- if (patch.nonBlocking !== void 0) await this.config.set("nonBlocking", patch.nonBlocking.map((l) => ({
1949
- mode: l.mode,
1950
- deviceIds: [...l.deviceIds]
1951
- })));
1952
- this.delays = this.readDelays();
2100
+ if (membership !== void 0 && membershipChanged !== null) this.ctx.logger.info("alarm non-blocking list changed", {
2101
+ tags: { deviceId: membership.deviceId },
2102
+ meta: {
2103
+ panelId: this.id,
2104
+ mode: membership.mode,
2105
+ nonBlocking: membership.nonBlocking,
2106
+ changed: membershipChanged
2107
+ }
2108
+ });
1953
2109
  return this.settings();
1954
2110
  }
2111
+ /**
2112
+ * Refuse a mode this panel does not offer — logged by reason, then thrown. A membership edit
2113
+ * passes its SENSOR as `deviceId` (the tag); a per-mode delay edit defaults to the panel.
2114
+ */
2115
+ refuseUnofferedMode(mode, deviceId = this.id) {
2116
+ if (NC_ALARM_MODES.includes(mode)) return;
2117
+ this.ctx.logger.info("alarm settings refused", {
2118
+ tags: { deviceId },
2119
+ meta: {
2120
+ panelId: this.id,
2121
+ reason: "mode-not-offered",
2122
+ mode
2123
+ }
2124
+ });
2125
+ throw new Error(`alarm mode '${mode}' is not offered by this panel`);
2126
+ }
2127
+ /** One INFO line per mode-delay field an edit really changed (`null` = inherit). */
2128
+ logModeDelayChanges(before, after, edits) {
2129
+ for (const edit of edits) {
2130
+ const was = before.find((e) => e.mode === edit.mode);
2131
+ const now = after.find((e) => e.mode === edit.mode);
2132
+ for (const field of require_clip_model_registry.NC_ALARM_DELAY_FIELDS) {
2133
+ const from = was?.[field] ?? null;
2134
+ const to = now?.[field] ?? null;
2135
+ if (from === to) continue;
2136
+ this.ctx.logger.info("alarm mode delays changed", {
2137
+ tags: { deviceId: this.id },
2138
+ meta: {
2139
+ mode: edit.mode,
2140
+ field,
2141
+ from,
2142
+ to
2143
+ }
2144
+ });
2145
+ }
2146
+ }
2147
+ }
2148
+ /** One INFO line per persisted per-mode delay entry the guard refused to trust. */
2149
+ logDroppedModeDelays() {
2150
+ for (const drop of modeDelaysFromPersisted(this.config.get("modeDelays")).dropped) this.ctx.logger.info("alarm per-mode delay dropped at boot", {
2151
+ tags: { deviceId: this.id },
2152
+ meta: {
2153
+ mode: drop.mode,
2154
+ why: drop.why,
2155
+ field: drop.field
2156
+ }
2157
+ });
2158
+ }
1955
2159
  /** Wire the state mirror and the transition notifications. See
1956
2160
  * {@link NcAlarmPublishHook}. */
1957
2161
  setPublishHook(hook) {
@@ -2234,7 +2438,17 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2234
2438
  else this.exclusions.clear();
2235
2439
  this.persist();
2236
2440
  }
2237
- this.machine = arm(mode, this.delays, now);
2441
+ const resolved = this.delaysFor(mode);
2442
+ this.machine = arm(mode, resolved, now);
2443
+ this.ctx.logger.info("alarm delays applied", {
2444
+ tags: { deviceId: this.id },
2445
+ meta: {
2446
+ mode,
2447
+ phase: "arm",
2448
+ exitDelaySec: resolved.exitDelaySec,
2449
+ source: resolved.source.exitDelaySec
2450
+ }
2451
+ });
2238
2452
  this.attribution.clear();
2239
2453
  this.publish(now);
2240
2454
  for (const { device, why } of exclusions ?? []) this.ctx.logger.info("alarm armed with an open device excluded", {
@@ -2334,7 +2548,9 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2334
2548
  this.announceIgnoredTrigger(source, now);
2335
2549
  return;
2336
2550
  }
2337
- const next = trigger(this.machine, this.delays, now);
2551
+ const armed = armModeOf(this.machine.target, NC_ALARM_MODES);
2552
+ const resolved = this.delaysFor(armed);
2553
+ const next = trigger(this.machine, resolved, now);
2338
2554
  if (next === this.machine) {
2339
2555
  const state = stateAt(this.machine, now);
2340
2556
  this.ctx.logger.info(ignoredTriggerMessage(state), {
@@ -2352,6 +2568,19 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2352
2568
  }
2353
2569
  });
2354
2570
  this.machine = next;
2571
+ this.ctx.logger.info("alarm delays applied", {
2572
+ tags: { deviceId: this.id },
2573
+ meta: {
2574
+ mode: armed,
2575
+ phase: "trigger",
2576
+ entryDelaySec: resolved.entryDelaySec,
2577
+ triggeredDurationSec: resolved.triggeredDurationSec,
2578
+ source: {
2579
+ entry: resolved.source.entryDelaySec,
2580
+ siren: resolved.source.triggeredDurationSec
2581
+ }
2582
+ }
2583
+ });
2355
2584
  this.publish(now);
2356
2585
  }
2357
2586
  });
@@ -2487,7 +2716,32 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2487
2716
  });
2488
2717
  }
2489
2718
  }
2490
- /** The three durations, read from the persisted config. */
2719
+ /** The global durations plus the per-mode overrides, read from the persisted config NOW. */
2720
+ delaySettings() {
2721
+ return {
2722
+ ...this.readDelays(),
2723
+ modeDelays: modeDelaysFromPersisted(this.config.get("modeDelays")).entries
2724
+ };
2725
+ }
2726
+ /**
2727
+ * What `mode` runs on, read at the transition that uses it — never cached. `null` (no armed
2728
+ * mode to look at) is the global values, every field `default`.
2729
+ */
2730
+ delaysFor(mode) {
2731
+ const settings = this.delaySettings();
2732
+ if (mode === null) return {
2733
+ exitDelaySec: settings.exitDelaySec,
2734
+ entryDelaySec: settings.entryDelaySec,
2735
+ triggeredDurationSec: settings.triggeredDurationSec,
2736
+ source: {
2737
+ exitDelaySec: "default",
2738
+ entryDelaySec: "default",
2739
+ triggeredDurationSec: "default"
2740
+ }
2741
+ };
2742
+ return require_clip_model_registry.resolveModeDelays(settings, mode);
2743
+ }
2744
+ /** The three global durations, read from the persisted config. */
2491
2745
  readDelays() {
2492
2746
  return {
2493
2747
  exitDelaySec: this.config.get("exitDelaySec"),
@@ -8353,6 +8607,79 @@ function sameWord(a, b) {
8353
8607
  return a !== void 0 && a.trim().toLowerCase() === b.trim().toLowerCase();
8354
8608
  }
8355
8609
  //#endregion
8610
+ //#region src/notification-center/alarm/alarm-sensors.ts
8611
+ /**
8612
+ * One id's state as the table shows it — spec §3.4's hub table, nothing else.
8613
+ *
8614
+ * Built ON `classifyCoveredOpening`, the arm check's classifier: what is
8615
+ * unknown, open or skipped is decided there, and this only splits a skip into
8616
+ * `closed` / `not-opening` for the table. A later change to the classifier
8617
+ * therefore moves the table with the arm.
8618
+ */
8619
+ function alarmSensorStateOf(deviceId, opening, online) {
8620
+ const verdict = classifyCoveredOpening(opening);
8621
+ switch (verdict.kind) {
8622
+ case "unknown": return {
8623
+ deviceId,
8624
+ reading: "unknown",
8625
+ state: null,
8626
+ unknownWhy: verdict.why,
8627
+ online
8628
+ };
8629
+ case "open": return {
8630
+ deviceId,
8631
+ reading: "open",
8632
+ state: verdict.state,
8633
+ unknownWhy: null,
8634
+ online
8635
+ };
8636
+ case "skip": return opening?.kind === "reading" ? {
8637
+ deviceId,
8638
+ reading: "closed",
8639
+ state: opening.state,
8640
+ unknownWhy: null,
8641
+ online
8642
+ } : {
8643
+ deviceId,
8644
+ reading: "not-opening",
8645
+ state: null,
8646
+ unknownWhy: null,
8647
+ online
8648
+ };
8649
+ }
8650
+ }
8651
+ /** The asked ids, de-duplicated and ascending. */
8652
+ function alarmSensorReadIds(ids) {
8653
+ return [...new Set(ids)].toSorted((a, b) => a - b);
8654
+ }
8655
+ /**
8656
+ * The sensors read's health, logged on CHANGE only (the call is polled every
8657
+ * 5-15 s by every open tab). "Wholesale failure" = ids were asked and not one
8658
+ * was answered (the device manager is unreachable): WARN once when it starts,
8659
+ * ONE INFO when answers return. A partial answer is not a health change — those
8660
+ * ids already leave their own debug line and show as `unknown`.
8661
+ */
8662
+ var NcAlarmSensorsReadLog = class {
8663
+ failing = false;
8664
+ next(asked, answered) {
8665
+ const wholesale = asked > 0 && answered === 0;
8666
+ if (wholesale === this.failing) return null;
8667
+ this.failing = wholesale;
8668
+ return wholesale ? {
8669
+ level: "warn",
8670
+ message: "alarm sensors read: no asked device answered — every row is unknown",
8671
+ meta: { asked }
8672
+ } : {
8673
+ level: "info",
8674
+ message: "alarm sensors read: devices answer again",
8675
+ meta: {
8676
+ asked,
8677
+ answered
8678
+ }
8679
+ };
8680
+ }
8681
+ };
8682
+ //#endregion
8356
8683
  //#region src/notification-center/alarm/alarm-live.ts
8357
8684
  /** Every covered id across all modes, ascending, once — what the one read asks for. */
8358
8685
  function alarmLiveReadIds(covered) {
@@ -11337,6 +11664,18 @@ function streamHealthAccepted(category, reason) {
11337
11664
  function discardedSystemEvent(event, lookup) {
11338
11665
  const accessory = discardedAccessoryLiveness(event, lookup);
11339
11666
  if (accessory !== null) return accessory;
11667
+ if (event.category === "worker.online" || event.category === "worker.offline") {
11668
+ const workerId = stringField(event.data, "workerId");
11669
+ return {
11670
+ category: event.category,
11671
+ reason: "a runner is not a node",
11672
+ tags: {},
11673
+ meta: {
11674
+ reason: "a runner is not a node",
11675
+ ...workerId !== void 0 ? { workerId } : {}
11676
+ }
11677
+ };
11678
+ }
11340
11679
  if (event.category !== "stream.online" && event.category !== "stream.offline") return null;
11341
11680
  const data = event.data;
11342
11681
  const reason = stringField(data, "reason");
@@ -11663,12 +12002,11 @@ function incomingFromSystemEvent(event, lookup) {
11663
12002
  deviceId
11664
12003
  });
11665
12004
  }
11666
- case "agent.online":
11667
- case "agent.offline":
11668
12005
  case "worker.online":
11669
- case "worker.offline": {
11670
- const agent = event.category.startsWith("agent.");
11671
- const nodeId = stringField(data, agent ? "agentId" : "workerId");
12006
+ case "worker.offline": return null;
12007
+ case "agent.online":
12008
+ case "agent.offline": {
12009
+ const nodeId = stringField(data, "agentId");
11672
12010
  if (nodeId === void 0) return null;
11673
12011
  const kind = event.category.endsWith(".online") ? "node-online" : "node-offline";
11674
12012
  return systemSubject({
@@ -11676,7 +12014,7 @@ function incomingFromSystemEvent(event, lookup) {
11676
12014
  kind,
11677
12015
  subject: `node:${nodeId}`,
11678
12016
  titleKey: `system.${kind}.title`,
11679
- bodyKey: agent ? `system.${kind}.body.agent` : `system.${kind}.body.worker`,
12017
+ bodyKey: `system.${kind}.body.agent`,
11680
12018
  textParams: { nodeId },
11681
12019
  nodeId
11682
12020
  });
@@ -13293,10 +13631,8 @@ var en_default = {
13293
13631
  "system.stream-offline.body.profiled": "{{profile}} stream for camera {{deviceName}} is offline",
13294
13632
  "system.node-online.title": "Node online",
13295
13633
  "system.node-online.body.agent": "Agent {{nodeId}} is online",
13296
- "system.node-online.body.worker": "Worker {{nodeId}} is online",
13297
13634
  "system.node-offline.title": "Node offline",
13298
13635
  "system.node-offline.body.agent": "Agent {{nodeId}} is offline",
13299
- "system.node-offline.body.worker": "Worker {{nodeId}} is offline",
13300
13636
  "system.node-inference-unavailable.title": "Node has no usable inference device",
13301
13637
  "system.node-inference-unavailable.body": "Node {{nodeId}} can no longer run inference — every enabled device is unavailable ({{devices}}). It will not receive cameras until one returns.",
13302
13638
  "system.addon-crash-loop.title": "Addon stopped after repeated crashes",
@@ -13623,10 +13959,8 @@ var it_default = {
13623
13959
  "system.stream-offline.body.profiled": "Lo stream {{profile}} della camera {{deviceName}} è offline",
13624
13960
  "system.node-online.title": "Nodo online",
13625
13961
  "system.node-online.body.agent": "L'agent {{nodeId}} è online",
13626
- "system.node-online.body.worker": "Il worker {{nodeId}} è online",
13627
13962
  "system.node-offline.title": "Nodo offline",
13628
13963
  "system.node-offline.body.agent": "L'agent {{nodeId}} è offline",
13629
- "system.node-offline.body.worker": "Il worker {{nodeId}} è offline",
13630
13964
  "system.node-inference-unavailable.title": "Nodo senza dispositivi di inferenza utilizzabili",
13631
13965
  "system.node-inference-unavailable.body": "Il nodo {{nodeId}} non può più eseguire inferenza — tutti i dispositivi abilitati non sono disponibili ({{devices}}). Non riceverà camere finché uno non torna disponibile.",
13632
13966
  "system.addon-crash-loop.title": "Addon fermato dopo ripetuti crash",
@@ -17027,7 +17361,8 @@ var ncActions = require_clip_model_registry.defineCustomActions({
17027
17361
  * `getStatus`; this one opens up once it filters to the caller's grants.
17028
17362
  * The config pair is the admin's, as `getAlarmConfig` / `setAlarmConfig` are
17029
17363
  * on the cap: it carries the delays, the recipients and the non-blocking
17030
- * lists.
17364
+ * lists. `getAlarmSensors` is the admin's too: it names which of the asked
17365
+ * devices are open.
17031
17366
  */
17032
17367
  "nc.getAlarmStatus": require_clip_model_registry.customAction(require_clip_model_registry.object({}), NcAlarmStatusSchema, {
17033
17368
  auth: "admin",
@@ -17068,6 +17403,10 @@ var ncActions = require_clip_model_registry.defineCustomActions({
17068
17403
  auth: "admin",
17069
17404
  caller: "required"
17070
17405
  }),
17406
+ "nc.getAlarmSensors": require_clip_model_registry.customAction(require_clip_model_registry.NcAlarmSensorsInputSchema, require_clip_model_registry.NcAlarmSensorsSchema, {
17407
+ auth: "admin",
17408
+ caller: "required"
17409
+ }),
17071
17410
  "nc.setAlarmConfig": require_clip_model_registry.customAction(require_clip_model_registry.object({ patch: require_clip_model_registry.NcAlarmSettingsPatchSchema }), require_clip_model_registry.NcAlarmConfigSchema, {
17072
17411
  kind: "mutation",
17073
17412
  auth: "admin",
@@ -17860,6 +18199,10 @@ function makeNcActionHandlers(deps) {
17860
18199
  requireAdmin(caller);
17861
18200
  return requireAlarm("nc.getAlarmConfig").getConfig();
17862
18201
  },
18202
+ "nc.getAlarmSensors": async (input, caller) => {
18203
+ requireAdmin(caller);
18204
+ return requireAlarm("nc.getAlarmSensors").getSensors(input);
18205
+ },
17863
18206
  "nc.setAlarmConfig": async (input, caller) => {
17864
18207
  requireAdmin(caller);
17865
18208
  return requireAlarm("nc.setAlarmConfig").setConfig(input.patch);
@@ -32694,7 +33037,8 @@ function consumablesSliceOf(event) {
32694
33037
  var NC_ALARM_SETTINGS_FALLBACK = {
32695
33038
  ...DEFAULT_DELAYS,
32696
33039
  announceTargets: [],
32697
- nonBlocking: []
33040
+ nonBlocking: [],
33041
+ modeDelays: []
32698
33042
  };
32699
33043
  /**
32700
33044
  * How often the "matched NO rule" report may fire per device. Long enough that
@@ -33076,6 +33420,7 @@ var NotificationCenter = class NotificationCenter {
33076
33420
  alarmPanel = null;
33077
33421
  /** The live view's read, logged on change only — see `alarm-live-read-log.ts`. */
33078
33422
  alarmLiveReadLog = new NcAlarmLiveReadLog();
33423
+ alarmSensorsReadLog = new NcAlarmSensorsReadLog();
33079
33424
  /**
33080
33425
  * The words the panel answers the `deviceState` gate with — its state, plus
33081
33426
  * the armed mode a `pending`/`triggered` belongs to (D630). Refreshed on
@@ -35394,6 +35739,7 @@ var NotificationCenter = class NotificationCenter {
35394
35739
  return { success: true };
35395
35740
  },
35396
35741
  getAlarmConfig: async () => this.alarmConfig(),
35742
+ getAlarmSensors: async (input) => this.alarmSensors(input),
35397
35743
  setAlarmConfig: async ({ patch }) => this.setAlarmConfig(patch)
35398
35744
  };
35399
35745
  }
@@ -35621,6 +35967,7 @@ var NotificationCenter = class NotificationCenter {
35621
35967
  });
35622
35968
  },
35623
35969
  getConfig: () => this.alarmConfig(),
35970
+ getSensors: (input) => this.alarmSensors(input),
35624
35971
  setConfig: (patch) => this.setAlarmConfig(patch)
35625
35972
  };
35626
35973
  }
@@ -35634,7 +35981,8 @@ var NotificationCenter = class NotificationCenter {
35634
35981
  exitDelaySec: settings.exitDelaySec,
35635
35982
  entryDelaySec: settings.entryDelaySec,
35636
35983
  triggeredDurationSec: settings.triggeredDurationSec,
35637
- announceTargets: settings.announceTargets.length
35984
+ announceTargets: settings.announceTargets.length,
35985
+ modeDelays: settings.modeDelays.length
35638
35986
  }
35639
35987
  });
35640
35988
  return this.alarmConfig();
@@ -35690,6 +36038,40 @@ var NotificationCenter = class NotificationCenter {
35690
36038
  name: this.deviceNameFromDirectory()
35691
36039
  });
35692
36040
  }
36041
+ /**
36042
+ * The opening states of the asked ids, for the alarm editors' sensor table.
36043
+ *
36044
+ * A separate read from `getAlarmConfig` on purpose: that one is polled every
36045
+ * 2–15 s and the household set needs a whole-fleet bindings pass, while the
36046
+ * client already holds the ids. Cost: one `loadRuntimeState` RPC per id, 8 in
36047
+ * flight (`readAlarmOpenings`); the client sends at most `NC_ALARM_SENSORS_MAX`
36048
+ * ids per call and polls every 15 s, every 5 s only while a countdown or the
36049
+ * siren runs. No log line per call (it is polled): one WARN/INFO per change of
36050
+ * wholesale read health (`NcAlarmSensorsReadLog`); a per-id read failure keeps
36051
+ * `readAlarmOpenings`' own debug line, and shows as `unknown` (D49).
36052
+ *
36053
+ * An id with no persisted device (removed) or whose provider never wrote an
36054
+ * opening slice reads `not-opening`, exactly as the arm check sees it — NOT
36055
+ * "verified not an opening". The client shows "Removed" from its fleet read.
36056
+ */
36057
+ async alarmSensors(input) {
36058
+ const read = this.deps.readDeviceOpenings;
36059
+ if (read === void 0) throw new Error("the opening reader is not wired on this node");
36060
+ const ids = alarmSensorReadIds(input.deviceIds);
36061
+ const openings = await read(ids);
36062
+ const line = this.alarmSensorsReadLog.next(ids.length, openings.size);
36063
+ if (line !== null) {
36064
+ const panelId = this.alarmPanel?.deviceId;
36065
+ this.logger[line.level](line.message, {
36066
+ ...panelId === void 0 ? {} : { tags: { deviceId: panelId } },
36067
+ meta: line.meta
36068
+ });
36069
+ }
36070
+ return {
36071
+ sensors: ids.map((id) => alarmSensorStateOf(id, openings.get(id), this.deviceDirectory.get(id)?.online ?? null)),
36072
+ readAt: Date.now()
36073
+ };
36074
+ }
35693
36075
  /** Settings + the DERIVED coverage + the live panel. Read together — see the cap doc. */
35694
36076
  async alarmConfig() {
35695
36077
  const panel = this.alarmPanel;