@camstack/system 1.2.31 → 1.2.32

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 (54) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.d.ts +10 -0
  6. package/dist/builtins/alerts/alerts.addon.js +60 -1
  7. package/dist/builtins/alerts/alerts.addon.mjs +60 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  10. package/dist/builtins/console-logging/index.js +1 -1
  11. package/dist/builtins/console-logging/index.mjs +1 -1
  12. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  13. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  14. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  15. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  16. package/dist/builtins/hub-forwarder/index.js +1 -1
  17. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  18. package/dist/builtins/liveness-monitor/index.d.ts +2 -0
  19. package/dist/builtins/liveness-monitor/index.js +6 -0
  20. package/dist/builtins/liveness-monitor/index.mjs +2 -0
  21. package/dist/builtins/liveness-monitor/liveness-checks.d.ts +67 -0
  22. package/dist/builtins/liveness-monitor/liveness-monitor.addon.d.ts +32 -0
  23. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +216 -0
  24. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +212 -0
  25. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  26. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  27. package/dist/builtins/local-network/local-network.addon.js +1 -1
  28. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  29. package/dist/builtins/loki-logging/index.js +1 -1
  30. package/dist/builtins/loki-logging/index.mjs +1 -1
  31. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  32. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  33. package/dist/builtins/platform-probe/index.js +1 -1
  34. package/dist/builtins/platform-probe/index.mjs +1 -1
  35. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  36. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  37. package/dist/builtins/snapshot/index.js +1 -1
  38. package/dist/builtins/snapshot/index.mjs +1 -1
  39. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  40. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  41. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
  42. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  43. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  44. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  45. package/dist/builtins/system-config/system-config.addon.js +1 -1
  46. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  47. package/dist/builtins/winston-logging/index.js +1 -1
  48. package/dist/builtins/winston-logging/index.mjs +1 -1
  49. package/dist/{dist-CNmXITJ-.mjs → dist-BL_FHWPP.mjs} +21 -1
  50. package/dist/{dist-CDv5YTHQ.js → dist-CSs1A90r.js} +21 -1
  51. package/dist/index.d.ts +1 -0
  52. package/dist/index.js +4 -1
  53. package/dist/index.mjs +4 -2
  54. package/package.json +10 -1
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CDv5YTHQ.js");
6
+ const require_dist = require("../../dist-CSs1A90r.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_fs = require("node:fs");
9
9
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { Mt as EventCategory, at as errMsg, ot as BaseAddon, p as addonPagesCapability } from "../../dist-CNmXITJ-.mjs";
1
+ import { Mt as EventCategory, at as errMsg, ot as BaseAddon, p as addonPagesCapability } from "../../dist-BL_FHWPP.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CDv5YTHQ.js");
6
+ const require_dist = require("../../dist-CSs1A90r.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_fs = require("node:fs");
9
9
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { Mt as EventCategory, at as errMsg, h as addonWidgetsCapability, ot as BaseAddon } from "../../dist-CNmXITJ-.mjs";
1
+ import { Mt as EventCategory, at as errMsg, h as addonWidgetsCapability, ot as BaseAddon } from "../../dist-BL_FHWPP.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import * as path$1 from "node:path";
@@ -57,6 +57,16 @@ export declare class AlertCenterAddon extends BaseAddon<AlertCenterConfig> {
57
57
  * Dismiss the matching alert so the operator's badge count drops.
58
58
  */
59
59
  private handleAddonLoadRecovered;
60
+ /**
61
+ * SystemLivenessFailed — a liveness invariant of this node is false.
62
+ *
63
+ * `findingId` is already stable across ticks (`liveness:no-devices`,
64
+ * `liveness:no-footage:<deviceId>`, …), so it IS the alert id: an ongoing
65
+ * fault updates one alert instead of producing one every five minutes.
66
+ */
67
+ private handleLivenessFailed;
68
+ /** SystemLivenessRecovered — the invariant holds again; drop the alert. */
69
+ private handleLivenessRecovered;
60
70
  private buildMessage;
61
71
  private emitAlert;
62
72
  private updateAlert;
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CDv5YTHQ.js");
6
+ const require_dist = require("../../dist-CSs1A90r.js");
7
7
  //#region src/builtins/alerts/prune-policy.ts
8
8
  /**
9
9
  * Ids older than `cutoffMs`, oldest first.
@@ -93,6 +93,10 @@ var CATEGORY_MAPPINGS = {
93
93
  severity: "error",
94
94
  titleTemplate: "Addon failed to load"
95
95
  },
96
+ "system.liveness-failed": {
97
+ severity: "error",
98
+ titleTemplate: "Node liveness"
99
+ },
96
100
  "addon.load-recovered": {
97
101
  severity: "info",
98
102
  titleTemplate: "Addon recovered"
@@ -289,6 +293,14 @@ var AlertCenterAddon = class extends require_dist.BaseAddon {
289
293
  await this.handleAddonLoadRecovered(event);
290
294
  return;
291
295
  }
296
+ if (category === require_dist.EventCategory.SystemLivenessFailed) {
297
+ await this.handleLivenessFailed(event);
298
+ return;
299
+ }
300
+ if (category === require_dist.EventCategory.SystemLivenessRecovered) {
301
+ await this.handleLivenessRecovered(event);
302
+ return;
303
+ }
292
304
  const mapping = CATEGORY_MAPPINGS[category];
293
305
  if (!mapping) return;
294
306
  if (this.shouldSuppressByRate(category, event)) return;
@@ -480,6 +492,53 @@ var AlertCenterAddon = class extends require_dist.BaseAddon {
480
492
  } });
481
493
  }
482
494
  }
495
+ /**
496
+ * SystemLivenessFailed — a liveness invariant of this node is false.
497
+ *
498
+ * `findingId` is already stable across ticks (`liveness:no-devices`,
499
+ * `liveness:no-footage:<deviceId>`, …), so it IS the alert id: an ongoing
500
+ * fault updates one alert instead of producing one every five minutes.
501
+ */
502
+ async handleLivenessFailed(event) {
503
+ const data = event.data;
504
+ const findingId = String(data["findingId"] ?? "");
505
+ if (!findingId) return;
506
+ const deviceId = typeof data["deviceId"] === "number" ? data["deviceId"] : void 0;
507
+ const severity = data["severity"] === "warning" ? "warning" : "error";
508
+ const alert = {
509
+ id: findingId,
510
+ category: require_dist.EventCategory.SystemLivenessFailed,
511
+ severity,
512
+ title: String(data["title"] ?? "Node liveness"),
513
+ message: String(data["message"] ?? ""),
514
+ status: "active",
515
+ read: false,
516
+ createdAt: Date.now(),
517
+ updatedAt: Date.now(),
518
+ source: {
519
+ type: "liveness",
520
+ id: findingId
521
+ },
522
+ metadata: {
523
+ findingId,
524
+ ...deviceId !== void 0 ? { deviceId } : {}
525
+ }
526
+ };
527
+ await this.emitAlert(alert);
528
+ }
529
+ /** SystemLivenessRecovered — the invariant holds again; drop the alert. */
530
+ async handleLivenessRecovered(event) {
531
+ const findingId = String(event.data["findingId"] ?? "");
532
+ if (!findingId) return;
533
+ try {
534
+ await this.dismiss(findingId);
535
+ } catch (err) {
536
+ this.ctx.logger?.debug("LivenessRecovered: dismiss failed (likely already dismissed)", { meta: {
537
+ findingId,
538
+ error: require_dist.errMsg(err)
539
+ } });
540
+ }
541
+ }
483
542
  buildMessage(category, data, sourceId) {
484
543
  const addonId = data.addonId ?? sourceId;
485
544
  const deviceId = data.deviceId;
@@ -1,4 +1,4 @@
1
- import { Mt as EventCategory, at as errMsg, g as alertsCapability, n as AlertSchema, ot as BaseAddon, yt as createEvent } from "../../dist-CNmXITJ-.mjs";
1
+ import { Mt as EventCategory, at as errMsg, g as alertsCapability, n as AlertSchema, ot as BaseAddon, yt as createEvent } from "../../dist-BL_FHWPP.mjs";
2
2
  //#region src/builtins/alerts/prune-policy.ts
3
3
  /**
4
4
  * Ids older than `cutoffMs`, oldest first.
@@ -88,6 +88,10 @@ var CATEGORY_MAPPINGS = {
88
88
  severity: "error",
89
89
  titleTemplate: "Addon failed to load"
90
90
  },
91
+ "system.liveness-failed": {
92
+ severity: "error",
93
+ titleTemplate: "Node liveness"
94
+ },
91
95
  "addon.load-recovered": {
92
96
  severity: "info",
93
97
  titleTemplate: "Addon recovered"
@@ -284,6 +288,14 @@ var AlertCenterAddon = class extends BaseAddon {
284
288
  await this.handleAddonLoadRecovered(event);
285
289
  return;
286
290
  }
291
+ if (category === EventCategory.SystemLivenessFailed) {
292
+ await this.handleLivenessFailed(event);
293
+ return;
294
+ }
295
+ if (category === EventCategory.SystemLivenessRecovered) {
296
+ await this.handleLivenessRecovered(event);
297
+ return;
298
+ }
287
299
  const mapping = CATEGORY_MAPPINGS[category];
288
300
  if (!mapping) return;
289
301
  if (this.shouldSuppressByRate(category, event)) return;
@@ -475,6 +487,53 @@ var AlertCenterAddon = class extends BaseAddon {
475
487
  } });
476
488
  }
477
489
  }
490
+ /**
491
+ * SystemLivenessFailed — a liveness invariant of this node is false.
492
+ *
493
+ * `findingId` is already stable across ticks (`liveness:no-devices`,
494
+ * `liveness:no-footage:<deviceId>`, …), so it IS the alert id: an ongoing
495
+ * fault updates one alert instead of producing one every five minutes.
496
+ */
497
+ async handleLivenessFailed(event) {
498
+ const data = event.data;
499
+ const findingId = String(data["findingId"] ?? "");
500
+ if (!findingId) return;
501
+ const deviceId = typeof data["deviceId"] === "number" ? data["deviceId"] : void 0;
502
+ const severity = data["severity"] === "warning" ? "warning" : "error";
503
+ const alert = {
504
+ id: findingId,
505
+ category: EventCategory.SystemLivenessFailed,
506
+ severity,
507
+ title: String(data["title"] ?? "Node liveness"),
508
+ message: String(data["message"] ?? ""),
509
+ status: "active",
510
+ read: false,
511
+ createdAt: Date.now(),
512
+ updatedAt: Date.now(),
513
+ source: {
514
+ type: "liveness",
515
+ id: findingId
516
+ },
517
+ metadata: {
518
+ findingId,
519
+ ...deviceId !== void 0 ? { deviceId } : {}
520
+ }
521
+ };
522
+ await this.emitAlert(alert);
523
+ }
524
+ /** SystemLivenessRecovered — the invariant holds again; drop the alert. */
525
+ async handleLivenessRecovered(event) {
526
+ const findingId = String(event.data["findingId"] ?? "");
527
+ if (!findingId) return;
528
+ try {
529
+ await this.dismiss(findingId);
530
+ } catch (err) {
531
+ this.ctx.logger?.debug("LivenessRecovered: dismiss failed (likely already dismissed)", { meta: {
532
+ findingId,
533
+ error: errMsg(err)
534
+ } });
535
+ }
536
+ }
478
537
  buildMessage(category, data, sourceId) {
479
538
  const addonId = data.addonId ?? sourceId;
480
539
  const deviceId = data.deviceId;
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CDv5YTHQ.js");
6
+ const require_dist = require("../../dist-CSs1A90r.js");
7
7
  let node_fs = require("node:fs");
8
8
  let node_fs$1 = require_chunk.__toESM(node_fs, 1);
9
9
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { Mt as EventCategory, ot as BaseAddon, y as backupCapability } from "../../dist-CNmXITJ-.mjs";
1
+ import { Mt as EventCategory, ot as BaseAddon, y as backupCapability } from "../../dist-BL_FHWPP.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import Kt from "node:fs";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CDv5YTHQ.js");
6
+ const require_dist = require("../../dist-CSs1A90r.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/console-logging/console-destination.ts
9
9
  var LEVEL_RANK = {
@@ -1,4 +1,4 @@
1
- import { B as logDestinationCapability, ot as BaseAddon } from "../../dist-CNmXITJ-.mjs";
1
+ import { B as logDestinationCapability, ot as BaseAddon } from "../../dist-BL_FHWPP.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/console-logging/console-destination.ts
4
4
  var LEVEL_RANK = {
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CDv5YTHQ.js");
6
+ const require_dist = require("../../dist-CSs1A90r.js");
7
7
  let _camstack_types_node = require("@camstack/types/node");
8
8
  let zod = require("zod");
9
9
  let node_crypto = require("node:crypto");
@@ -1,4 +1,4 @@
1
- import { C as deviceStateCapability, D as enumerateItemArrayFields, K as parseStreamParamsFormPatch, M as getByPath, Mt as EventCategory, O as enumerateSchemaFields, S as deviceManagerCapability, W as normalizeUnit, X as setByPath, _ as applyTransform, a as CAP_NAMES_WITH_STATUS, at as errMsg, b as buildStreamParamsConfigSchema, dt as DeviceRole, ft as DeviceType, ht as WELL_KNOWN_TAB_MAP, it as validateExpressionSource, jt as sleep, k as evaluateLinkExpression, l as STREAM_PROFILE_META, nt as toExpressionValue, o as DeviceStatusSchema, ot as BaseAddon, t as ALL_CAPABILITY_DEFINITIONS, ut as DeviceFeature, w as deviceStatusCapability, wt as isDeviceConfigCap } from "../../dist-CNmXITJ-.mjs";
1
+ import { C as deviceStateCapability, D as enumerateItemArrayFields, K as parseStreamParamsFormPatch, M as getByPath, Mt as EventCategory, O as enumerateSchemaFields, S as deviceManagerCapability, W as normalizeUnit, X as setByPath, _ as applyTransform, a as CAP_NAMES_WITH_STATUS, at as errMsg, b as buildStreamParamsConfigSchema, dt as DeviceRole, ft as DeviceType, ht as WELL_KNOWN_TAB_MAP, it as validateExpressionSource, jt as sleep, k as evaluateLinkExpression, l as STREAM_PROFILE_META, nt as toExpressionValue, o as DeviceStatusSchema, ot as BaseAddon, t as ALL_CAPABILITY_DEFINITIONS, ut as DeviceFeature, w as deviceStatusCapability, wt as isDeviceConfigCap } from "../../dist-BL_FHWPP.mjs";
2
2
  import { canonicalDeviceFingerprint } from "@camstack/types/node";
3
3
  import { z } from "zod";
4
4
  import { randomUUID } from "node:crypto";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CDv5YTHQ.js");
6
+ const require_dist = require("../../dist-CSs1A90r.js");
7
7
  //#region src/builtins/doorbell/trigger-engine.ts
8
8
  /**
9
9
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -1,4 +1,4 @@
1
- import { Mt as EventCategory, T as doorbellCapability, at as errMsg, ft as DeviceType, ot as BaseAddon, yt as createEvent } from "../../dist-CNmXITJ-.mjs";
1
+ import { Mt as EventCategory, T as doorbellCapability, at as errMsg, ft as DeviceType, ot as BaseAddon, yt as createEvent } from "../../dist-BL_FHWPP.mjs";
2
2
  //#region src/builtins/doorbell/trigger-engine.ts
3
3
  /**
4
4
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CDv5YTHQ.js");
6
+ const require_dist = require("../../dist-CSs1A90r.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
9
9
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,4 +1,4 @@
1
- import { B as logDestinationCapability, ot as BaseAddon } from "../../dist-CNmXITJ-.mjs";
1
+ import { B as logDestinationCapability, ot as BaseAddon } from "../../dist-BL_FHWPP.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
4
4
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -0,0 +1,2 @@
1
+ export { LivenessMonitorAddon } from './liveness-monitor.addon.js';
2
+ export { evaluateLiveness, isLivenessFindingId, type CameraFootage, type LivenessFacts, type LivenessFinding, } from './liveness-checks.js';
@@ -0,0 +1,6 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ require("../../chunk-Cek0wNdY.js");
3
+ const require_builtins_liveness_monitor_liveness_monitor_addon = require("./liveness-monitor.addon.js");
4
+ exports.LivenessMonitorAddon = require_builtins_liveness_monitor_liveness_monitor_addon.LivenessMonitorAddon;
5
+ exports.evaluateLiveness = require_builtins_liveness_monitor_liveness_monitor_addon.evaluateLiveness;
6
+ exports.isLivenessFindingId = require_builtins_liveness_monitor_liveness_monitor_addon.isLivenessFindingId;
@@ -0,0 +1,2 @@
1
+ import { LivenessMonitorAddon, n as isLivenessFindingId, t as evaluateLiveness } from "./liveness-monitor.addon.mjs";
2
+ export { LivenessMonitorAddon, evaluateLiveness, isLivenessFindingId };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The liveness invariants, as pure functions.
3
+ *
4
+ * On 2026-08-03 the hub ran for four hours with zero devices, zero published
5
+ * streams and zero recordings, and nothing said a word. Three separate defects
6
+ * caused it — optional chaining that no-oped, an `if` with no `else`, a JSON
7
+ * parse that returned `{}` — and each was silent at the point of failure. All
8
+ * three were fixed and a log line added at each. This module exists for the
9
+ * CLASS rather than the instances: assert what must be true of a working node,
10
+ * and report when it is not.
11
+ *
12
+ * **Every check here is unambiguous, on purpose.** An alert that fires on a
13
+ * quiet night is one an operator learns to ignore, and an ignored alert is
14
+ * worse than no alert. So "the pipeline emitted no tracks recently" is NOT a
15
+ * check — a camera can legitimately see nothing for hours — while "a camera
16
+ * that has been recording all day stopped producing segments" is: continuous
17
+ * recording produces segments whether or not anything moves.
18
+ *
19
+ * I/O lives in the addon; everything here is a function of facts, so the
20
+ * outage can be replayed in a test.
21
+ */
22
+ /** What the monitor learned about one camera this tick. */
23
+ export interface CameraFootage {
24
+ readonly deviceId: number;
25
+ readonly online: boolean;
26
+ /**
27
+ * ms-epoch of the newest recorded segment within the lookback window, or
28
+ * `null` when the camera has produced NO footage in it.
29
+ *
30
+ * `null` means "this camera does not record" and is deliberately not a
31
+ * finding — it is what makes the check self-calibrating. Nothing has to be
32
+ * told which cameras are supposed to record: a camera that was recording and
33
+ * stopped is a fault, a camera that never records is a configuration.
34
+ */
35
+ readonly lastSegmentMs: number | null;
36
+ }
37
+ export interface LivenessFacts {
38
+ readonly nowMs: number;
39
+ /** Every device the node knows about, cameras and accessories alike. */
40
+ readonly deviceCount: number;
41
+ readonly cameras: readonly CameraFootage[];
42
+ /** How long a recording camera may stay silent before it is a fault. */
43
+ readonly staleAfterMs: number;
44
+ }
45
+ export interface LivenessFinding {
46
+ /**
47
+ * Stable across ticks, so an ongoing fault UPDATES one alert instead of
48
+ * producing a new one every minute. Same contract as the kernel's
49
+ * `addon-load-failed:<packageName>` ids.
50
+ */
51
+ readonly id: string;
52
+ readonly severity: 'error' | 'warning';
53
+ readonly title: string;
54
+ readonly message: string;
55
+ /** Present on per-camera findings. Always the numeric id. */
56
+ readonly deviceId?: number;
57
+ }
58
+ /**
59
+ * Evaluate every invariant against one tick's facts.
60
+ *
61
+ * Ordered most-fundamental first, and it STOPS at "no devices": when the node
62
+ * has nothing, every camera is also silent, and reporting twelve consequences
63
+ * of one cause is how an operator ends up reading none of them.
64
+ */
65
+ export declare function evaluateLiveness(facts: LivenessFacts): readonly LivenessFinding[];
66
+ /** Every finding id this module can produce, for the recovery sweep. */
67
+ export declare function isLivenessFindingId(id: string): boolean;
@@ -0,0 +1,32 @@
1
+ import { BaseAddon, ProviderRegistration } from '@camstack/types';
2
+ import { LivenessFinding } from './liveness-checks.js';
3
+ interface LivenessMonitorConfig extends Record<string, unknown> {
4
+ readonly staleAfterMs?: number;
5
+ }
6
+ export declare class LivenessMonitorAddon extends BaseAddon<LivenessMonitorConfig> {
7
+ private tickTimer;
8
+ private startedAtMs;
9
+ /** Finding ids currently reported, so recovery can be detected and announced. */
10
+ private active;
11
+ constructor();
12
+ protected onInitialize(): Promise<ProviderRegistration[]>;
13
+ protected onShutdown(): Promise<void>;
14
+ /**
15
+ * One evaluation. Public so a test or an operator-facing action can force it
16
+ * without waiting out the interval.
17
+ */
18
+ tick(): Promise<readonly LivenessFinding[]>;
19
+ /** Read the facts through the cap surface. Changes nothing. */
20
+ private gather;
21
+ /**
22
+ * Newest segment for a camera, or null when it has produced none in a day.
23
+ *
24
+ * Two steps on purpose: the healthy case asks only for the recent window,
25
+ * which is a handful of ranges, and only a camera that looks silent pays for
26
+ * the 24-hour lookback that decides whether it records at all.
27
+ */
28
+ private lastSegmentMs;
29
+ /** Announce new faults and recoveries; say nothing about an unchanged fault. */
30
+ private report;
31
+ }
32
+ export {};
@@ -0,0 +1,216 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ require("../../chunk-Cek0wNdY.js");
3
+ const require_dist = require("../../dist-CSs1A90r.js");
4
+ //#region src/builtins/liveness-monitor/liveness-checks.ts
5
+ var NO_DEVICES = "liveness:no-devices";
6
+ var ALL_OFFLINE = "liveness:all-devices-offline";
7
+ var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
8
+ function minutes(ms) {
9
+ return Math.round(ms / 6e4);
10
+ }
11
+ /**
12
+ * Evaluate every invariant against one tick's facts.
13
+ *
14
+ * Ordered most-fundamental first, and it STOPS at "no devices": when the node
15
+ * has nothing, every camera is also silent, and reporting twelve consequences
16
+ * of one cause is how an operator ends up reading none of them.
17
+ */
18
+ function evaluateLiveness(facts) {
19
+ if (facts.deviceCount === 0) return [{
20
+ id: NO_DEVICES,
21
+ severity: "error",
22
+ title: "No devices",
23
+ message: "This node has no devices. Nothing can publish a stream, record, or detect. The persisted device ledger is either unreachable or empty — check the boot log for \"SettingsStore not initialized\" and for \"[worker-restore] no persisted devices\"."
24
+ }];
25
+ const findings = [];
26
+ if (facts.cameras.length > 0 && facts.cameras.every((c) => !c.online)) findings.push({
27
+ id: ALL_OFFLINE,
28
+ severity: "error",
29
+ title: "Every camera offline",
30
+ message: `All ${facts.cameras.length} cameras report offline. This is a provider or network fault, not a per-camera one.`
31
+ });
32
+ for (const cam of facts.cameras) {
33
+ if (cam.lastSegmentMs === null) continue;
34
+ if (!cam.online) continue;
35
+ const silentFor = facts.nowMs - cam.lastSegmentMs;
36
+ if (silentFor <= facts.staleAfterMs) continue;
37
+ findings.push({
38
+ id: `${NO_FOOTAGE_PREFIX}${cam.deviceId}`,
39
+ severity: "error",
40
+ title: "Recording stopped",
41
+ message: `Camera ${cam.deviceId} is online and was recording, but has produced no segment for ${minutes(silentFor)} minutes.`,
42
+ deviceId: cam.deviceId
43
+ });
44
+ }
45
+ return findings;
46
+ }
47
+ /** Every finding id this module can produce, for the recovery sweep. */
48
+ function isLivenessFindingId(id) {
49
+ return id === NO_DEVICES || id === ALL_OFFLINE || id.startsWith(NO_FOOTAGE_PREFIX);
50
+ }
51
+ //#endregion
52
+ //#region src/builtins/liveness-monitor/liveness-monitor.addon.ts
53
+ /**
54
+ * liveness-monitor — the node states, out loud, when it has stopped working.
55
+ *
56
+ * On 2026-08-03 the hub ran for four hours with zero devices, zero published
57
+ * streams and zero recordings. Three separate defects caused it, each silent at
58
+ * the point of failure, and the operator found out by opening the viewer. Every
59
+ * one of those defects has since been fixed and given a log line — but a log
60
+ * line per known bug does not cover the next one. This addon covers the class:
61
+ * it asserts what must be true of a working node and reports when it is not.
62
+ *
63
+ * It emits `SystemLivenessFailed` / `SystemLivenessRecovered` on the event bus
64
+ * with a STABLE finding id, exactly like the kernel's addon-health monitor, so
65
+ * AlertCenter raises one persistent alert per fault and dismisses it on
66
+ * recovery instead of producing a stream of them. The same lines go to the log
67
+ * at ERROR, which is what makes them alertable in Loki.
68
+ *
69
+ * Deliberately hub-only and deliberately read-only: it observes through
70
+ * `ctx.api` and changes nothing. A monitor that can act is a monitor that can
71
+ * cause the outage it was meant to report — this repo has already stopped a
72
+ * hub that way once (the remedial-action watcher, 2026-07-30).
73
+ */
74
+ /** Event source stamped on every emission — the addon that observed it. */
75
+ var LIVENESS_SOURCE = {
76
+ type: "addon",
77
+ id: "liveness-monitor"
78
+ };
79
+ /** How often the invariants are checked. */
80
+ var TICK_INTERVAL_MS = 5 * 6e4;
81
+ /**
82
+ * Grace period after boot before anything is reported.
83
+ *
84
+ * Every check would be true during startup — devices restore, providers
85
+ * connect, the first segment takes a minute — and a monitor that cries at every
86
+ * restart is one nobody reads. The kernel's addon-health monitor takes the same
87
+ * position for the same reason.
88
+ */
89
+ var BOOT_GRACE_MS = 6 * 6e4;
90
+ /** How long a recording camera may stay silent before it is a fault. */
91
+ var DEFAULT_STALE_AFTER_MS = 15 * 6e4;
92
+ /**
93
+ * How far back to look for "does this camera record at all". A camera with no
94
+ * footage in this window is treated as a non-recording camera and never
95
+ * reported — which is what keeps the check free of any configuration coupling.
96
+ */
97
+ var RECORDING_LOOKBACK_MS = 1440 * 6e4;
98
+ var LivenessMonitorAddon = class extends require_dist.BaseAddon {
99
+ tickTimer = null;
100
+ startedAtMs = 0;
101
+ /** Finding ids currently reported, so recovery can be detected and announced. */
102
+ active = /* @__PURE__ */ new Set();
103
+ constructor() {
104
+ super({});
105
+ }
106
+ async onInitialize() {
107
+ this.startedAtMs = Date.now();
108
+ this.tickTimer = setInterval(() => {
109
+ this.tick();
110
+ }, TICK_INTERVAL_MS);
111
+ this.tickTimer.unref?.();
112
+ this.ctx.logger.info("liveness-monitor started", { meta: {
113
+ tickIntervalMs: TICK_INTERVAL_MS,
114
+ bootGraceMs: BOOT_GRACE_MS
115
+ } });
116
+ return [];
117
+ }
118
+ async onShutdown() {
119
+ if (this.tickTimer) clearInterval(this.tickTimer);
120
+ this.tickTimer = null;
121
+ }
122
+ /**
123
+ * One evaluation. Public so a test or an operator-facing action can force it
124
+ * without waiting out the interval.
125
+ */
126
+ async tick() {
127
+ if (Date.now() - this.startedAtMs < BOOT_GRACE_MS) return [];
128
+ try {
129
+ const findings = evaluateLiveness(await this.gather());
130
+ this.report(findings);
131
+ return findings;
132
+ } catch (err) {
133
+ this.ctx.logger.error("liveness-monitor: check failed — node health is UNKNOWN this tick", { meta: { error: require_dist.errMsg(err) } });
134
+ return [];
135
+ }
136
+ }
137
+ /** Read the facts through the cap surface. Changes nothing. */
138
+ async gather() {
139
+ const nowMs = Date.now();
140
+ const staleAfterMs = this.config.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
141
+ const devices = await this.ctx.api.deviceManager.listAll.query({});
142
+ if (devices.length === 0) return {
143
+ nowMs,
144
+ deviceCount: 0,
145
+ cameras: [],
146
+ staleAfterMs
147
+ };
148
+ const cameraDevices = devices.filter((d) => d.isCamera === true);
149
+ const cameras = [];
150
+ for (const device of cameraDevices) cameras.push({
151
+ deviceId: device.id,
152
+ online: device.online === true,
153
+ lastSegmentMs: await this.lastSegmentMs(device.id, nowMs, staleAfterMs)
154
+ });
155
+ return {
156
+ nowMs,
157
+ deviceCount: devices.length,
158
+ cameras,
159
+ staleAfterMs
160
+ };
161
+ }
162
+ /**
163
+ * Newest segment for a camera, or null when it has produced none in a day.
164
+ *
165
+ * Two steps on purpose: the healthy case asks only for the recent window,
166
+ * which is a handful of ranges, and only a camera that looks silent pays for
167
+ * the 24-hour lookback that decides whether it records at all.
168
+ */
169
+ async lastSegmentMs(deviceId, nowMs, staleAfterMs) {
170
+ const recent = await this.ctx.api.recording.getAvailability.query({
171
+ deviceId,
172
+ fromMs: nowMs - staleAfterMs,
173
+ toMs: nowMs
174
+ });
175
+ if (recent.ranges.length > 0) return Math.max(...recent.ranges.map((r) => r.endMs));
176
+ const day = await this.ctx.api.recording.getAvailability.query({
177
+ deviceId,
178
+ fromMs: nowMs - RECORDING_LOOKBACK_MS,
179
+ toMs: nowMs
180
+ });
181
+ if (day.ranges.length === 0) return null;
182
+ return Math.max(...day.ranges.map((r) => r.endMs));
183
+ }
184
+ /** Announce new faults and recoveries; say nothing about an unchanged fault. */
185
+ report(findings) {
186
+ const current = new Set(findings.map((f) => f.id));
187
+ for (const finding of findings) {
188
+ if (this.active.has(finding.id)) continue;
189
+ this.active.add(finding.id);
190
+ this.ctx.logger.error(`liveness: ${finding.title} — ${finding.message}`, {
191
+ ...finding.deviceId !== void 0 ? { tags: { deviceId: finding.deviceId } } : {},
192
+ meta: {
193
+ findingId: finding.id,
194
+ severity: finding.severity
195
+ }
196
+ });
197
+ this.ctx.eventBus.emit(require_dist.createEvent(require_dist.EventCategory.SystemLivenessFailed, LIVENESS_SOURCE, {
198
+ findingId: finding.id,
199
+ severity: finding.severity,
200
+ title: finding.title,
201
+ message: finding.message,
202
+ ...finding.deviceId !== void 0 ? { deviceId: finding.deviceId } : {}
203
+ }));
204
+ }
205
+ for (const id of [...this.active]) {
206
+ if (current.has(id)) continue;
207
+ this.active.delete(id);
208
+ this.ctx.logger.info(`liveness: recovered — ${id}`, { meta: { findingId: id } });
209
+ this.ctx.eventBus.emit(require_dist.createEvent(require_dist.EventCategory.SystemLivenessRecovered, LIVENESS_SOURCE, { findingId: id }));
210
+ }
211
+ }
212
+ };
213
+ //#endregion
214
+ exports.LivenessMonitorAddon = LivenessMonitorAddon;
215
+ exports.evaluateLiveness = evaluateLiveness;
216
+ exports.isLivenessFindingId = isLivenessFindingId;