@camstack/system 1.2.452 → 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 (96) hide show
  1. package/dist/{agent-https-CMz4S56_.js → agent-https-B7IjRRzT.js} +2 -2
  2. package/dist/{agent-https-D6lRxHma.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 +15 -2
  44. package/dist/builtins/native-metrics/native-metrics.addon.mjs +15 -2
  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-BQmhc3se.js → composition-sources-Cukc78eu.js} +1 -1
  75. package/dist/{composition-sources-DzuruDVY.mjs → composition-sources-bt6IQDLu.mjs} +1 -1
  76. package/dist/{constants-BPWA-r-Y.js → constants-DBGG6Zu5.js} +1 -1
  77. package/dist/{constants-gzGZbnwO.mjs → constants-DQcVrhVI.mjs} +1 -1
  78. package/dist/{device-ca-75rt3U_U.js → device-ca-B3hUVfYk.js} +1 -1
  79. package/dist/{device-ca-D05nqNXE.mjs → device-ca-Utfvsx5x.mjs} +1 -1
  80. package/dist/{dist-DV2ll9Hi.mjs → dist-7ZciHCIp.mjs} +394 -38
  81. package/dist/{dist-Ct88catN.js → dist-Bi4Ioxop.js} +417 -37
  82. package/dist/file-host-worker.js +1 -1
  83. package/dist/file-host-worker.mjs +1 -1
  84. package/dist/{hub-mdns-ciao-DAU1WttG.js → hub-mdns-ciao-2FzLWfbD.js} +2 -2
  85. package/dist/{hub-mdns-ciao-BPOg2iyc.mjs → hub-mdns-ciao-BRs3DUT-.mjs} +2 -2
  86. package/dist/index.js +17 -9
  87. package/dist/index.mjs +17 -9
  88. package/dist/kernel/cap-router-builder.d.ts +9 -0
  89. package/dist/{phase6-declare-gate-BJplW6tX.mjs → phase6-declare-gate-D-2LMw9o.mjs} +1 -1
  90. package/dist/{phase6-declare-gate-C6WqagkC.js → phase6-declare-gate-DwYAep7y.js} +1 -1
  91. package/dist/{restore-archive-Cprdh3P9.js → restore-archive-C0ymYQk7.js} +1 -1
  92. package/dist/{restore-archive-Jiz63mVB.mjs → restore-archive-UVZv_hmr.mjs} +1 -1
  93. package/dist/{wal-maintenance-BspX29jz.js → wal-maintenance-Bo_lo3Dh.js} +0 -0
  94. package/dist/{wal-maintenance-BNIaf3QP.mjs → wal-maintenance-Df89xxrK.mjs} +0 -0
  95. package/package.json +1 -1
  96. package/dist/builtins/alerts/critical-push.d.ts +0 -90
@@ -1,111 +1,47 @@
1
- import { Hr as BaseAddon, Mi as defineRecordKinds, Oi as createEvent, Vr as errMsg, i as AlertSchema, ma as EventCategory, pt as alertsCapability } from "../../dist-DV2ll9Hi.mjs";
1
+ import { Gr as errMsg, Ii as defineRecordKinds, Kr as BaseAddon, Mi as createEvent, fn as isAlertEpisodeOpen, ht as alertsCapability, i as AlertSchema, mt as alertEpisodePayload, pn as isAlertEpisodeSeverity, pt as alertEpisodeId, va as EventCategory } from "../../dist-7ZciHCIp.mjs";
2
2
  import { t as require_read_all_rows } from "../../read-all-rows-TaPqdWNF.mjs";
3
- import { t as resolvePhase6UseRecordsPathForHub } from "../../phase6-declare-gate-BJplW6tX.mjs";
4
- //#region src/builtins/alerts/critical-push.ts
5
- /** Alert categories that are critical system findings. */
6
- var CRITICAL_CATEGORIES = new Set([
7
- "addon.runner-failed",
8
- "addon.runner-refused",
9
- "system.runner-check"
10
- ]);
11
- function isCriticalPush(alert) {
12
- return alert.severity === "critical" || CRITICAL_CATEGORIES.has(alert.category);
13
- }
14
- function isPushState(value) {
15
- if (typeof value !== "object" || value === null) return false;
16
- const rec = value;
17
- return (rec["state"] === "pending" || rec["state"] === "delivered" || rec["state"] === "disabled") && typeof rec["key"] === "string" && typeof rec["attempts"] === "number";
18
- }
19
- function selectPushTargets(targets, policy) {
20
- const enabled = targets.filter((t) => t.enabled);
21
- if (policy.targetIds.length > 0) return enabled.filter((t) => policy.targetIds.includes(t.id));
22
- return enabled.filter((t) => t.ownerUserId === void 0);
23
- }
24
- var MAX_DIGEST_LINES = 20;
25
- /** The one message a target gets for a set of findings. */
26
- function composeDigest(items) {
27
- const first = items[0];
28
- if (items.length === 1 && first !== void 0) return {
29
- title: first.title,
30
- body: first.line
31
- };
32
- const shown = items.slice(0, MAX_DIGEST_LINES).map((i) => i.line);
33
- const more = items.length - shown.length;
34
- return {
35
- title: `${String(items.length)} addons need attention`,
36
- body: [...shown, ...more > 0 ? [`... and ${String(more)} more`] : []].join("\n")
37
- };
38
- }
3
+ import { t as resolvePhase6UseRecordsPathForHub } from "../../phase6-declare-gate-D-2LMw9o.mjs";
4
+ //#region src/builtins/alerts/alert-episode-policy.ts
39
5
  /**
40
- * Push every item that still needs it, as ONE message per target. Returns the new
41
- * state of each item whose state changed (an item already delivered/disabled for
42
- * its key is absent from the result).
6
+ * When does an alert open a new EPISODE, and which edge of it is announced?
7
+ *
8
+ * The alert centre sends no notification. It stamps every alert it stores with
9
+ * an `episodeId` (`metadata.episodeId`) and announces the opening and the
10
+ * closing of critical and error episodes on the bus; Notification Center turns
11
+ * those into notifications (`alert-critical`, `alert-error`,
12
+ * `alert-critical-resolved`). The identity is what makes a notification
13
+ * once-per-episode: any number of re-emits of an open alert keep the id, a new
14
+ * episode after completion gets a new one.
43
15
  */
44
- async function deliverDigest(input) {
45
- const out = /* @__PURE__ */ new Map();
46
- const work = input.items.filter((i) => !(i.previous !== null && i.previous.key === i.key && i.previous.state !== "pending"));
47
- if (work.length === 0) return out;
48
- const sameKey = (i) => i.previous !== null && i.previous.key === i.key ? i.previous : null;
49
- const attemptsOf = (i) => (sameKey(i)?.attempts ?? 0) + 1;
50
- const sentOf = (i) => sameKey(i)?.sentTo ?? [];
51
- if (!input.policy.enabled) {
52
- for (const i of work) out.set(i.alertId, {
53
- state: "disabled",
54
- key: i.key,
55
- attempts: attemptsOf(i)
56
- });
57
- return out;
58
- }
59
- const fail = (error) => {
60
- for (const i of work) out.set(i.alertId, {
61
- state: "pending",
62
- key: i.key,
63
- attempts: attemptsOf(i),
64
- lastError: error,
65
- ...sentOf(i).length > 0 ? { sentTo: sentOf(i) } : {}
66
- });
67
- return out;
68
- };
69
- let targets;
70
- try {
71
- targets = selectPushTargets(await input.ports.listTargets(), input.policy);
72
- } catch (err) {
73
- return fail(`notifiers unavailable: ${err instanceof Error ? err.message : String(err)}`);
74
- }
75
- if (targets.length === 0) return fail("no eligible notifier target");
76
- const sent = new Map(work.map((i) => [i.alertId, [...sentOf(i)]]));
77
- const errors = new Map(work.map((i) => [i.alertId, []]));
78
- for (const target of targets) {
79
- const todo = work.filter((i) => !(sent.get(i.alertId) ?? []).includes(target.id));
80
- if (todo.length === 0) continue;
81
- let failure = null;
82
- try {
83
- const result = await input.ports.send(target, composeDigest(todo));
84
- if (!result.success) failure = `${target.id}: ${result.error ?? "send failed"}`;
85
- } catch (err) {
86
- failure = `${target.id}: ${err instanceof Error ? err.message : String(err)}`;
87
- }
88
- for (const i of todo) if (failure === null) sent.get(i.alertId)?.push(target.id);
89
- else errors.get(i.alertId)?.push(failure);
90
- }
91
- for (const i of work) {
92
- const sentTo = sent.get(i.alertId) ?? [];
93
- const complete = targets.every((t) => sentTo.includes(t.id));
94
- out.set(i.alertId, complete ? {
95
- state: "delivered",
96
- key: i.key,
97
- at: input.now,
98
- attempts: attemptsOf(i),
99
- sentTo
100
- } : {
101
- state: "pending",
102
- key: i.key,
103
- attempts: attemptsOf(i),
104
- lastError: (errors.get(i.alertId) ?? []).join("; "),
105
- ...sentTo.length > 0 ? { sentTo } : {}
106
- });
16
+ /**
17
+ * The previous episode continues while its alert is still open and the emitter
18
+ * did not mark a new one; anything else opens `<alertId>@<now>`.
19
+ */
20
+ function nextEpisodeId(input) {
21
+ const { previous } = input;
22
+ if (previous !== null && isAlertEpisodeOpen(previous.status)) {
23
+ if (input.incomingKey === void 0 || input.incomingKey === previous.episodeKey) return previous.episodeId;
107
24
  }
108
- return out;
25
+ const minted = `${input.alertId}@${String(input.now)}`;
26
+ return previous !== null && previous.episodeId === minted ? `${minted}+` : minted;
27
+ }
28
+ /**
29
+ * The edge to announce for a write that took `previous` to `next`, or `null`.
30
+ * A re-emit that changes nothing about the episode announces nothing; an
31
+ * escalation from `error` to `critical` inside one episode is announced again
32
+ * (it is a different kind, and the operator opted into it separately).
33
+ */
34
+ function episodeEdge(previous, next) {
35
+ const nextOpen = isAlertEpisodeOpen(next.status);
36
+ const continues = previous !== null && isAlertEpisodeOpen(previous.status) && previous.episodeId === next.episodeId;
37
+ if (nextOpen) {
38
+ if (!isAlertEpisodeSeverity(next.severity)) return null;
39
+ if (!continues || previous === null) return "raised";
40
+ if (!isAlertEpisodeSeverity(previous.severity)) return "raised";
41
+ return previous.severity === "error" && next.severity === "critical" ? "raised" : null;
42
+ }
43
+ if (previous !== null && isAlertEpisodeOpen(previous.status) && isAlertEpisodeSeverity(previous.severity)) return "resolved";
44
+ return null;
109
45
  }
110
46
  //#endregion
111
47
  //#region src/builtins/alerts/emit-alert-policy.ts
@@ -113,24 +49,9 @@ function episodeKeyFromMetadata(metadata) {
113
49
  const key = metadata?.["episodeKey"];
114
50
  return typeof key === "string" ? key : void 0;
115
51
  }
116
- /** Completed episodes start fresh — prior push must not suppress the next digest. */
117
- function shouldDropPriorPush(existingStatus) {
118
- return existingStatus === "completed";
119
- }
120
- function alertWithRetainedPush(toStore, existingPush, existingStatus) {
121
- if (existingPush === void 0 || shouldDropPriorPush(existingStatus)) return toStore;
122
- return {
123
- ...toStore,
124
- metadata: {
125
- ...toStore.metadata,
126
- push: existingPush
127
- }
128
- };
129
- }
130
52
  /**
131
- * Re-emit of the same alert id: preserve read when the episode is unchanged
132
- * and still active. A completed episode starts again. Queue a critical push
133
- * only for active, unread critical findings.
53
+ * Re-emit of the same alert id: preserve `read` when the episode is unchanged
54
+ * and still active. A completed episode starts again.
134
55
  */
135
56
  function mergeEmitAlert(input) {
136
57
  const incomingKey = episodeKeyFromMetadata(input.incoming.metadata);
@@ -139,26 +60,11 @@ function mergeEmitAlert(input) {
139
60
  const sameEpisode = incomingKey === void 0 || incomingKey === existingKey;
140
61
  const priorCompleted = input.existing.status === "completed";
141
62
  if (sameEpisode && !priorCompleted) return {
142
- alert: {
143
- ...input.incoming,
144
- read: true
145
- },
146
- queuePush: false
63
+ ...input.incoming,
64
+ read: true
147
65
  };
148
66
  }
149
- const queuePush = isCriticalPush(input.incoming) && input.incoming.status === "active" && input.incoming.read === false;
150
- return {
151
- alert: input.incoming,
152
- queuePush
153
- };
154
- }
155
- function criticalPushPayload(alert) {
156
- const key = episodeKeyFromMetadata(alert.metadata) ?? alert.id;
157
- const pushLine = alert.metadata?.["pushLine"];
158
- return {
159
- key,
160
- line: typeof pushLine === "string" ? pushLine : alert.title
161
- };
67
+ return input.incoming;
162
68
  }
163
69
  //#endregion
164
70
  //#region src/builtins/alerts/device-label.ts
@@ -292,12 +198,12 @@ function renderRunnerAlert(record) {
292
198
  const where = record.nodeId === "hub" ? record.runnerId : `${record.runnerId} on ${record.nodeId}`;
293
199
  switch (record.state) {
294
200
  case "refused": return {
295
- severity: "error",
201
+ severity: "critical",
296
202
  title: "Addon refused to start",
297
203
  message: `${where}: refused - ${record.reason ?? "no reason reported"}${record.remedy !== void 0 ? ` ${record.remedy}` : ""}`
298
204
  };
299
205
  case "failed": return {
300
- severity: "error",
206
+ severity: "critical",
301
207
  title: "Addon crash loop",
302
208
  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`
303
209
  };
@@ -308,32 +214,6 @@ function renderRunnerAlert(record) {
308
214
  };
309
215
  }
310
216
  }
311
- /** Is the record in a state an operator must act on (terminal)? */
312
- function isTerminalRunnerState(state) {
313
- return state === "failed" || state === "refused";
314
- }
315
- function hash(text) {
316
- let h = 5381;
317
- for (let i = 0; i < text.length; i++) h = (h << 5) + h + text.charCodeAt(i) | 0;
318
- return (h >>> 0).toString(36);
319
- }
320
- /**
321
- * The push key of a record: its episode and its cause. The same refusal of the
322
- * same episode is never pushed twice; a refusal with another reason, a breaker
323
- * trip, or any failure after a recovery (a new episode) is a new push.
324
- */
325
- function runnerPushKey(record) {
326
- const cause = record.state === "refused" ? hash(`${record.reason ?? ""}|${record.remedy ?? ""}`) : "breaker";
327
- return `runner:${record.state}:${String(record.since)}:${cause}`;
328
- }
329
- /**
330
- * The one line a push carries: runner, state and the remedy. Deliberately NOT
331
- * the raw reason or error text - those stay on the alert.
332
- */
333
- function runnerPushLine(record) {
334
- const where = record.nodeId === "hub" ? record.runnerId : `${record.runnerId} on ${record.nodeId}`;
335
- 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.`;
336
- }
337
217
  //#endregion
338
218
  //#region src/builtins/alerts/record-kinds.ts
339
219
  var import_read_all_rows = require_read_all_rows();
@@ -640,7 +520,6 @@ var CATEGORY_MAPPINGS = {
640
520
  * {@link CATEGORY_MAPPINGS}.
641
521
  */
642
522
  var SILENT_TRACKED_CATEGORIES = [
643
- "addon.started",
644
523
  "process.state-changed",
645
524
  "addon.load-recovered",
646
525
  "system.liveness-recovered",
@@ -709,24 +588,26 @@ function parseAlert(record) {
709
588
  }
710
589
  var DEFAULT_CONFIG = {
711
590
  maxAlerts: 500,
712
- retentionDays: 30,
713
- criticalPushEnabled: true,
714
- criticalPushTargetIds: []
591
+ retentionDays: 30
715
592
  };
716
- /** Accept the live `string[]` shape and the pre-multiselect CSV string. */
717
- function normalizeCriticalPushTargetIds(raw) {
718
- if (Array.isArray(raw)) return raw.filter((id) => typeof id === "string" && id.trim().length > 0).map((id) => id.trim());
719
- if (typeof raw === "string" && raw.trim().length > 0) return raw.split(",").map((id) => id.trim()).filter((id) => id.length > 0);
720
- return [];
721
- }
722
- /** One burst of critical findings (a whole boot of refused runners) is ONE digest push. */
723
- var PUSH_COALESCE_MS = 2e3;
724
- /** Addons whose (re)start is a reason to retry pending critical pushes. */
725
- var PUSH_RECONCILE_ADDONS = new Set(["notifiers", "notification-center"]);
726
593
  var EVENT_SOURCE = {
727
594
  type: "addon",
728
595
  id: "alert-center"
729
596
  };
597
+ /** Where the operator acts on a runner that failed, was refused or is missing. */
598
+ var ADDONS_PAGE_ACTION = {
599
+ label: "Open Addons",
600
+ href: "/system/addons"
601
+ };
602
+ function episodeViewOf(alert) {
603
+ const key = episodeKeyFromMetadata(alert.metadata);
604
+ return {
605
+ status: alert.status,
606
+ severity: alert.severity,
607
+ episodeId: alertEpisodeId(alert),
608
+ ...key !== void 0 ? { episodeKey: key } : {}
609
+ };
610
+ }
730
611
  /**
731
612
  * Alert Center addon — core builtin that persists alerts in a
732
613
  * structured SQLite table and serves them to the admin UI.
@@ -745,11 +626,8 @@ function toPrunable(record) {
745
626
  var AlertCenterAddon = class extends BaseAddon {
746
627
  alertStore = null;
747
628
  unsubscribers = [];
748
- /** How long a burst of critical findings is gathered into one digest push. */
749
- pushCoalesceMs;
750
- constructor(options) {
629
+ constructor() {
751
630
  super({ ...DEFAULT_CONFIG });
752
- this.pushCoalesceMs = options?.pushCoalesceMs ?? PUSH_COALESCE_MS;
753
631
  }
754
632
  /**
755
633
  * Per `(category, source.id)` counter used to rate-limit chatty
@@ -780,9 +658,7 @@ var AlertCenterAddon = class extends BaseAddon {
780
658
  unsubs.push(unsub);
781
659
  }
782
660
  this.unsubscribers = unsubs;
783
- this.reconcilePendingPushes();
784
661
  this.dismissLegacyStreamCards(void 0);
785
- this.migrateCriticalPushTargetIds();
786
662
  this.ctx.logger.info("Initialized", { meta: { trackedCategoriesCount: TRACKED_CATEGORIES.length } });
787
663
  return [{
788
664
  capability: alertsCapability,
@@ -801,46 +677,29 @@ var AlertCenterAddon = class extends BaseAddon {
801
677
  description: "Configure alert storage and retention.",
802
678
  descriptionKey: "server:globalSettings.s_alerts.description",
803
679
  columns: 1,
804
- fields: [
805
- this.field({
806
- type: "number",
807
- key: "maxAlerts",
808
- label: "Maximum stored alerts",
809
- labelKey: "server:globalSettings.s_alerts.maxAlerts.label",
810
- description: "Oldest read alerts are deleted when this limit is exceeded.",
811
- descriptionKey: "server:globalSettings.s_alerts.maxAlerts.description",
812
- min: 50,
813
- max: 5e3,
814
- step: 50,
815
- default: 500
816
- }),
817
- this.field({
818
- type: "number",
819
- key: "retentionDays",
820
- label: "Alert retention (days)",
821
- labelKey: "server:globalSettings.s_alerts.retentionDays.label",
822
- description: "Alerts older than this are automatically removed.",
823
- descriptionKey: "server:globalSettings.s_alerts.retentionDays.description",
824
- min: 1,
825
- max: 365,
826
- step: 1,
827
- default: 30
828
- }),
829
- this.field({
830
- type: "boolean",
831
- key: "criticalPushEnabled",
832
- label: "Push critical system alerts",
833
- 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.",
834
- default: true
835
- }),
836
- this.field({
837
- type: "notification-target-multiselect",
838
- key: "criticalPushTargetIds",
839
- label: "Critical push targets",
840
- description: "Notifier targets that receive critical system alerts. Empty = every enabled shared (not personal) target.",
841
- default: []
842
- })
843
- ]
680
+ fields: [this.field({
681
+ type: "number",
682
+ key: "maxAlerts",
683
+ label: "Maximum stored alerts",
684
+ labelKey: "server:globalSettings.s_alerts.maxAlerts.label",
685
+ description: "Oldest read alerts are deleted when this limit is exceeded.",
686
+ descriptionKey: "server:globalSettings.s_alerts.maxAlerts.description",
687
+ min: 50,
688
+ max: 5e3,
689
+ step: 50,
690
+ default: 500
691
+ }), this.field({
692
+ type: "number",
693
+ key: "retentionDays",
694
+ label: "Alert retention (days)",
695
+ labelKey: "server:globalSettings.s_alerts.retentionDays.label",
696
+ description: "Alerts older than this are automatically removed.",
697
+ descriptionKey: "server:globalSettings.s_alerts.retentionDays.description",
698
+ min: 1,
699
+ max: 365,
700
+ step: 1,
701
+ default: 30
702
+ })]
844
703
  }] });
845
704
  }
846
705
  async listAlerts(filter) {
@@ -879,7 +738,17 @@ var AlertCenterAddon = class extends BaseAddon {
879
738
  updatedAt: Date.now()
880
739
  });
881
740
  }
882
- async handleEvent(category, event) {
741
+ /**
742
+ * Events are handled one at a time: every handler is a read-modify-write of
743
+ * an alert, and two concurrent events about one runner would both read "no
744
+ * alert yet" and open two episodes for it.
745
+ */
746
+ eventChain = Promise.resolve();
747
+ handleEvent(category, event) {
748
+ this.eventChain = this.eventChain.then(() => this.processEvent(category, event));
749
+ return this.eventChain;
750
+ }
751
+ async processEvent(category, event) {
883
752
  try {
884
753
  if (category === "model.download.progress") {
885
754
  await this.handleModelDownload(event);
@@ -897,11 +766,6 @@ var AlertCenterAddon = class extends BaseAddon {
897
766
  await this.handleRunnerCheck(event);
898
767
  return;
899
768
  }
900
- if (category === EventCategory.AddonStarted) {
901
- const addonId = event.data["addonId"];
902
- if (typeof addonId === "string" && PUSH_RECONCILE_ADDONS.has(addonId)) await this.reconcilePendingPushes();
903
- return;
904
- }
905
769
  if (category === EventCategory.AddonLoadFailed) {
906
770
  await this.handleAddonLoadFailed(event);
907
771
  return;
@@ -1002,11 +866,8 @@ var AlertCenterAddon = class extends BaseAddon {
1002
866
  type: "runner",
1003
867
  id: record.runnerId
1004
868
  },
1005
- metadata: {
1006
- runner: record,
1007
- pushLine: runnerPushLine(record),
1008
- ...existing?.push !== void 0 ? { push: existing.push } : {}
1009
- }
869
+ action: ADDONS_PAGE_ACTION,
870
+ metadata: { runner: record }
1010
871
  };
1011
872
  await this.persistAlert(alert);
1012
873
  if (previous === null) this.emitAlertCreatedEvent(alert);
@@ -1015,17 +876,11 @@ var AlertCenterAddon = class extends BaseAddon {
1015
876
  title: alert.title
1016
877
  });
1017
878
  await this.enforceMaxAlerts();
1018
- if (isTerminalRunnerState(record.state)) this.queuePush({
1019
- alertId,
1020
- key: runnerPushKey(record),
1021
- title: rendered.title,
1022
- line: runnerPushLine(record)
1023
- });
1024
879
  }
1025
880
  /**
1026
881
  * A runner reached `running`: its episode is over. Close the runner alert so a
1027
- * later failure opens a NEW episode (new push key), instead of folding into a
1028
- * stale `refused` record that would never push again.
882
+ * later failure opens a NEW episode, instead of folding into a stale
883
+ * `refused` record whose notification was already sent.
1029
884
  */
1030
885
  async handleRunnerStateChanged(event) {
1031
886
  if (event.data["state"] !== "running") return;
@@ -1051,24 +906,20 @@ var AlertCenterAddon = class extends BaseAddon {
1051
906
  }) : [];
1052
907
  const failedIds = ids(event.data["failed"]);
1053
908
  const missingIds = ids(event.data["missing"]);
1054
- const pushLine = `${nodeId}: runners degraded - ${[
909
+ const parts = [
1055
910
  ...failedIds.length > 0 ? [`failed: ${failedIds.join(", ")}`] : [],
1056
911
  ...refusedIds.length > 0 ? [`refused: ${refusedIds.join(", ")}`] : [],
1057
912
  ...missingIds.length > 0 ? [`missing: ${missingIds.join(", ")}`] : []
1058
- ].join("; ")}. See the Addons page.`;
1059
- const pushKey = `check:${[
1060
- ...failedIds,
1061
- ...refusedIds,
1062
- ...missingIds
1063
- ].sort().join(",")}`;
913
+ ];
1064
914
  const now = Date.now();
1065
915
  const existing = await this.readAlertRaw(alertId);
1066
916
  const alert = {
1067
917
  id: alertId,
1068
918
  category: "system.runner-check",
1069
- severity: "error",
919
+ severity: "critical",
1070
920
  title: "Runners degraded",
1071
- message: `${nodeId}: ${summary}`,
921
+ message: `${nodeId}: ${summary}${parts.length > 0 ? ` (${parts.join("; ")})` : ""}`,
922
+ action: ADDONS_PAGE_ACTION,
1072
923
  status: "active",
1073
924
  read: false,
1074
925
  createdAt: existing?.createdAt ?? now,
@@ -1081,19 +932,11 @@ var AlertCenterAddon = class extends BaseAddon {
1081
932
  nodeId,
1082
933
  failed: event.data["failed"],
1083
934
  refused: event.data["refused"],
1084
- missing: event.data["missing"],
1085
- pushLine,
1086
- ...existing?.push !== void 0 ? { push: existing.push } : {}
935
+ missing: event.data["missing"]
1087
936
  }
1088
937
  };
1089
938
  await this.persistAlert(alert);
1090
939
  this.emitAlertCreatedEvent(alert);
1091
- this.queuePush({
1092
- alertId,
1093
- key: pushKey,
1094
- title: alert.title,
1095
- line: pushLine
1096
- });
1097
940
  }
1098
941
  async readAlertRaw(alertId) {
1099
942
  const store = this.alertStore;
@@ -1102,155 +945,19 @@ var AlertCenterAddon = class extends BaseAddon {
1102
945
  if (raw === null) return null;
1103
946
  const rec = Object.fromEntries(Object.entries(raw));
1104
947
  const meta = rec["metadata"];
1105
- const push = typeof meta === "object" && meta !== null && !Array.isArray(meta) ? meta["push"] : void 0;
1106
948
  const metadata = typeof meta === "object" && meta !== null && !Array.isArray(meta) ? meta : void 0;
949
+ const parsed = AlertSchema.safeParse({
950
+ id: alertId,
951
+ ...rec
952
+ });
1107
953
  return {
1108
954
  ...typeof rec["createdAt"] === "number" ? { createdAt: rec["createdAt"] } : {},
1109
955
  ...typeof rec["read"] === "boolean" ? { read: rec["read"] } : {},
1110
956
  ...typeof rec["status"] === "string" ? { status: rec["status"] } : {},
1111
- ...isPushState(push) ? { push } : {},
1112
- ...episodeKeyFromMetadata(metadata) !== void 0 ? { episodeKey: episodeKeyFromMetadata(metadata) } : {}
1113
- };
1114
- }
1115
- /** Pushes run one at a time so a burst can never send the same state twice. */
1116
- pushChain = Promise.resolve();
1117
- /** Findings waiting for the next digest, by alert id. */
1118
- pushQueue = /* @__PURE__ */ new Map();
1119
- pushTimer = null;
1120
- pushPolicy() {
1121
- return {
1122
- enabled: this.config.criticalPushEnabled,
1123
- targetIds: normalizeCriticalPushTargetIds(this.config.criticalPushTargetIds)
1124
- };
1125
- }
1126
- /** Rewrite a legacy CSV `criticalPushTargetIds` string into `string[]`. */
1127
- async migrateCriticalPushTargetIds() {
1128
- const raw = this.config.criticalPushTargetIds;
1129
- if (typeof raw !== "string") return;
1130
- try {
1131
- await this.updateGlobalSettings({ criticalPushTargetIds: normalizeCriticalPushTargetIds(raw) });
1132
- } catch (err) {
1133
- this.ctx.logger.warn("criticalPushTargetIds CSV migration failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
1134
- }
1135
- }
1136
- pushPorts() {
1137
- const api = this.ctx.api;
1138
- return {
1139
- listTargets: async () => {
1140
- return (await api.notificationOutput.listTargets.query({})).map((t) => {
1141
- const owner = t.config["ownerUserId"];
1142
- return {
1143
- id: t.id,
1144
- addonId: t.addonId,
1145
- enabled: t.enabled,
1146
- ...typeof owner === "string" && owner.length > 0 ? { ownerUserId: owner } : {}
1147
- };
1148
- });
1149
- },
1150
- send: async (target, message) => api.notificationOutput.send.mutate({
1151
- addonId: target.addonId,
1152
- targetId: target.id,
1153
- notification: {
1154
- title: message.title,
1155
- body: message.body,
1156
- format: "text",
1157
- priority: 5
1158
- }
1159
- })
957
+ ...episodeKeyFromMetadata(metadata) !== void 0 ? { episodeKey: episodeKeyFromMetadata(metadata) } : {},
958
+ ...parsed.success ? { episode: episodeViewOf(parsed.data) } : {}
1160
959
  };
1161
960
  }
1162
- /**
1163
- * Queue a critical finding for the next digest. A burst (every runner of a boot)
1164
- * arrives within the coalesce window and leaves as ONE message per target.
1165
- */
1166
- queuePush(item) {
1167
- this.pushQueue.set(item.alertId, item);
1168
- if (this.pushTimer !== null) return;
1169
- this.pushTimer = setTimeout(() => {
1170
- this.pushTimer = null;
1171
- this.flushPushes();
1172
- }, this.pushCoalesceMs);
1173
- this.pushTimer.unref?.();
1174
- }
1175
- /** Send the queued findings now (also the reconcile path). */
1176
- flushPushes() {
1177
- this.pushChain = this.pushChain.then(async () => {
1178
- const queued = [...this.pushQueue.values()];
1179
- this.pushQueue.clear();
1180
- if (queued.length === 0) return;
1181
- try {
1182
- const items = [];
1183
- for (const q of queued) {
1184
- const existing = await this.readAlertRaw(q.alertId);
1185
- items.push({
1186
- ...q,
1187
- previous: existing?.push ?? null
1188
- });
1189
- }
1190
- const states = await deliverDigest({
1191
- items,
1192
- policy: this.pushPolicy(),
1193
- ports: this.pushPorts(),
1194
- now: Date.now()
1195
- });
1196
- for (const [alertId, state] of states) {
1197
- await this.storePushState(alertId, state);
1198
- this.ctx.logger?.info("critical alert push", { meta: {
1199
- alertId,
1200
- state: state.state,
1201
- attempts: state.attempts,
1202
- error: state.lastError
1203
- } });
1204
- }
1205
- } catch (err) {
1206
- this.ctx.logger?.warn("critical alert push failed unexpectedly", { meta: { error: errMsg(err) } });
1207
- }
1208
- });
1209
- return this.pushChain;
1210
- }
1211
- async storePushState(alertId, push) {
1212
- const store = this.alertStore;
1213
- if (store === null) return;
1214
- const raw = await store.getData(alertId);
1215
- if (raw === null) return;
1216
- const meta = raw["metadata"];
1217
- const metadata = typeof meta === "object" && meta !== null && !Array.isArray(meta) ? meta : {};
1218
- await store.mergeUpdate(alertId, { metadata: {
1219
- ...metadata,
1220
- push
1221
- } });
1222
- }
1223
- /**
1224
- * Retry every critical alert whose push is still pending - at boot and when
1225
- * the notifiers or notification-center start. An alert whose state was already
1226
- * delivered is skipped by its key, so reconciling never sends twice.
1227
- */
1228
- async reconcilePendingPushes() {
1229
- const store = this.alertStore;
1230
- if (store === null) return;
1231
- try {
1232
- const rows = await store.listRecent(500);
1233
- for (const row of rows) {
1234
- const parsed = AlertSchema.safeParse({
1235
- id: row.id,
1236
- ...row.data
1237
- });
1238
- if (!parsed.success || parsed.data.status !== "active") continue;
1239
- if (!isCriticalPush(parsed.data)) continue;
1240
- const push = parsed.data.metadata?.["push"];
1241
- if (!isPushState(push) || push.state !== "pending") continue;
1242
- this.pushQueue.set(parsed.data.id, {
1243
- alertId: parsed.data.id,
1244
- key: push.key,
1245
- title: parsed.data.title,
1246
- line: String(parsed.data.metadata?.["pushLine"] ?? parsed.data.title)
1247
- });
1248
- }
1249
- await this.flushPushes();
1250
- } catch (err) {
1251
- this.ctx.logger?.warn("pending critical push reconcile failed", { meta: { error: errMsg(err) } });
1252
- }
1253
- }
1254
961
  async handleModelDownload(event) {
1255
962
  const data = event.data;
1256
963
  const modelId = data.modelId ?? "unknown";
@@ -1583,7 +1290,7 @@ var AlertCenterAddon = class extends BaseAddon {
1583
1290
  }
1584
1291
  async emitAlert(alert) {
1585
1292
  const existingRaw = await this.readAlertRaw(alert.id);
1586
- const { alert: toStore, queuePush } = mergeEmitAlert({
1293
+ const toStore = mergeEmitAlert({
1587
1294
  existing: existingRaw === null ? null : {
1588
1295
  read: existingRaw.read,
1589
1296
  episodeKey: existingRaw.episodeKey,
@@ -1591,20 +1298,10 @@ var AlertCenterAddon = class extends BaseAddon {
1591
1298
  },
1592
1299
  incoming: alert
1593
1300
  });
1594
- const stored = alertWithRetainedPush(toStore, existingRaw?.push, existingRaw?.status);
1595
1301
  const isNew = existingRaw === null;
1596
- await this.persistAlert(stored);
1302
+ const stored = await this.persistAlert(toStore);
1597
1303
  if (isNew) this.emitAlertCreatedEvent(stored);
1598
1304
  else this.emitAlertUpdatedEvent(alert.id, stored);
1599
- if (queuePush) {
1600
- const { key, line } = criticalPushPayload(stored);
1601
- this.queuePush({
1602
- alertId: stored.id,
1603
- key,
1604
- title: stored.title,
1605
- line
1606
- });
1607
- }
1608
1305
  await this.enforceMaxAlerts();
1609
1306
  await this.enforceRetention();
1610
1307
  }
@@ -1619,15 +1316,73 @@ var AlertCenterAddon = class extends BaseAddon {
1619
1316
  * Read-merge-write: SettingsBackend.update() replaces the entire `data` blob,
1620
1317
  * so we must read the existing record, merge the patch, and write back.
1621
1318
  */
1622
- async mergeUpdate(id, patch) {
1319
+ /** Alert writes (stamp + store + announce) from any caller, one at a time. */
1320
+ writeChain = Promise.resolve();
1321
+ serializedWrite(write) {
1322
+ const run = this.writeChain.then(write, write);
1323
+ this.writeChain = run.catch(() => void 0);
1324
+ return run;
1325
+ }
1326
+ mergeUpdate(id, patch) {
1327
+ return this.serializedWrite(() => this.mergeUpdateNow(id, patch));
1328
+ }
1329
+ async mergeUpdateNow(id, patch) {
1623
1330
  const store = this.alertStore;
1624
1331
  if (store === null) return;
1625
- await store.mergeUpdate(id, patch);
1332
+ const previous = (await this.readAlertRaw(id))?.episode ?? null;
1333
+ const metadata = patch["metadata"];
1334
+ const keepEpisode = previous !== null && typeof metadata === "object" && metadata !== null ? { metadata: {
1335
+ ...metadata,
1336
+ episodeId: previous.episodeId
1337
+ } } : {};
1338
+ await store.mergeUpdate(id, {
1339
+ ...patch,
1340
+ ...keepEpisode
1341
+ });
1342
+ const after = await store.getData(id);
1343
+ const parsed = after === null ? null : AlertSchema.safeParse({
1344
+ id,
1345
+ ...after
1346
+ });
1347
+ if (parsed?.success === true) this.announceEpisode(previous, parsed.data);
1626
1348
  }
1627
- async persistAlert(alert) {
1349
+ /**
1350
+ * Store an alert, stamping its episode identity. The stored alert is what a
1351
+ * caller must use afterwards, and the opening or closing of a critical/error
1352
+ * episode is announced here, in the one place every write passes through.
1353
+ */
1354
+ persistAlert(alert) {
1355
+ return this.serializedWrite(() => this.persistAlertNow(alert));
1356
+ }
1357
+ async persistAlertNow(alert) {
1628
1358
  const store = this.alertStore;
1629
- if (store === null) return;
1630
- await store.persist(alert);
1359
+ if (store === null) return alert;
1360
+ const previous = (await this.readAlertRaw(alert.id))?.episode ?? null;
1361
+ const episodeId = nextEpisodeId({
1362
+ alertId: alert.id,
1363
+ previous,
1364
+ incomingKey: episodeKeyFromMetadata(alert.metadata),
1365
+ now: Date.now()
1366
+ });
1367
+ const stamped = {
1368
+ ...alert,
1369
+ metadata: {
1370
+ ...alert.metadata,
1371
+ episodeId
1372
+ }
1373
+ };
1374
+ await store.persist(stamped);
1375
+ this.announceEpisode(previous, stamped);
1376
+ return stamped;
1377
+ }
1378
+ /**
1379
+ * Announce an episode edge on the bus. Telemetry, and it may be dropped
1380
+ * (D8): Notification Center reconciles from the alerts store (D11, D49).
1381
+ */
1382
+ announceEpisode(previous, alert) {
1383
+ const edge = episodeEdge(previous, episodeViewOf(alert));
1384
+ if (edge === null) return;
1385
+ this.ctx.eventBus?.emit(createEvent(EventCategory.AlertEpisode, EVENT_SOURCE, alertEpisodePayload(alert, edge, Date.now())));
1631
1386
  }
1632
1387
  emitAlertCreatedEvent(alert) {
1633
1388
  this.ctx.eventBus?.emit(createEvent("alert.created", EVENT_SOURCE, {