@glassmkr/crucible 0.9.0 → 0.9.2

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/init.js ADDED
@@ -0,0 +1,254 @@
1
+ // `glassmkr-crucible init` subcommand: canonical first-run setup.
2
+ //
3
+ // Validates the API key, optionally probes the ingest endpoint to
4
+ // confirm it's accepted, writes /etc/glassmkr/collector.yaml (mode
5
+ // 0600), writes a systemd unit at /etc/systemd/system/glassmkr-crucible
6
+ // (mode 0644) with ExecStart pointing at the dynamically-detected
7
+ // binary path (so Debian's /usr/local/bin/ vs Ubuntu's /usr/bin/
8
+ // divergence is handled), runs daemon-reload, and (unless --no-start)
9
+ // enables-and-starts the service.
10
+ //
11
+ // Designed to run unprivileged for the validation/connectivity steps
12
+ // and require root for the filesystem and systemd writes (same as the
13
+ // agent itself). Errors are surfaced with non-zero exit codes.
14
+ //
15
+ // IO is injected via the InitDeps interface so unit tests can run
16
+ // without touching /etc, systemctl, or the network.
17
+ import * as fsDefault from "node:fs";
18
+ import { execFileSync as execFileSyncDefault } from "node:child_process";
19
+ import * as osDefault from "node:os";
20
+ import * as pathDefault from "node:path";
21
+ export const DEFAULT_INGEST_URL = "https://forge.glassmkr.com/api/v1/ingest";
22
+ export const DEFAULT_CONFIG_PATH = "/etc/glassmkr/collector.yaml";
23
+ export const SYSTEMD_UNIT_PATH = "/etc/systemd/system/glassmkr-crucible.service";
24
+ const KEY_RE_NEW = /^gmk_cru_live_[A-Za-z0-9]{20,}_[A-Za-z0-9]{4}$/;
25
+ const KEY_RE_LEGACY = /^col_[A-Fa-f0-9]{16,}$/;
26
+ export function isValidApiKey(key) {
27
+ if (!key || /\s/.test(key))
28
+ return false;
29
+ return KEY_RE_NEW.test(key) || KEY_RE_LEGACY.test(key);
30
+ }
31
+ export function buildCollectorYaml(serverName, ingestUrl, apiKey) {
32
+ // Hand-written so the format is human-readable, deterministic, and
33
+ // doesn't pull in a YAML serializer at install time. Mirrors the
34
+ // existing /etc/glassmkr/collector.yaml shape.
35
+ return [
36
+ `# Generated by 'glassmkr-crucible init'. Hand-edit if needed.`,
37
+ `server_name: "${escapeDoubleQuoted(serverName)}"`,
38
+ `collection:`,
39
+ ` interval_seconds: 300`,
40
+ ` ipmi: true`,
41
+ ` smart: true`,
42
+ `forge:`,
43
+ ` enabled: true`,
44
+ ` url: "${escapeDoubleQuoted(ingestUrl.replace(/\/api\/v1\/ingest$/, ""))}"`,
45
+ ` api_key: "${apiKey}"`,
46
+ ``,
47
+ ].join("\n");
48
+ }
49
+ function escapeDoubleQuoted(s) {
50
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
51
+ }
52
+ export function buildSystemdUnit(binPath, configPath) {
53
+ return [
54
+ `[Unit]`,
55
+ `Description=Glassmkr Crucible - Bare Metal Monitoring`,
56
+ `After=network.target`,
57
+ ``,
58
+ `[Service]`,
59
+ `Type=simple`,
60
+ `User=root`,
61
+ `ExecStart=${binPath} ${configPath}`,
62
+ `Restart=always`,
63
+ `RestartSec=10`,
64
+ ``,
65
+ `[Install]`,
66
+ `WantedBy=multi-user.target`,
67
+ ``,
68
+ ].join("\n");
69
+ }
70
+ /**
71
+ * Run the init flow with caller-provided IO. Returns the exit code
72
+ * (0 = success). Pure orchestration; no side effects on `process`.
73
+ */
74
+ export async function runInit(opts, deps) {
75
+ // Resolve API key (stdin form supports `--api-key -`).
76
+ let apiKey = opts.apiKey;
77
+ if (apiKey === "-") {
78
+ try {
79
+ apiKey = (await deps.readStdin()).replace(/\r?\n$/, "").trim();
80
+ }
81
+ catch (err) {
82
+ deps.error(`[init] failed to read api key from stdin: ${err?.message ?? err}`);
83
+ return 1;
84
+ }
85
+ }
86
+ if (!isValidApiKey(apiKey)) {
87
+ deps.error(`[init] invalid --api-key: must look like "gmk_cru_live_<...>_<4>" or "col_<hex>". Got ${apiKey.length} char(s).`);
88
+ return 2;
89
+ }
90
+ const ingestUrl = opts.ingestUrl ?? DEFAULT_INGEST_URL;
91
+ const configPath = opts.configPath ?? DEFAULT_CONFIG_PATH;
92
+ const serverName = opts.name && opts.name.trim() ? opts.name.trim() : deps.hostname();
93
+ // Connectivity probe (optional). 401 means key is wrong → hard fail.
94
+ // 5xx / network errors → warn but continue (the host's network may
95
+ // still be coming up during a fresh install).
96
+ if (!opts.noVerify) {
97
+ deps.log(`[init] probing ${ingestUrl} ...`);
98
+ try {
99
+ const ctrl = new AbortController();
100
+ const timer = setTimeout(() => ctrl.abort(), 8000);
101
+ const resp = await deps.fetch(ingestUrl, {
102
+ method: "GET",
103
+ headers: { Authorization: `Bearer ${apiKey}` },
104
+ signal: ctrl.signal,
105
+ });
106
+ clearTimeout(timer);
107
+ if (resp.status === 401 || resp.status === 403) {
108
+ deps.error(`[init] api key rejected by ${ingestUrl} (HTTP ${resp.status}). Double-check the key in your Forge dashboard.`);
109
+ return 3;
110
+ }
111
+ if (resp.status >= 500) {
112
+ deps.warn(`[init] ingest endpoint returned ${resp.status}; continuing anyway. Check connectivity post-install.`);
113
+ }
114
+ else {
115
+ deps.log(`[init] api key validated (HTTP ${resp.status}).`);
116
+ }
117
+ }
118
+ catch (err) {
119
+ deps.warn(`[init] connectivity probe failed: ${err?.message ?? err}. Continuing; verify after install with 'journalctl -u glassmkr-crucible'.`);
120
+ }
121
+ }
122
+ // Write config (0600) — refuse to overwrite without --force.
123
+ if (deps.fs.existsSync(configPath) && !opts.force) {
124
+ deps.error(`[init] config already exists at ${configPath}. Pass --force to overwrite, or remove the file first.`);
125
+ return 4;
126
+ }
127
+ const parent = pathDefault.dirname(configPath);
128
+ try {
129
+ deps.fs.mkdirSync(parent, { recursive: true, mode: 0o755 });
130
+ }
131
+ catch (err) {
132
+ deps.error(`[init] failed to create config directory ${parent}: ${err?.message ?? err}`);
133
+ return 5;
134
+ }
135
+ const yaml = buildCollectorYaml(serverName, ingestUrl, apiKey);
136
+ try {
137
+ deps.fs.writeFileSync(configPath, yaml, { mode: 0o600 });
138
+ deps.fs.chmodSync(configPath, 0o600);
139
+ deps.log(`[init] wrote config: ${configPath} (mode 0600, server_name=${serverName})`);
140
+ }
141
+ catch (err) {
142
+ deps.error(`[init] failed to write ${configPath}: ${err?.message ?? err}`);
143
+ return 6;
144
+ }
145
+ // Resolve binary path via `command -v`. Mirrors the F5 install fix.
146
+ let binPath = null;
147
+ try {
148
+ const r = deps.exec("command", ["-v", "glassmkr-crucible"]);
149
+ const candidate = r.stdout.trim();
150
+ if (candidate)
151
+ binPath = candidate;
152
+ }
153
+ catch {
154
+ // fall through; we'll try `which` next
155
+ }
156
+ if (!binPath) {
157
+ try {
158
+ const r = deps.exec("which", ["glassmkr-crucible"]);
159
+ const candidate = r.stdout.trim();
160
+ if (candidate)
161
+ binPath = candidate;
162
+ }
163
+ catch {
164
+ // ignored
165
+ }
166
+ }
167
+ if (!binPath) {
168
+ deps.error(`[init] could not locate the glassmkr-crucible binary on PATH. Make sure 'npm install -g @glassmkr/crucible' completed before running init.`);
169
+ return 7;
170
+ }
171
+ // Write systemd unit (0644).
172
+ const unit = buildSystemdUnit(binPath, configPath);
173
+ try {
174
+ deps.fs.writeFileSync(SYSTEMD_UNIT_PATH, unit, { mode: 0o644 });
175
+ deps.fs.chmodSync(SYSTEMD_UNIT_PATH, 0o644);
176
+ deps.log(`[init] wrote systemd unit: ${SYSTEMD_UNIT_PATH} (ExecStart=${binPath})`);
177
+ }
178
+ catch (err) {
179
+ deps.error(`[init] failed to write systemd unit: ${err?.message ?? err}`);
180
+ return 8;
181
+ }
182
+ // daemon-reload to pick up the new unit.
183
+ try {
184
+ const r = deps.exec("systemctl", ["daemon-reload"]);
185
+ if (r.status !== 0) {
186
+ deps.warn(`[init] systemctl daemon-reload returned ${r.status}; continuing.`);
187
+ }
188
+ }
189
+ catch (err) {
190
+ deps.warn(`[init] systemctl daemon-reload failed: ${err?.message ?? err}; continuing.`);
191
+ }
192
+ if (opts.noStart) {
193
+ deps.log(`[init] --no-start set; not enabling/starting glassmkr-crucible.`);
194
+ deps.log(`[init] when ready: 'sudo systemctl enable --now glassmkr-crucible'`);
195
+ return 0;
196
+ }
197
+ try {
198
+ const r = deps.exec("systemctl", ["enable", "--now", "glassmkr-crucible"]);
199
+ if (r.status !== 0) {
200
+ deps.error(`[init] systemctl enable --now glassmkr-crucible failed (status=${r.status}). Inspect with: 'systemctl status glassmkr-crucible --no-pager' and 'journalctl -u glassmkr-crucible -n 50 --no-pager'`);
201
+ return 9;
202
+ }
203
+ deps.log(`[init] service enabled and started.`);
204
+ }
205
+ catch (err) {
206
+ deps.error(`[init] failed to enable/start service: ${err?.message ?? err}`);
207
+ return 9;
208
+ }
209
+ deps.log(`[init] done. Next steps:`);
210
+ deps.log(` systemctl status glassmkr-crucible --no-pager | head -20`);
211
+ deps.log(` journalctl -u glassmkr-crucible -n 50 --no-pager`);
212
+ return 0;
213
+ }
214
+ /**
215
+ * Default IO bindings to the real OS. Used by index.ts when init runs
216
+ * for real; tests pass their own mocks.
217
+ */
218
+ export function defaultDeps() {
219
+ return {
220
+ fs: {
221
+ existsSync: fsDefault.existsSync,
222
+ mkdirSync: (p, opts) => { fsDefault.mkdirSync(p, opts); },
223
+ writeFileSync: (p, data, opts) => { fsDefault.writeFileSync(p, data, opts); },
224
+ chmodSync: fsDefault.chmodSync,
225
+ },
226
+ exec: (cmd, args) => {
227
+ try {
228
+ const stdout = execFileSyncDefault(cmd, args, { encoding: "utf8" });
229
+ return { stdout, status: 0 };
230
+ }
231
+ catch (err) {
232
+ return { stdout: err?.stdout?.toString() ?? "", status: typeof err?.status === "number" ? err.status : null };
233
+ }
234
+ },
235
+ hostname: () => osDefault.hostname(),
236
+ log: (m) => console.log(m),
237
+ warn: (m) => console.warn(m),
238
+ error: (m) => console.error(m),
239
+ fetch: async (url, init) => {
240
+ const r = await fetch(url, init);
241
+ return { status: r.status };
242
+ },
243
+ readStdin: () => new Promise((resolve, reject) => {
244
+ let buf = "";
245
+ process.stdin.setEncoding("utf8");
246
+ process.stdin.on("data", (chunk) => { buf += chunk; });
247
+ process.stdin.on("end", () => resolve(buf));
248
+ process.stdin.on("error", reject);
249
+ }),
250
+ };
251
+ }
252
+ // Marker exports so tsc emits the file even if only used via dynamic import.
253
+ export const _typecheck_helpers = { pathDefault, osDefault };
254
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { filterRedundantCpuDtsSensors } from "../ipmi-sensor-filter.js";
3
+ const s = (name) => ({ name });
4
+ describe("filterRedundantCpuDtsSensors", () => {
5
+ it("Gigabyte AMD: drops CPU0_DTS when CPU0_TEMP is present", () => {
6
+ const out = filterRedundantCpuDtsSensors([s("CPU0_TEMP"), s("CPU0_DTS"), s("DIMMG0_TEMP")]);
7
+ expect(out.map((x) => x.name)).toEqual(["CPU0_TEMP", "DIMMG0_TEMP"]);
8
+ });
9
+ it("dual-socket EPYC: drops both DTS sensors when both _TEMP are present", () => {
10
+ const out = filterRedundantCpuDtsSensors([
11
+ s("CPU0_TEMP"),
12
+ s("CPU1_TEMP"),
13
+ s("CPU0_DTS"),
14
+ s("CPU1_DTS"),
15
+ s("System Temp"),
16
+ ]);
17
+ expect(out.map((x) => x.name)).toEqual(["CPU0_TEMP", "CPU1_TEMP", "System Temp"]);
18
+ });
19
+ it("keeps DTS as fallback when no _TEMP sensor exists for that socket", () => {
20
+ const out = filterRedundantCpuDtsSensors([s("CPU0_DTS"), s("System Temp")]);
21
+ expect(out.map((x) => x.name)).toEqual(["CPU0_DTS", "System Temp"]);
22
+ });
23
+ it("mixed: keeps CPU1_DTS when only CPU0_TEMP/CPU0_DTS pair has a sibling", () => {
24
+ const out = filterRedundantCpuDtsSensors([
25
+ s("CPU0_TEMP"),
26
+ s("CPU0_DTS"),
27
+ s("CPU1_DTS"),
28
+ ]);
29
+ expect(out.map((x) => x.name)).toEqual(["CPU0_TEMP", "CPU1_DTS"]);
30
+ });
31
+ it("Dell-style single 'CPU Temp' sensor passes through unchanged", () => {
32
+ const out = filterRedundantCpuDtsSensors([s("CPU Temp"), s("PS1 Status")]);
33
+ expect(out.map((x) => x.name)).toEqual(["CPU Temp", "PS1 Status"]);
34
+ });
35
+ it("Supermicro 'CPU1 Temp' (space-form) passes through unchanged", () => {
36
+ const out = filterRedundantCpuDtsSensors([s("CPU1 Temp"), s("CPU2 Temp")]);
37
+ expect(out.map((x) => x.name)).toEqual(["CPU1 Temp", "CPU2 Temp"]);
38
+ });
39
+ it("Supermicro 'CPU1 Temp' (space-form) drops 'CPU1_DTS' on the same socket", () => {
40
+ // Hypothetical: a board exposing both space-form _TEMP and underscore-form _DTS.
41
+ // The socket prefix matches via either of the two patterns we recognise.
42
+ const out = filterRedundantCpuDtsSensors([s("CPU1 Temp"), s("CPU1_DTS")]);
43
+ expect(out.map((x) => x.name)).toEqual(["CPU1 Temp"]);
44
+ });
45
+ it("preserves non-CPU sensors unchanged (DIMM, PSU, fan)", () => {
46
+ const out = filterRedundantCpuDtsSensors([
47
+ s("CPU0_TEMP"),
48
+ s("CPU0_DTS"),
49
+ s("DIMMG0_TEMP"),
50
+ s("PSU1 Status"),
51
+ s("FAN1"),
52
+ ]);
53
+ expect(out.map((x) => x.name)).toEqual(["CPU0_TEMP", "DIMMG0_TEMP", "PSU1 Status", "FAN1"]);
54
+ });
55
+ it("empty input returns empty array", () => {
56
+ expect(filterRedundantCpuDtsSensors([])).toEqual([]);
57
+ });
58
+ it("input with no DTS sensors is a pass-through", () => {
59
+ const input = [s("CPU0_TEMP"), s("CPU1_TEMP"), s("System Temp")];
60
+ expect(filterRedundantCpuDtsSensors(input).map((x) => x.name)).toEqual(input.map((x) => x.name));
61
+ });
62
+ it("does not match non-CPU DTS-shaped names (e.g. random vendor sensors)", () => {
63
+ // Defensive: only `^CPU\d+_DTS$` is filtered. A sensor like `BOARD_DTS`
64
+ // or `CPU_DTS` (no socket index) is preserved.
65
+ const out = filterRedundantCpuDtsSensors([s("CPU_DTS"), s("BOARD_DTS"), s("CPU0_TEMP")]);
66
+ expect(out.map((x) => x.name)).toEqual(["CPU_DTS", "BOARD_DTS", "CPU0_TEMP"]);
67
+ });
68
+ it("preserves stable order of kept sensors", () => {
69
+ const out = filterRedundantCpuDtsSensors([
70
+ s("System Temp"),
71
+ s("CPU0_DTS"),
72
+ s("CPU1_TEMP"),
73
+ s("CPU0_TEMP"),
74
+ s("CPU1_DTS"),
75
+ s("PSU1 Status"),
76
+ ]);
77
+ expect(out.map((x) => x.name)).toEqual([
78
+ "System Temp",
79
+ "CPU1_TEMP",
80
+ "CPU0_TEMP",
81
+ "PSU1 Status",
82
+ ]);
83
+ });
84
+ });
85
+ //# sourceMappingURL=ipmi-sensor-filter.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ipmi-sensor-filter.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/ipmi-sensor-filter.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export interface SensorLike {
2
+ name: string;
3
+ }
4
+ /**
5
+ * Drop `CPU<N>_DTS` sensors whose socket has a sibling `CPU<N>_TEMP`
6
+ * (or `CPU<N> Temp`) sensor in the same list. Returns a new array.
7
+ *
8
+ * Stable order: kept sensors retain their original input order. Pure
9
+ * function; safe to call multiple times.
10
+ */
11
+ export declare function filterRedundantCpuDtsSensors<T extends SensorLike>(sensors: T[]): T[];
@@ -0,0 +1,74 @@
1
+ // Per-socket pre-filter for IPMI CPU thermal sensors.
2
+ //
3
+ // Background: Gigabyte BMC firmware (observed on H262-Z63 and MC12-LE0
4
+ // with firmware 12.61) reports a `CPU<N>_DTS` sensor that runs ~30°C
5
+ // hotter than the actual AMD die temperature read directly via the
6
+ // kernel's k10temp driver. The same boards also expose a much closer
7
+ // `CPU<N>_TEMP` sensor on each socket. The IPMI fallback path of
8
+ // Forge's `cpu_temperature_high` evaluator picks the maximum across
9
+ // all CPU thermal sensors, which over-fires when both are present.
10
+ //
11
+ // Fix: when the IPMI sensor list contains both `CPU<N>_TEMP` and
12
+ // `CPU<N>_DTS` for the same socket, drop the DTS variant. If only DTS
13
+ // exists for a socket (rare, but possible on firmware that doesn't
14
+ // expose `*_TEMP`), keep it as the only available CPU thermal reading.
15
+ //
16
+ // Tracked: glassmkr/crucible#2. Closed in 0.9.1.
17
+ //
18
+ // This filter only inspects names matching `^CPU\d+_DTS$` (Gigabyte's
19
+ // specific convention). Non-CPU sensors and DTS-shaped sensors that
20
+ // don't fit the per-socket prefix pattern are passed through unchanged
21
+ // so we don't accidentally hide a useful reading on a vendor we
22
+ // haven't characterised.
23
+ const DTS_RE = /^CPU(\d+)_DTS$/i;
24
+ function tempSensorPatternsForSocket(socket) {
25
+ // Match the two observed temperature naming conventions on the same
26
+ // socket: `CPU0_TEMP` (Gigabyte) and `CPU0 Temp` (Supermicro-style
27
+ // with a space). Either form being present means we have a non-DTS
28
+ // reading for this socket and can safely drop the DTS variant.
29
+ const escaped = socket.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
30
+ return [
31
+ new RegExp(`^CPU${escaped}_TEMP$`, "i"),
32
+ new RegExp(`^CPU${escaped}\\s+Temp$`, "i"),
33
+ ];
34
+ }
35
+ /**
36
+ * Drop `CPU<N>_DTS` sensors whose socket has a sibling `CPU<N>_TEMP`
37
+ * (or `CPU<N> Temp`) sensor in the same list. Returns a new array.
38
+ *
39
+ * Stable order: kept sensors retain their original input order. Pure
40
+ * function; safe to call multiple times.
41
+ */
42
+ export function filterRedundantCpuDtsSensors(sensors) {
43
+ const sockets = new Set();
44
+ const dtsBySocket = new Map();
45
+ for (const s of sensors) {
46
+ const m = DTS_RE.exec(s.name);
47
+ if (!m)
48
+ continue;
49
+ sockets.add(m[1]);
50
+ const list = dtsBySocket.get(m[1]) ?? [];
51
+ list.push(s);
52
+ dtsBySocket.set(m[1], list);
53
+ }
54
+ if (sockets.size === 0)
55
+ return sensors.slice();
56
+ // For each socket with a DTS sensor, check whether any non-DTS CPU
57
+ // thermal sensor exists for the same socket.
58
+ const dropDts = new Set();
59
+ for (const socket of sockets) {
60
+ const patterns = tempSensorPatternsForSocket(socket);
61
+ const hasTemp = sensors.some((s) => patterns.some((re) => re.test(s.name)));
62
+ if (hasTemp)
63
+ dropDts.add(socket);
64
+ }
65
+ if (dropDts.size === 0)
66
+ return sensors.slice();
67
+ return sensors.filter((s) => {
68
+ const m = DTS_RE.exec(s.name);
69
+ if (!m)
70
+ return true;
71
+ return !dropDts.has(m[1]);
72
+ });
73
+ }
74
+ //# sourceMappingURL=ipmi-sensor-filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ipmi-sensor-filter.js","sourceRoot":"","sources":["../../src/lib/ipmi-sensor-filter.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,EAAE;AACF,uEAAuE;AACvE,qEAAqE;AACrE,mEAAmE;AACnE,qEAAqE;AACrE,iEAAiE;AACjE,oEAAoE;AACpE,mEAAmE;AACnE,EAAE;AACF,iEAAiE;AACjE,sEAAsE;AACtE,mEAAmE;AACnE,uEAAuE;AACvE,EAAE;AACF,iDAAiD;AACjD,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,uEAAuE;AACvE,gEAAgE;AAChE,yBAAyB;AAMzB,MAAM,MAAM,GAAG,iBAAiB,CAAC;AAEjC,SAAS,2BAA2B,CAAC,MAAc;IACjD,oEAAoE;IACpE,mEAAmE;IACnE,mEAAmE;IACnE,+DAA+D;IAC/D,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;IAC9D,OAAO;QACL,IAAI,MAAM,CAAC,OAAO,OAAO,QAAQ,EAAE,GAAG,CAAC;QACvC,IAAI,MAAM,CAAC,OAAO,OAAO,WAAW,EAAE,GAAG,CAAC;KAC3C,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,4BAA4B,CAAuB,OAAY;IAC7E,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,MAAM,WAAW,GAAG,IAAI,GAAG,EAAe,CAAC;IAC3C,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,CAAC,CAAC;YAAE,SAAS;QACjB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAClB,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACzC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACb,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IAC9B,CAAC;IACD,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC,KAAK,EAAE,CAAC;IAE/C,mEAAmE;IACnE,6CAA6C;IAC7C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,2BAA2B,CAAC,MAAM,CAAC,CAAC;QACrD,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC5E,IAAI,OAAO;YAAE,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC;IACD,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC,KAAK,EAAE,CAAC;IAE/C,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QAC1B,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;QACpB,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -41,6 +41,10 @@ export interface ConntrackData {
41
41
  export interface SystemdData {
42
42
  failed_units: string[];
43
43
  failed_count: number;
44
+ /** Last 5 journal lines per failed unit, populated only when at
45
+ * least one unit is failed. Keys match `failed_units`. Codex
46
+ * experiment 2026-05-12. */
47
+ journal_excerpts?: Record<string, string[]>;
44
48
  }
45
49
  export interface NtpData {
46
50
  synced: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassmkr/crucible",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Lightweight bare metal server monitoring. IPMI, SMART, OS, network. Opinionated alerts.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",