@camstack/system 1.2.453 → 1.2.454

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 (95) hide show
  1. package/dist/{agent-https-DL2cFOQv.js → agent-https-B7IjRRzT.js} +2 -2
  2. package/dist/{agent-https-XAZFJjOS.mjs → agent-https-oXsR24R2.mjs} +2 -2
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alert-episode-policy.d.ts +39 -0
  8. package/dist/builtins/alerts/alerts.addon.d.ts +25 -38
  9. package/dist/builtins/alerts/alerts.addon.js +178 -423
  10. package/dist/builtins/alerts/alerts.addon.mjs +178 -423
  11. package/dist/builtins/alerts/emit-alert-policy.d.ts +3 -16
  12. package/dist/builtins/alerts/runner-alert.d.ts +0 -13
  13. package/dist/builtins/autotrack/index.js +1 -1
  14. package/dist/builtins/autotrack/index.mjs +1 -1
  15. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  16. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  17. package/dist/builtins/calendars/calendars.addon.js +1 -1
  18. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  19. package/dist/builtins/camera-grid/index.js +1 -1
  20. package/dist/builtins/camera-grid/index.mjs +1 -1
  21. package/dist/builtins/composer/composer.addon.js +2 -2
  22. package/dist/builtins/composer/composer.addon.mjs +2 -2
  23. package/dist/builtins/console-logging/index.js +1 -1
  24. package/dist/builtins/console-logging/index.mjs +1 -1
  25. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  26. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  27. package/dist/builtins/device-history/device-history.addon.js +1 -1
  28. package/dist/builtins/device-history/device-history.addon.mjs +1 -1
  29. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  30. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  31. package/dist/builtins/hub-forwarder/index.js +1 -1
  32. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  33. package/dist/builtins/keychain/index.js +4 -4
  34. package/dist/builtins/keychain/index.mjs +4 -4
  35. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  36. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  37. package/dist/builtins/local-auth/local-auth.addon.js +3 -3
  38. package/dist/builtins/local-auth/local-auth.addon.mjs +3 -3
  39. package/dist/builtins/local-network/local-network.addon.js +2 -2
  40. package/dist/builtins/local-network/local-network.addon.mjs +2 -2
  41. package/dist/builtins/loki-logging/index.js +1 -1
  42. package/dist/builtins/loki-logging/index.mjs +1 -1
  43. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  44. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  45. package/dist/builtins/platform-probe/index.js +1 -1
  46. package/dist/builtins/platform-probe/index.mjs +1 -1
  47. package/dist/builtins/record-entries/index.js +22 -2
  48. package/dist/builtins/record-entries/index.mjs +22 -2
  49. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  50. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  51. package/dist/builtins/snapshot/index.js +1 -1
  52. package/dist/builtins/snapshot/index.mjs +1 -1
  53. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  54. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  55. package/dist/builtins/sqlite-storage/index.js +1 -1
  56. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  57. package/dist/builtins/sqlite-storage/records/records-engine.d.ts +13 -0
  58. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
  59. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
  60. package/dist/builtins/storage-orchestrator/location-class-paths.d.ts +13 -0
  61. package/dist/builtins/storage-orchestrator/location-repoint-move.d.ts +6 -1
  62. package/dist/builtins/storage-orchestrator/location-repointer.d.ts +97 -0
  63. package/dist/builtins/storage-orchestrator/location-store.d.ts +13 -2
  64. package/dist/builtins/storage-orchestrator/record-kinds.d.ts +5 -0
  65. package/dist/builtins/storage-orchestrator/storage-location-band-monitor.d.ts +38 -4
  66. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +8 -2
  67. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +486 -143
  68. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +486 -143
  69. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +26 -1
  70. package/dist/builtins/system-config/system-config.addon.js +3 -3
  71. package/dist/builtins/system-config/system-config.addon.mjs +3 -3
  72. package/dist/builtins/winston-logging/index.js +1 -1
  73. package/dist/builtins/winston-logging/index.mjs +1 -1
  74. package/dist/{composition-sources-DwEvAg5Z.js → composition-sources-Cukc78eu.js} +1 -1
  75. package/dist/{composition-sources-jYNpYEhS.mjs → composition-sources-bt6IQDLu.mjs} +1 -1
  76. package/dist/{constants-B3fDUnJt.js → constants-DBGG6Zu5.js} +1 -1
  77. package/dist/{constants-DGBB3tNi.mjs → constants-DQcVrhVI.mjs} +1 -1
  78. package/dist/{device-ca-njpTMpJC.js → device-ca-B3hUVfYk.js} +1 -1
  79. package/dist/{device-ca-Dpkxg7px.mjs → device-ca-Utfvsx5x.mjs} +1 -1
  80. package/dist/{dist-B9C2v4SG.mjs → dist-7ZciHCIp.mjs} +315 -44
  81. package/dist/{dist-DyjNaeIj.js → dist-Bi4Ioxop.js} +338 -43
  82. package/dist/file-host-worker.js +1 -1
  83. package/dist/file-host-worker.mjs +1 -1
  84. package/dist/{hub-mdns-ciao-DpdFaLz-.js → hub-mdns-ciao-2FzLWfbD.js} +2 -2
  85. package/dist/{hub-mdns-ciao-B4wHDMvk.mjs → hub-mdns-ciao-BRs3DUT-.mjs} +2 -2
  86. package/dist/index.js +7 -7
  87. package/dist/index.mjs +7 -7
  88. package/dist/{phase6-declare-gate-CNxKbsvm.mjs → phase6-declare-gate-D-2LMw9o.mjs} +1 -1
  89. package/dist/{phase6-declare-gate-BlTDMI3t.js → phase6-declare-gate-DwYAep7y.js} +1 -1
  90. package/dist/{restore-archive-BeGP9IYs.js → restore-archive-C0ymYQk7.js} +1 -1
  91. package/dist/{restore-archive-D7HwsszI.mjs → restore-archive-UVZv_hmr.mjs} +1 -1
  92. package/dist/{wal-maintenance-i3GRvmQW.js → wal-maintenance-Bo_lo3Dh.js} +0 -0
  93. package/dist/{wal-maintenance-HEmw2Fik.mjs → wal-maintenance-Df89xxrK.mjs} +0 -0
  94. package/package.json +1 -1
  95. package/dist/builtins/alerts/critical-push.d.ts +0 -90
@@ -3,114 +3,50 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DyjNaeIj.js");
6
+ const require_dist = require("../../dist-Bi4Ioxop.js");
7
7
  const require_read_all_rows$1 = require("../../read-all-rows-CVoO1ONM.js");
8
- const require_phase6_declare_gate = require("../../phase6-declare-gate-BlTDMI3t.js");
9
- //#region src/builtins/alerts/critical-push.ts
10
- /** Alert categories that are critical system findings. */
11
- var CRITICAL_CATEGORIES = new Set([
12
- "addon.runner-failed",
13
- "addon.runner-refused",
14
- "system.runner-check"
15
- ]);
16
- function isCriticalPush(alert) {
17
- return alert.severity === "critical" || CRITICAL_CATEGORIES.has(alert.category);
18
- }
19
- function isPushState(value) {
20
- if (typeof value !== "object" || value === null) return false;
21
- const rec = value;
22
- return (rec["state"] === "pending" || rec["state"] === "delivered" || rec["state"] === "disabled") && typeof rec["key"] === "string" && typeof rec["attempts"] === "number";
23
- }
24
- function selectPushTargets(targets, policy) {
25
- const enabled = targets.filter((t) => t.enabled);
26
- if (policy.targetIds.length > 0) return enabled.filter((t) => policy.targetIds.includes(t.id));
27
- return enabled.filter((t) => t.ownerUserId === void 0);
28
- }
29
- var MAX_DIGEST_LINES = 20;
30
- /** The one message a target gets for a set of findings. */
31
- function composeDigest(items) {
32
- const first = items[0];
33
- if (items.length === 1 && first !== void 0) return {
34
- title: first.title,
35
- body: first.line
36
- };
37
- const shown = items.slice(0, MAX_DIGEST_LINES).map((i) => i.line);
38
- const more = items.length - shown.length;
39
- return {
40
- title: `${String(items.length)} addons need attention`,
41
- body: [...shown, ...more > 0 ? [`... and ${String(more)} more`] : []].join("\n")
42
- };
43
- }
8
+ const require_phase6_declare_gate = require("../../phase6-declare-gate-DwYAep7y.js");
9
+ //#region src/builtins/alerts/alert-episode-policy.ts
44
10
  /**
45
- * Push every item that still needs it, as ONE message per target. Returns the new
46
- * state of each item whose state changed (an item already delivered/disabled for
47
- * its key is absent from the result).
11
+ * When does an alert open a new EPISODE, and which edge of it is announced?
12
+ *
13
+ * The alert centre sends no notification. It stamps every alert it stores with
14
+ * an `episodeId` (`metadata.episodeId`) and announces the opening and the
15
+ * closing of critical and error episodes on the bus; Notification Center turns
16
+ * those into notifications (`alert-critical`, `alert-error`,
17
+ * `alert-critical-resolved`). The identity is what makes a notification
18
+ * once-per-episode: any number of re-emits of an open alert keep the id, a new
19
+ * episode after completion gets a new one.
48
20
  */
49
- async function deliverDigest(input) {
50
- const out = /* @__PURE__ */ new Map();
51
- const work = input.items.filter((i) => !(i.previous !== null && i.previous.key === i.key && i.previous.state !== "pending"));
52
- if (work.length === 0) return out;
53
- const sameKey = (i) => i.previous !== null && i.previous.key === i.key ? i.previous : null;
54
- const attemptsOf = (i) => (sameKey(i)?.attempts ?? 0) + 1;
55
- const sentOf = (i) => sameKey(i)?.sentTo ?? [];
56
- if (!input.policy.enabled) {
57
- for (const i of work) out.set(i.alertId, {
58
- state: "disabled",
59
- key: i.key,
60
- attempts: attemptsOf(i)
61
- });
62
- return out;
63
- }
64
- const fail = (error) => {
65
- for (const i of work) out.set(i.alertId, {
66
- state: "pending",
67
- key: i.key,
68
- attempts: attemptsOf(i),
69
- lastError: error,
70
- ...sentOf(i).length > 0 ? { sentTo: sentOf(i) } : {}
71
- });
72
- return out;
73
- };
74
- let targets;
75
- try {
76
- targets = selectPushTargets(await input.ports.listTargets(), input.policy);
77
- } catch (err) {
78
- return fail(`notifiers unavailable: ${err instanceof Error ? err.message : String(err)}`);
79
- }
80
- if (targets.length === 0) return fail("no eligible notifier target");
81
- const sent = new Map(work.map((i) => [i.alertId, [...sentOf(i)]]));
82
- const errors = new Map(work.map((i) => [i.alertId, []]));
83
- for (const target of targets) {
84
- const todo = work.filter((i) => !(sent.get(i.alertId) ?? []).includes(target.id));
85
- if (todo.length === 0) continue;
86
- let failure = null;
87
- try {
88
- const result = await input.ports.send(target, composeDigest(todo));
89
- if (!result.success) failure = `${target.id}: ${result.error ?? "send failed"}`;
90
- } catch (err) {
91
- failure = `${target.id}: ${err instanceof Error ? err.message : String(err)}`;
92
- }
93
- for (const i of todo) if (failure === null) sent.get(i.alertId)?.push(target.id);
94
- else errors.get(i.alertId)?.push(failure);
95
- }
96
- for (const i of work) {
97
- const sentTo = sent.get(i.alertId) ?? [];
98
- const complete = targets.every((t) => sentTo.includes(t.id));
99
- out.set(i.alertId, complete ? {
100
- state: "delivered",
101
- key: i.key,
102
- at: input.now,
103
- attempts: attemptsOf(i),
104
- sentTo
105
- } : {
106
- state: "pending",
107
- key: i.key,
108
- attempts: attemptsOf(i),
109
- lastError: (errors.get(i.alertId) ?? []).join("; "),
110
- ...sentTo.length > 0 ? { sentTo } : {}
111
- });
21
+ /**
22
+ * The previous episode continues while its alert is still open and the emitter
23
+ * did not mark a new one; anything else opens `<alertId>@<now>`.
24
+ */
25
+ function nextEpisodeId(input) {
26
+ const { previous } = input;
27
+ if (previous !== null && require_dist.isAlertEpisodeOpen(previous.status)) {
28
+ if (input.incomingKey === void 0 || input.incomingKey === previous.episodeKey) return previous.episodeId;
112
29
  }
113
- return out;
30
+ const minted = `${input.alertId}@${String(input.now)}`;
31
+ return previous !== null && previous.episodeId === minted ? `${minted}+` : minted;
32
+ }
33
+ /**
34
+ * The edge to announce for a write that took `previous` to `next`, or `null`.
35
+ * A re-emit that changes nothing about the episode announces nothing; an
36
+ * escalation from `error` to `critical` inside one episode is announced again
37
+ * (it is a different kind, and the operator opted into it separately).
38
+ */
39
+ function episodeEdge(previous, next) {
40
+ const nextOpen = require_dist.isAlertEpisodeOpen(next.status);
41
+ const continues = previous !== null && require_dist.isAlertEpisodeOpen(previous.status) && previous.episodeId === next.episodeId;
42
+ if (nextOpen) {
43
+ if (!require_dist.isAlertEpisodeSeverity(next.severity)) return null;
44
+ if (!continues || previous === null) return "raised";
45
+ if (!require_dist.isAlertEpisodeSeverity(previous.severity)) return "raised";
46
+ return previous.severity === "error" && next.severity === "critical" ? "raised" : null;
47
+ }
48
+ if (previous !== null && require_dist.isAlertEpisodeOpen(previous.status) && require_dist.isAlertEpisodeSeverity(previous.severity)) return "resolved";
49
+ return null;
114
50
  }
115
51
  //#endregion
116
52
  //#region src/builtins/alerts/emit-alert-policy.ts
@@ -118,24 +54,9 @@ function episodeKeyFromMetadata(metadata) {
118
54
  const key = metadata?.["episodeKey"];
119
55
  return typeof key === "string" ? key : void 0;
120
56
  }
121
- /** Completed episodes start fresh — prior push must not suppress the next digest. */
122
- function shouldDropPriorPush(existingStatus) {
123
- return existingStatus === "completed";
124
- }
125
- function alertWithRetainedPush(toStore, existingPush, existingStatus) {
126
- if (existingPush === void 0 || shouldDropPriorPush(existingStatus)) return toStore;
127
- return {
128
- ...toStore,
129
- metadata: {
130
- ...toStore.metadata,
131
- push: existingPush
132
- }
133
- };
134
- }
135
57
  /**
136
- * Re-emit of the same alert id: preserve read when the episode is unchanged
137
- * and still active. A completed episode starts again. Queue a critical push
138
- * only for active, unread critical findings.
58
+ * Re-emit of the same alert id: preserve `read` when the episode is unchanged
59
+ * and still active. A completed episode starts again.
139
60
  */
140
61
  function mergeEmitAlert(input) {
141
62
  const incomingKey = episodeKeyFromMetadata(input.incoming.metadata);
@@ -144,26 +65,11 @@ function mergeEmitAlert(input) {
144
65
  const sameEpisode = incomingKey === void 0 || incomingKey === existingKey;
145
66
  const priorCompleted = input.existing.status === "completed";
146
67
  if (sameEpisode && !priorCompleted) return {
147
- alert: {
148
- ...input.incoming,
149
- read: true
150
- },
151
- queuePush: false
68
+ ...input.incoming,
69
+ read: true
152
70
  };
153
71
  }
154
- const queuePush = isCriticalPush(input.incoming) && input.incoming.status === "active" && input.incoming.read === false;
155
- return {
156
- alert: input.incoming,
157
- queuePush
158
- };
159
- }
160
- function criticalPushPayload(alert) {
161
- const key = episodeKeyFromMetadata(alert.metadata) ?? alert.id;
162
- const pushLine = alert.metadata?.["pushLine"];
163
- return {
164
- key,
165
- line: typeof pushLine === "string" ? pushLine : alert.title
166
- };
72
+ return input.incoming;
167
73
  }
168
74
  //#endregion
169
75
  //#region src/builtins/alerts/device-label.ts
@@ -297,12 +203,12 @@ function renderRunnerAlert(record) {
297
203
  const where = record.nodeId === "hub" ? record.runnerId : `${record.runnerId} on ${record.nodeId}`;
298
204
  switch (record.state) {
299
205
  case "refused": return {
300
- severity: "error",
206
+ severity: "critical",
301
207
  title: "Addon refused to start",
302
208
  message: `${where}: refused - ${record.reason ?? "no reason reported"}${record.remedy !== void 0 ? ` ${record.remedy}` : ""}`
303
209
  };
304
210
  case "failed": return {
305
- severity: "error",
211
+ severity: "critical",
306
212
  title: "Addon crash loop",
307
213
  message: `Runner "${where}" gave up${record.crashesInWindow !== void 0 ? ` after ${String(record.crashesInWindow)} crashes` : ""}${record.addonIds.length > 0 ? ` (addons: ${record.addonIds.join(", ")})` : ""} - ${String(record.count)} events`
308
214
  };
@@ -313,32 +219,6 @@ function renderRunnerAlert(record) {
313
219
  };
314
220
  }
315
221
  }
316
- /** Is the record in a state an operator must act on (terminal)? */
317
- function isTerminalRunnerState(state) {
318
- return state === "failed" || state === "refused";
319
- }
320
- function hash(text) {
321
- let h = 5381;
322
- for (let i = 0; i < text.length; i++) h = (h << 5) + h + text.charCodeAt(i) | 0;
323
- return (h >>> 0).toString(36);
324
- }
325
- /**
326
- * The push key of a record: its episode and its cause. The same refusal of the
327
- * same episode is never pushed twice; a refusal with another reason, a breaker
328
- * trip, or any failure after a recovery (a new episode) is a new push.
329
- */
330
- function runnerPushKey(record) {
331
- const cause = record.state === "refused" ? hash(`${record.reason ?? ""}|${record.remedy ?? ""}`) : "breaker";
332
- return `runner:${record.state}:${String(record.since)}:${cause}`;
333
- }
334
- /**
335
- * The one line a push carries: runner, state and the remedy. Deliberately NOT
336
- * the raw reason or error text - those stay on the alert.
337
- */
338
- function runnerPushLine(record) {
339
- const where = record.nodeId === "hub" ? record.runnerId : `${record.runnerId} on ${record.nodeId}`;
340
- return record.state === "refused" ? `${where}: refused to start. ${record.remedy ?? "See the Addons page."}` : `${where}: stopped after repeated crashes. Restart it from the Addons page.`;
341
- }
342
222
  //#endregion
343
223
  //#region src/builtins/alerts/record-kinds.ts
344
224
  var import_read_all_rows = require_read_all_rows$1.require_read_all_rows();
@@ -646,7 +526,6 @@ var CATEGORY_MAPPINGS = {
646
526
  * {@link CATEGORY_MAPPINGS}.
647
527
  */
648
528
  var SILENT_TRACKED_CATEGORIES = [
649
- "addon.started",
650
529
  "process.state-changed",
651
530
  "addon.load-recovered",
652
531
  "system.liveness-recovered",
@@ -715,24 +594,26 @@ function parseAlert(record) {
715
594
  }
716
595
  var DEFAULT_CONFIG = {
717
596
  maxAlerts: 500,
718
- retentionDays: 30,
719
- criticalPushEnabled: true,
720
- criticalPushTargetIds: []
597
+ retentionDays: 30
721
598
  };
722
- /** Accept the live `string[]` shape and the pre-multiselect CSV string. */
723
- function normalizeCriticalPushTargetIds(raw) {
724
- if (Array.isArray(raw)) return raw.filter((id) => typeof id === "string" && id.trim().length > 0).map((id) => id.trim());
725
- if (typeof raw === "string" && raw.trim().length > 0) return raw.split(",").map((id) => id.trim()).filter((id) => id.length > 0);
726
- return [];
727
- }
728
- /** One burst of critical findings (a whole boot of refused runners) is ONE digest push. */
729
- var PUSH_COALESCE_MS = 2e3;
730
- /** Addons whose (re)start is a reason to retry pending critical pushes. */
731
- var PUSH_RECONCILE_ADDONS = new Set(["notifiers", "notification-center"]);
732
599
  var EVENT_SOURCE = {
733
600
  type: "addon",
734
601
  id: "alert-center"
735
602
  };
603
+ /** Where the operator acts on a runner that failed, was refused or is missing. */
604
+ var ADDONS_PAGE_ACTION = {
605
+ label: "Open Addons",
606
+ href: "/system/addons"
607
+ };
608
+ function episodeViewOf(alert) {
609
+ const key = episodeKeyFromMetadata(alert.metadata);
610
+ return {
611
+ status: alert.status,
612
+ severity: alert.severity,
613
+ episodeId: require_dist.alertEpisodeId(alert),
614
+ ...key !== void 0 ? { episodeKey: key } : {}
615
+ };
616
+ }
736
617
  /**
737
618
  * Alert Center addon — core builtin that persists alerts in a
738
619
  * structured SQLite table and serves them to the admin UI.
@@ -751,11 +632,8 @@ function toPrunable(record) {
751
632
  var AlertCenterAddon = class extends require_dist.BaseAddon {
752
633
  alertStore = null;
753
634
  unsubscribers = [];
754
- /** How long a burst of critical findings is gathered into one digest push. */
755
- pushCoalesceMs;
756
- constructor(options) {
635
+ constructor() {
757
636
  super({ ...DEFAULT_CONFIG });
758
- this.pushCoalesceMs = options?.pushCoalesceMs ?? PUSH_COALESCE_MS;
759
637
  }
760
638
  /**
761
639
  * Per `(category, source.id)` counter used to rate-limit chatty
@@ -786,9 +664,7 @@ var AlertCenterAddon = class extends require_dist.BaseAddon {
786
664
  unsubs.push(unsub);
787
665
  }
788
666
  this.unsubscribers = unsubs;
789
- this.reconcilePendingPushes();
790
667
  this.dismissLegacyStreamCards(void 0);
791
- this.migrateCriticalPushTargetIds();
792
668
  this.ctx.logger.info("Initialized", { meta: { trackedCategoriesCount: TRACKED_CATEGORIES.length } });
793
669
  return [{
794
670
  capability: require_dist.alertsCapability,
@@ -807,46 +683,29 @@ var AlertCenterAddon = class extends require_dist.BaseAddon {
807
683
  description: "Configure alert storage and retention.",
808
684
  descriptionKey: "server:globalSettings.s_alerts.description",
809
685
  columns: 1,
810
- fields: [
811
- this.field({
812
- type: "number",
813
- key: "maxAlerts",
814
- label: "Maximum stored alerts",
815
- labelKey: "server:globalSettings.s_alerts.maxAlerts.label",
816
- description: "Oldest read alerts are deleted when this limit is exceeded.",
817
- descriptionKey: "server:globalSettings.s_alerts.maxAlerts.description",
818
- min: 50,
819
- max: 5e3,
820
- step: 50,
821
- default: 500
822
- }),
823
- this.field({
824
- type: "number",
825
- key: "retentionDays",
826
- label: "Alert retention (days)",
827
- labelKey: "server:globalSettings.s_alerts.retentionDays.label",
828
- description: "Alerts older than this are automatically removed.",
829
- descriptionKey: "server:globalSettings.s_alerts.retentionDays.description",
830
- min: 1,
831
- max: 365,
832
- step: 1,
833
- default: 30
834
- }),
835
- this.field({
836
- type: "boolean",
837
- key: "criticalPushEnabled",
838
- label: "Push critical system alerts",
839
- description: "A runner that failed or refused to start, or a node whose runners are degraded, is pushed straight to the notifier targets — even when Notification Center is down. This path does not use notification rules.",
840
- default: true
841
- }),
842
- this.field({
843
- type: "notification-target-multiselect",
844
- key: "criticalPushTargetIds",
845
- label: "Critical push targets",
846
- description: "Notifier targets that receive critical system alerts. Empty = every enabled shared (not personal) target.",
847
- default: []
848
- })
849
- ]
686
+ fields: [this.field({
687
+ type: "number",
688
+ key: "maxAlerts",
689
+ label: "Maximum stored alerts",
690
+ labelKey: "server:globalSettings.s_alerts.maxAlerts.label",
691
+ description: "Oldest read alerts are deleted when this limit is exceeded.",
692
+ descriptionKey: "server:globalSettings.s_alerts.maxAlerts.description",
693
+ min: 50,
694
+ max: 5e3,
695
+ step: 50,
696
+ default: 500
697
+ }), this.field({
698
+ type: "number",
699
+ key: "retentionDays",
700
+ label: "Alert retention (days)",
701
+ labelKey: "server:globalSettings.s_alerts.retentionDays.label",
702
+ description: "Alerts older than this are automatically removed.",
703
+ descriptionKey: "server:globalSettings.s_alerts.retentionDays.description",
704
+ min: 1,
705
+ max: 365,
706
+ step: 1,
707
+ default: 30
708
+ })]
850
709
  }] });
851
710
  }
852
711
  async listAlerts(filter) {
@@ -885,7 +744,17 @@ var AlertCenterAddon = class extends require_dist.BaseAddon {
885
744
  updatedAt: Date.now()
886
745
  });
887
746
  }
888
- async handleEvent(category, event) {
747
+ /**
748
+ * Events are handled one at a time: every handler is a read-modify-write of
749
+ * an alert, and two concurrent events about one runner would both read "no
750
+ * alert yet" and open two episodes for it.
751
+ */
752
+ eventChain = Promise.resolve();
753
+ handleEvent(category, event) {
754
+ this.eventChain = this.eventChain.then(() => this.processEvent(category, event));
755
+ return this.eventChain;
756
+ }
757
+ async processEvent(category, event) {
889
758
  try {
890
759
  if (category === "model.download.progress") {
891
760
  await this.handleModelDownload(event);
@@ -903,11 +772,6 @@ var AlertCenterAddon = class extends require_dist.BaseAddon {
903
772
  await this.handleRunnerCheck(event);
904
773
  return;
905
774
  }
906
- if (category === require_dist.EventCategory.AddonStarted) {
907
- const addonId = event.data["addonId"];
908
- if (typeof addonId === "string" && PUSH_RECONCILE_ADDONS.has(addonId)) await this.reconcilePendingPushes();
909
- return;
910
- }
911
775
  if (category === require_dist.EventCategory.AddonLoadFailed) {
912
776
  await this.handleAddonLoadFailed(event);
913
777
  return;
@@ -1008,11 +872,8 @@ var AlertCenterAddon = class extends require_dist.BaseAddon {
1008
872
  type: "runner",
1009
873
  id: record.runnerId
1010
874
  },
1011
- metadata: {
1012
- runner: record,
1013
- pushLine: runnerPushLine(record),
1014
- ...existing?.push !== void 0 ? { push: existing.push } : {}
1015
- }
875
+ action: ADDONS_PAGE_ACTION,
876
+ metadata: { runner: record }
1016
877
  };
1017
878
  await this.persistAlert(alert);
1018
879
  if (previous === null) this.emitAlertCreatedEvent(alert);
@@ -1021,17 +882,11 @@ var AlertCenterAddon = class extends require_dist.BaseAddon {
1021
882
  title: alert.title
1022
883
  });
1023
884
  await this.enforceMaxAlerts();
1024
- if (isTerminalRunnerState(record.state)) this.queuePush({
1025
- alertId,
1026
- key: runnerPushKey(record),
1027
- title: rendered.title,
1028
- line: runnerPushLine(record)
1029
- });
1030
885
  }
1031
886
  /**
1032
887
  * A runner reached `running`: its episode is over. Close the runner alert so a
1033
- * later failure opens a NEW episode (new push key), instead of folding into a
1034
- * stale `refused` record that would never push again.
888
+ * later failure opens a NEW episode, instead of folding into a stale
889
+ * `refused` record whose notification was already sent.
1035
890
  */
1036
891
  async handleRunnerStateChanged(event) {
1037
892
  if (event.data["state"] !== "running") return;
@@ -1057,24 +912,20 @@ var AlertCenterAddon = class extends require_dist.BaseAddon {
1057
912
  }) : [];
1058
913
  const failedIds = ids(event.data["failed"]);
1059
914
  const missingIds = ids(event.data["missing"]);
1060
- const pushLine = `${nodeId}: runners degraded - ${[
915
+ const parts = [
1061
916
  ...failedIds.length > 0 ? [`failed: ${failedIds.join(", ")}`] : [],
1062
917
  ...refusedIds.length > 0 ? [`refused: ${refusedIds.join(", ")}`] : [],
1063
918
  ...missingIds.length > 0 ? [`missing: ${missingIds.join(", ")}`] : []
1064
- ].join("; ")}. See the Addons page.`;
1065
- const pushKey = `check:${[
1066
- ...failedIds,
1067
- ...refusedIds,
1068
- ...missingIds
1069
- ].sort().join(",")}`;
919
+ ];
1070
920
  const now = Date.now();
1071
921
  const existing = await this.readAlertRaw(alertId);
1072
922
  const alert = {
1073
923
  id: alertId,
1074
924
  category: "system.runner-check",
1075
- severity: "error",
925
+ severity: "critical",
1076
926
  title: "Runners degraded",
1077
- message: `${nodeId}: ${summary}`,
927
+ message: `${nodeId}: ${summary}${parts.length > 0 ? ` (${parts.join("; ")})` : ""}`,
928
+ action: ADDONS_PAGE_ACTION,
1078
929
  status: "active",
1079
930
  read: false,
1080
931
  createdAt: existing?.createdAt ?? now,
@@ -1087,19 +938,11 @@ var AlertCenterAddon = class extends require_dist.BaseAddon {
1087
938
  nodeId,
1088
939
  failed: event.data["failed"],
1089
940
  refused: event.data["refused"],
1090
- missing: event.data["missing"],
1091
- pushLine,
1092
- ...existing?.push !== void 0 ? { push: existing.push } : {}
941
+ missing: event.data["missing"]
1093
942
  }
1094
943
  };
1095
944
  await this.persistAlert(alert);
1096
945
  this.emitAlertCreatedEvent(alert);
1097
- this.queuePush({
1098
- alertId,
1099
- key: pushKey,
1100
- title: alert.title,
1101
- line: pushLine
1102
- });
1103
946
  }
1104
947
  async readAlertRaw(alertId) {
1105
948
  const store = this.alertStore;
@@ -1108,155 +951,19 @@ var AlertCenterAddon = class extends require_dist.BaseAddon {
1108
951
  if (raw === null) return null;
1109
952
  const rec = Object.fromEntries(Object.entries(raw));
1110
953
  const meta = rec["metadata"];
1111
- const push = typeof meta === "object" && meta !== null && !Array.isArray(meta) ? meta["push"] : void 0;
1112
954
  const metadata = typeof meta === "object" && meta !== null && !Array.isArray(meta) ? meta : void 0;
955
+ const parsed = require_dist.AlertSchema.safeParse({
956
+ id: alertId,
957
+ ...rec
958
+ });
1113
959
  return {
1114
960
  ...typeof rec["createdAt"] === "number" ? { createdAt: rec["createdAt"] } : {},
1115
961
  ...typeof rec["read"] === "boolean" ? { read: rec["read"] } : {},
1116
962
  ...typeof rec["status"] === "string" ? { status: rec["status"] } : {},
1117
- ...isPushState(push) ? { push } : {},
1118
- ...episodeKeyFromMetadata(metadata) !== void 0 ? { episodeKey: episodeKeyFromMetadata(metadata) } : {}
1119
- };
1120
- }
1121
- /** Pushes run one at a time so a burst can never send the same state twice. */
1122
- pushChain = Promise.resolve();
1123
- /** Findings waiting for the next digest, by alert id. */
1124
- pushQueue = /* @__PURE__ */ new Map();
1125
- pushTimer = null;
1126
- pushPolicy() {
1127
- return {
1128
- enabled: this.config.criticalPushEnabled,
1129
- targetIds: normalizeCriticalPushTargetIds(this.config.criticalPushTargetIds)
1130
- };
1131
- }
1132
- /** Rewrite a legacy CSV `criticalPushTargetIds` string into `string[]`. */
1133
- async migrateCriticalPushTargetIds() {
1134
- const raw = this.config.criticalPushTargetIds;
1135
- if (typeof raw !== "string") return;
1136
- try {
1137
- await this.updateGlobalSettings({ criticalPushTargetIds: normalizeCriticalPushTargetIds(raw) });
1138
- } catch (err) {
1139
- this.ctx.logger.warn("criticalPushTargetIds CSV migration failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
1140
- }
1141
- }
1142
- pushPorts() {
1143
- const api = this.ctx.api;
1144
- return {
1145
- listTargets: async () => {
1146
- return (await api.notificationOutput.listTargets.query({})).map((t) => {
1147
- const owner = t.config["ownerUserId"];
1148
- return {
1149
- id: t.id,
1150
- addonId: t.addonId,
1151
- enabled: t.enabled,
1152
- ...typeof owner === "string" && owner.length > 0 ? { ownerUserId: owner } : {}
1153
- };
1154
- });
1155
- },
1156
- send: async (target, message) => api.notificationOutput.send.mutate({
1157
- addonId: target.addonId,
1158
- targetId: target.id,
1159
- notification: {
1160
- title: message.title,
1161
- body: message.body,
1162
- format: "text",
1163
- priority: 5
1164
- }
1165
- })
963
+ ...episodeKeyFromMetadata(metadata) !== void 0 ? { episodeKey: episodeKeyFromMetadata(metadata) } : {},
964
+ ...parsed.success ? { episode: episodeViewOf(parsed.data) } : {}
1166
965
  };
1167
966
  }
1168
- /**
1169
- * Queue a critical finding for the next digest. A burst (every runner of a boot)
1170
- * arrives within the coalesce window and leaves as ONE message per target.
1171
- */
1172
- queuePush(item) {
1173
- this.pushQueue.set(item.alertId, item);
1174
- if (this.pushTimer !== null) return;
1175
- this.pushTimer = setTimeout(() => {
1176
- this.pushTimer = null;
1177
- this.flushPushes();
1178
- }, this.pushCoalesceMs);
1179
- this.pushTimer.unref?.();
1180
- }
1181
- /** Send the queued findings now (also the reconcile path). */
1182
- flushPushes() {
1183
- this.pushChain = this.pushChain.then(async () => {
1184
- const queued = [...this.pushQueue.values()];
1185
- this.pushQueue.clear();
1186
- if (queued.length === 0) return;
1187
- try {
1188
- const items = [];
1189
- for (const q of queued) {
1190
- const existing = await this.readAlertRaw(q.alertId);
1191
- items.push({
1192
- ...q,
1193
- previous: existing?.push ?? null
1194
- });
1195
- }
1196
- const states = await deliverDigest({
1197
- items,
1198
- policy: this.pushPolicy(),
1199
- ports: this.pushPorts(),
1200
- now: Date.now()
1201
- });
1202
- for (const [alertId, state] of states) {
1203
- await this.storePushState(alertId, state);
1204
- this.ctx.logger?.info("critical alert push", { meta: {
1205
- alertId,
1206
- state: state.state,
1207
- attempts: state.attempts,
1208
- error: state.lastError
1209
- } });
1210
- }
1211
- } catch (err) {
1212
- this.ctx.logger?.warn("critical alert push failed unexpectedly", { meta: { error: require_dist.errMsg(err) } });
1213
- }
1214
- });
1215
- return this.pushChain;
1216
- }
1217
- async storePushState(alertId, push) {
1218
- const store = this.alertStore;
1219
- if (store === null) return;
1220
- const raw = await store.getData(alertId);
1221
- if (raw === null) return;
1222
- const meta = raw["metadata"];
1223
- const metadata = typeof meta === "object" && meta !== null && !Array.isArray(meta) ? meta : {};
1224
- await store.mergeUpdate(alertId, { metadata: {
1225
- ...metadata,
1226
- push
1227
- } });
1228
- }
1229
- /**
1230
- * Retry every critical alert whose push is still pending - at boot and when
1231
- * the notifiers or notification-center start. An alert whose state was already
1232
- * delivered is skipped by its key, so reconciling never sends twice.
1233
- */
1234
- async reconcilePendingPushes() {
1235
- const store = this.alertStore;
1236
- if (store === null) return;
1237
- try {
1238
- const rows = await store.listRecent(500);
1239
- for (const row of rows) {
1240
- const parsed = require_dist.AlertSchema.safeParse({
1241
- id: row.id,
1242
- ...row.data
1243
- });
1244
- if (!parsed.success || parsed.data.status !== "active") continue;
1245
- if (!isCriticalPush(parsed.data)) continue;
1246
- const push = parsed.data.metadata?.["push"];
1247
- if (!isPushState(push) || push.state !== "pending") continue;
1248
- this.pushQueue.set(parsed.data.id, {
1249
- alertId: parsed.data.id,
1250
- key: push.key,
1251
- title: parsed.data.title,
1252
- line: String(parsed.data.metadata?.["pushLine"] ?? parsed.data.title)
1253
- });
1254
- }
1255
- await this.flushPushes();
1256
- } catch (err) {
1257
- this.ctx.logger?.warn("pending critical push reconcile failed", { meta: { error: require_dist.errMsg(err) } });
1258
- }
1259
- }
1260
967
  async handleModelDownload(event) {
1261
968
  const data = event.data;
1262
969
  const modelId = data.modelId ?? "unknown";
@@ -1589,7 +1296,7 @@ var AlertCenterAddon = class extends require_dist.BaseAddon {
1589
1296
  }
1590
1297
  async emitAlert(alert) {
1591
1298
  const existingRaw = await this.readAlertRaw(alert.id);
1592
- const { alert: toStore, queuePush } = mergeEmitAlert({
1299
+ const toStore = mergeEmitAlert({
1593
1300
  existing: existingRaw === null ? null : {
1594
1301
  read: existingRaw.read,
1595
1302
  episodeKey: existingRaw.episodeKey,
@@ -1597,20 +1304,10 @@ var AlertCenterAddon = class extends require_dist.BaseAddon {
1597
1304
  },
1598
1305
  incoming: alert
1599
1306
  });
1600
- const stored = alertWithRetainedPush(toStore, existingRaw?.push, existingRaw?.status);
1601
1307
  const isNew = existingRaw === null;
1602
- await this.persistAlert(stored);
1308
+ const stored = await this.persistAlert(toStore);
1603
1309
  if (isNew) this.emitAlertCreatedEvent(stored);
1604
1310
  else this.emitAlertUpdatedEvent(alert.id, stored);
1605
- if (queuePush) {
1606
- const { key, line } = criticalPushPayload(stored);
1607
- this.queuePush({
1608
- alertId: stored.id,
1609
- key,
1610
- title: stored.title,
1611
- line
1612
- });
1613
- }
1614
1311
  await this.enforceMaxAlerts();
1615
1312
  await this.enforceRetention();
1616
1313
  }
@@ -1625,15 +1322,73 @@ var AlertCenterAddon = class extends require_dist.BaseAddon {
1625
1322
  * Read-merge-write: SettingsBackend.update() replaces the entire `data` blob,
1626
1323
  * so we must read the existing record, merge the patch, and write back.
1627
1324
  */
1628
- async mergeUpdate(id, patch) {
1325
+ /** Alert writes (stamp + store + announce) from any caller, one at a time. */
1326
+ writeChain = Promise.resolve();
1327
+ serializedWrite(write) {
1328
+ const run = this.writeChain.then(write, write);
1329
+ this.writeChain = run.catch(() => void 0);
1330
+ return run;
1331
+ }
1332
+ mergeUpdate(id, patch) {
1333
+ return this.serializedWrite(() => this.mergeUpdateNow(id, patch));
1334
+ }
1335
+ async mergeUpdateNow(id, patch) {
1629
1336
  const store = this.alertStore;
1630
1337
  if (store === null) return;
1631
- await store.mergeUpdate(id, patch);
1338
+ const previous = (await this.readAlertRaw(id))?.episode ?? null;
1339
+ const metadata = patch["metadata"];
1340
+ const keepEpisode = previous !== null && typeof metadata === "object" && metadata !== null ? { metadata: {
1341
+ ...metadata,
1342
+ episodeId: previous.episodeId
1343
+ } } : {};
1344
+ await store.mergeUpdate(id, {
1345
+ ...patch,
1346
+ ...keepEpisode
1347
+ });
1348
+ const after = await store.getData(id);
1349
+ const parsed = after === null ? null : require_dist.AlertSchema.safeParse({
1350
+ id,
1351
+ ...after
1352
+ });
1353
+ if (parsed?.success === true) this.announceEpisode(previous, parsed.data);
1632
1354
  }
1633
- async persistAlert(alert) {
1355
+ /**
1356
+ * Store an alert, stamping its episode identity. The stored alert is what a
1357
+ * caller must use afterwards, and the opening or closing of a critical/error
1358
+ * episode is announced here, in the one place every write passes through.
1359
+ */
1360
+ persistAlert(alert) {
1361
+ return this.serializedWrite(() => this.persistAlertNow(alert));
1362
+ }
1363
+ async persistAlertNow(alert) {
1634
1364
  const store = this.alertStore;
1635
- if (store === null) return;
1636
- await store.persist(alert);
1365
+ if (store === null) return alert;
1366
+ const previous = (await this.readAlertRaw(alert.id))?.episode ?? null;
1367
+ const episodeId = nextEpisodeId({
1368
+ alertId: alert.id,
1369
+ previous,
1370
+ incomingKey: episodeKeyFromMetadata(alert.metadata),
1371
+ now: Date.now()
1372
+ });
1373
+ const stamped = {
1374
+ ...alert,
1375
+ metadata: {
1376
+ ...alert.metadata,
1377
+ episodeId
1378
+ }
1379
+ };
1380
+ await store.persist(stamped);
1381
+ this.announceEpisode(previous, stamped);
1382
+ return stamped;
1383
+ }
1384
+ /**
1385
+ * Announce an episode edge on the bus. Telemetry, and it may be dropped
1386
+ * (D8): Notification Center reconciles from the alerts store (D11, D49).
1387
+ */
1388
+ announceEpisode(previous, alert) {
1389
+ const edge = episodeEdge(previous, episodeViewOf(alert));
1390
+ if (edge === null) return;
1391
+ this.ctx.eventBus?.emit(require_dist.createEvent(require_dist.EventCategory.AlertEpisode, EVENT_SOURCE, require_dist.alertEpisodePayload(alert, edge, Date.now())));
1637
1392
  }
1638
1393
  emitAlertCreatedEvent(alert) {
1639
1394
  this.ctx.eventBus?.emit(require_dist.createEvent("alert.created", EVENT_SOURCE, {