@camstack/system 1.2.453 → 1.2.455
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-CCvlRQKV.js} +2 -2
- package/dist/{agent-https-XAZFJjOS.mjs → agent-https-CLWrOO3o.mjs} +2 -2
- package/dist/auth/auth-audit-log.d.ts +1 -1
- package/dist/auth/auth-manager.d.ts +7 -1
- 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 +14 -4
- package/dist/builtins/local-auth/local-auth.addon.mjs +14 -4
- 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-BxUkLNSo.js} +1 -1
- package/dist/{composition-sources-jYNpYEhS.mjs → composition-sources-CSzD5_qX.mjs} +1 -1
- package/dist/{constants-DGBB3tNi.mjs → constants-DOlYt1t1.mjs} +1 -1
- package/dist/{constants-B3fDUnJt.js → constants-DruwCkfp.js} +1 -1
- package/dist/{device-ca-njpTMpJC.js → device-ca-QPBgfqVY.js} +1 -1
- package/dist/{device-ca-Dpkxg7px.mjs → device-ca-uFbTyY_o.mjs} +1 -1
- package/dist/{dist-B9C2v4SG.mjs → dist-BpSENkRJ.mjs} +550 -240
- package/dist/{dist-DyjNaeIj.js → dist-DjQ8C-ku.js} +579 -239
- 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-Bkrvgnma.js} +2 -2
- package/dist/{hub-mdns-ciao-B4wHDMvk.mjs → hub-mdns-ciao-DlgkFJVY.mjs} +2 -2
- package/dist/index.js +7 -7
- package/dist/index.mjs +7 -7
- package/dist/{phase6-declare-gate-BlTDMI3t.js → phase6-declare-gate-DrmTtz7F.js} +1 -1
- package/dist/{phase6-declare-gate-CNxKbsvm.mjs → phase6-declare-gate-JQUqgQgq.mjs} +1 -1
- package/dist/{restore-archive-BeGP9IYs.js → restore-archive-4OP0EYg6.js} +1 -1
- package/dist/{restore-archive-D7HwsszI.mjs → restore-archive-YFSJRtKy.mjs} +1 -1
- package/dist/{wal-maintenance-i3GRvmQW.js → wal-maintenance-Bl0ZKjVR.js} +0 -0
- package/dist/{wal-maintenance-HEmw2Fik.mjs → wal-maintenance-TyuOEVy1.mjs} +0 -0
- package/package.json +1 -1
- 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-
|
|
6
|
+
const require_dist = require("../../dist-DjQ8C-ku.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-
|
|
9
|
-
//#region src/builtins/alerts/
|
|
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-DrmTtz7F.js");
|
|
9
|
+
//#region src/builtins/alerts/alert-episode-policy.ts
|
|
44
10
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
read: true
|
|
150
|
-
},
|
|
151
|
-
queuePush: false
|
|
68
|
+
...input.incoming,
|
|
69
|
+
read: true
|
|
152
70
|
};
|
|
153
71
|
}
|
|
154
|
-
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1012
|
-
|
|
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
|
|
1034
|
-
*
|
|
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
|
|
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
|
-
]
|
|
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: "
|
|
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
|
-
...
|
|
1118
|
-
...
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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, {
|