@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.
- package/dist/{agent-https-DL2cFOQv.js → agent-https-B7IjRRzT.js} +2 -2
- package/dist/{agent-https-XAZFJjOS.mjs → agent-https-oXsR24R2.mjs} +2 -2
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alert-episode-policy.d.ts +39 -0
- package/dist/builtins/alerts/alerts.addon.d.ts +25 -38
- package/dist/builtins/alerts/alerts.addon.js +178 -423
- package/dist/builtins/alerts/alerts.addon.mjs +178 -423
- package/dist/builtins/alerts/emit-alert-policy.d.ts +3 -16
- package/dist/builtins/alerts/runner-alert.d.ts +0 -13
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-history/device-history.addon.js +1 -1
- package/dist/builtins/device-history/device-history.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +4 -4
- package/dist/builtins/keychain/index.mjs +4 -4
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +3 -3
- package/dist/builtins/local-auth/local-auth.addon.mjs +3 -3
- package/dist/builtins/local-network/local-network.addon.js +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/record-entries/index.js +22 -2
- package/dist/builtins/record-entries/index.mjs +22 -2
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/records/records-engine.d.ts +13 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
- package/dist/builtins/storage-orchestrator/location-class-paths.d.ts +13 -0
- package/dist/builtins/storage-orchestrator/location-repoint-move.d.ts +6 -1
- package/dist/builtins/storage-orchestrator/location-repointer.d.ts +97 -0
- package/dist/builtins/storage-orchestrator/location-store.d.ts +13 -2
- package/dist/builtins/storage-orchestrator/record-kinds.d.ts +5 -0
- package/dist/builtins/storage-orchestrator/storage-location-band-monitor.d.ts +38 -4
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +8 -2
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +486 -143
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +486 -143
- package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +26 -1
- package/dist/builtins/system-config/system-config.addon.js +3 -3
- package/dist/builtins/system-config/system-config.addon.mjs +3 -3
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{composition-sources-DwEvAg5Z.js → composition-sources-Cukc78eu.js} +1 -1
- package/dist/{composition-sources-jYNpYEhS.mjs → composition-sources-bt6IQDLu.mjs} +1 -1
- package/dist/{constants-B3fDUnJt.js → constants-DBGG6Zu5.js} +1 -1
- package/dist/{constants-DGBB3tNi.mjs → constants-DQcVrhVI.mjs} +1 -1
- package/dist/{device-ca-njpTMpJC.js → device-ca-B3hUVfYk.js} +1 -1
- package/dist/{device-ca-Dpkxg7px.mjs → device-ca-Utfvsx5x.mjs} +1 -1
- package/dist/{dist-B9C2v4SG.mjs → dist-7ZciHCIp.mjs} +315 -44
- package/dist/{dist-DyjNaeIj.js → dist-Bi4Ioxop.js} +338 -43
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/{hub-mdns-ciao-DpdFaLz-.js → hub-mdns-ciao-2FzLWfbD.js} +2 -2
- package/dist/{hub-mdns-ciao-B4wHDMvk.mjs → hub-mdns-ciao-BRs3DUT-.mjs} +2 -2
- package/dist/index.js +7 -7
- package/dist/index.mjs +7 -7
- package/dist/{phase6-declare-gate-CNxKbsvm.mjs → phase6-declare-gate-D-2LMw9o.mjs} +1 -1
- package/dist/{phase6-declare-gate-BlTDMI3t.js → phase6-declare-gate-DwYAep7y.js} +1 -1
- package/dist/{restore-archive-BeGP9IYs.js → restore-archive-C0ymYQk7.js} +1 -1
- package/dist/{restore-archive-D7HwsszI.mjs → restore-archive-UVZv_hmr.mjs} +1 -1
- package/dist/{wal-maintenance-i3GRvmQW.js → wal-maintenance-Bo_lo3Dh.js} +0 -0
- package/dist/{wal-maintenance-HEmw2Fik.mjs → wal-maintenance-Df89xxrK.mjs} +0 -0
- package/package.json +1 -1
- package/dist/builtins/alerts/critical-push.d.ts +0 -90
|
@@ -1,111 +1,47 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
4
|
-
//#region src/builtins/alerts/
|
|
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
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
read: true
|
|
145
|
-
},
|
|
146
|
-
queuePush: false
|
|
63
|
+
...input.incoming,
|
|
64
|
+
read: true
|
|
147
65
|
};
|
|
148
66
|
}
|
|
149
|
-
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1006
|
-
|
|
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
|
|
1028
|
-
*
|
|
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
|
|
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
|
-
]
|
|
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: "
|
|
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
|
-
...
|
|
1112
|
-
...
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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, {
|