@glassmkr/crucible 0.6.1 → 0.6.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.
Files changed (43) hide show
  1. package/.github/workflows/publish.yml +25 -0
  2. package/README.md +83 -45
  3. package/dist/__tests__/cli.test.d.ts +1 -0
  4. package/dist/__tests__/cli.test.js +64 -0
  5. package/dist/__tests__/cli.test.js.map +1 -0
  6. package/dist/cli.d.ts +10 -0
  7. package/dist/cli.js +53 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/collect/__tests__/ipmi.test.d.ts +1 -0
  10. package/dist/collect/__tests__/ipmi.test.js +90 -0
  11. package/dist/collect/__tests__/ipmi.test.js.map +1 -0
  12. package/dist/collect/__tests__/smart.test.d.ts +1 -0
  13. package/dist/collect/__tests__/smart.test.js +64 -0
  14. package/dist/collect/__tests__/smart.test.js.map +1 -0
  15. package/dist/collect/__tests__/zfs.test.d.ts +1 -0
  16. package/dist/collect/__tests__/zfs.test.js +68 -0
  17. package/dist/collect/__tests__/zfs.test.js.map +1 -0
  18. package/dist/collect/ipmi.d.ts +5 -1
  19. package/dist/collect/ipmi.js +6 -3
  20. package/dist/collect/ipmi.js.map +1 -1
  21. package/dist/collect/smart.d.ts +26 -0
  22. package/dist/collect/smart.js +28 -25
  23. package/dist/collect/smart.js.map +1 -1
  24. package/dist/collect/zfs.d.ts +2 -1
  25. package/dist/collect/zfs.js +7 -3
  26. package/dist/collect/zfs.js.map +1 -1
  27. package/dist/index.js +11 -3
  28. package/dist/index.js.map +1 -1
  29. package/dist/lib/__tests__/parse.test.d.ts +1 -0
  30. package/dist/lib/__tests__/parse.test.js +27 -0
  31. package/dist/lib/__tests__/parse.test.js.map +1 -0
  32. package/package.json +4 -2
  33. package/src/__tests__/cli.test.ts +74 -0
  34. package/src/cli.ts +62 -0
  35. package/src/collect/__tests__/ipmi.test.ts +96 -0
  36. package/src/collect/__tests__/smart.test.ts +68 -0
  37. package/src/collect/__tests__/zfs.test.ts +72 -0
  38. package/src/collect/ipmi.ts +6 -3
  39. package/src/collect/smart.ts +40 -28
  40. package/src/collect/zfs.ts +7 -2
  41. package/src/index.ts +13 -3
  42. package/src/lib/__tests__/parse.test.ts +28 -0
  43. package/vitest.config.ts +12 -0
@@ -0,0 +1,25 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ tags: ['v*']
6
+
7
+ permissions:
8
+ contents: read
9
+ id-token: write
10
+
11
+ jobs:
12
+ publish:
13
+ name: npm publish
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-node@v4
18
+ with:
19
+ # Node 24 ships with npm 11.x which has working npm Trusted Publishing.
20
+ node-version: 24
21
+ registry-url: 'https://registry.npmjs.org'
22
+ - run: npm --version
23
+ - run: npm install
24
+ - run: npm run build
25
+ - run: npm publish --access public --provenance
package/README.md CHANGED
@@ -3,77 +3,115 @@
3
3
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
4
4
  [![npm version](https://img.shields.io/npm/v/@glassmkr/crucible.svg)](https://www.npmjs.com/package/@glassmkr/crucible)
5
5
 
6
- Lightweight bare metal server monitoring agent. Collects hardware and OS health data, pushes snapshots to [Forge](https://forge.glassmkr.com) every 5 minutes. Forge evaluates 36 alert rules and sends notifications.
6
+ Lightweight bare metal server monitoring agent. Collects hardware and OS health every 5 minutes and pushes snapshots to a [Forge](https://forge.glassmkr.com) dashboard, which evaluates 36 alert rules and sends notifications.
7
7
 
8
- Open source. MIT licensed. Built by [Glassmkr](https://glassmkr.com).
8
+ Open source. MIT licensed. Built by [Glassmkr](https://glassmkr.com). See also [Bench](https://github.com/glassmkr/bench), the MCP server collection.
9
9
 
10
- ## Quick Install
10
+ ## Install
11
11
 
12
12
  ```bash
13
- curl -sf https://forge.glassmkr.com/install | bash
13
+ npm install -g @glassmkr/crucible
14
14
  ```
15
15
 
16
- Or via npm:
16
+ Or use the bootstrap script:
17
17
 
18
18
  ```bash
19
- npm install -g @glassmkr/crucible
19
+ curl -sf https://forge.glassmkr.com/install | bash
20
20
  ```
21
21
 
22
- ## What Crucible Collects
22
+ ## Quick Start
23
23
 
24
- | Module | Data |
25
- |--------|------|
26
- | **CPU** | Aggregate and per-core utilization (user, system, iowait, idle, irq, softirq) |
27
- | **Memory** | RAM usage, swap usage |
28
- | **Disks** | Space per mount point, inode counts, mount options, filesystem type |
29
- | **SMART** | Drive health, model, temperature, power-on hours, reallocated sectors, NVMe wear |
30
- | **Network** | Interface traffic, error/drop counts, link speed |
31
- | **RAID** | mdadm array status, degraded detection |
32
- | **IPMI** | Sensor readings (temperatures, fans, voltages, power), ECC errors, SEL events |
33
- | **Security** | SSH config, firewall status, pending updates, kernel vulnerabilities |
24
+ 1. Create an API key in the Forge dashboard (Servers, then Add server).
25
+ 2. Drop a config at `/etc/glassmkr/collector.yaml`:
26
+
27
+ ```yaml
28
+ server_name: "web-01"
29
+ collection:
30
+ interval_seconds: 300
31
+ ipmi: true
32
+ smart: true
33
+ forge:
34
+ enabled: true
35
+ url: "https://forge.glassmkr.com"
36
+ api_key: "col_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
37
+ ```
38
+
39
+ 3. Run as a systemd service (recommended) or directly:
40
+
41
+ ```bash
42
+ sudo glassmkr-crucible
43
+ ```
44
+
45
+ Snapshots appear in the Forge dashboard within seconds of the first push.
46
+
47
+ ## CLI Reference
34
48
 
35
- ## Alert Rules
49
+ ```
50
+ glassmkr-crucible [options]
51
+
52
+ Options:
53
+ -v, --version Print version and exit
54
+ -h, --help Print this help and exit
55
+ -c, --config Path to config file (default: /etc/glassmkr/collector.yaml)
56
+ ```
36
57
 
37
- Crucible collects the data. Forge evaluates 36 alert rules server-side and sends notifications via Telegram and Slack.
58
+ `--config=PATH` and the legacy positional form `glassmkr-crucible /path/to.yaml` both work. Without options, Crucible runs as a long-lived collector daemon.
38
59
 
39
- **Categories:** OS (9), Storage (8), Network (4), Hardware/IPMI (5), ZFS (2), Security (6), Service Health (2).
60
+ ## Systemd Service
40
61
 
41
- **Priorities:** P1 Urgent, P2 High, P3 Medium, P4 Low.
62
+ Create `/etc/systemd/system/glassmkr-collector.service`:
42
63
 
43
- See the full rule list: [forge.glassmkr.com/docs/alerts](https://forge.glassmkr.com/docs/alerts)
64
+ ```ini
65
+ [Unit]
66
+ Description=Glassmkr Crucible - Bare Metal Monitoring
67
+ After=network.target
44
68
 
45
- ## Configuration
69
+ [Service]
70
+ Type=simple
71
+ User=root
72
+ ExecStart=/usr/bin/glassmkr-crucible /etc/glassmkr/collector.yaml
73
+ Restart=on-failure
74
+ RestartSec=10
46
75
 
47
- Edit `/etc/glassmkr/collector.yaml`:
76
+ [Install]
77
+ WantedBy=multi-user.target
78
+ ```
79
+
80
+ Enable and start:
48
81
 
49
- ```yaml
50
- server_name: "web-01"
51
- collection:
52
- interval_seconds: 300
53
- ipmi: true
54
- smart: true
55
- forge:
56
- enabled: true
57
- url: "https://forge.glassmkr.com"
58
- api_key: "col_xxx"
82
+ ```bash
83
+ sudo systemctl daemon-reload
84
+ sudo systemctl enable --now glassmkr-collector
85
+ sudo systemctl status glassmkr-collector
59
86
  ```
60
87
 
61
- Full configuration reference: [forge.glassmkr.com/docs/configuration](https://forge.glassmkr.com/docs/configuration)
88
+ ## What It Collects
89
+
90
+ | Module | Data |
91
+ |--------|------|
92
+ | CPU | Aggregate and per-core utilization (user, system, iowait, idle) |
93
+ | Memory | RAM usage, swap usage |
94
+ | Disks | Space per mount point, inode counts, mount options, filesystem type |
95
+ | SMART | Drive health, model, temperature, power-on hours, reallocated sectors, NVMe wear |
96
+ | Network | Interface traffic, delta error/drop counters, link speed |
97
+ | RAID | mdadm array status, degraded detection |
98
+ | IPMI | Sensor readings, ECC errors, SEL events, fan RPM |
99
+ | Security | SSH config, firewall status, pending updates, kernel vulnerabilities, kernel-needs-reboot |
100
+ | ZFS | Pool state, scrub age, scrub errors |
101
+ | I/O | Per-device latency, IOPS, dmesg I/O errors |
102
+ | Conntrack | nf_conntrack table usage |
103
+ | Systemd | Failed unit count |
104
+ | NTP | Sync state and source |
105
+ | File descriptors | System-wide allocation |
106
+
107
+ Forge evaluates 36 alert rules server-side across OS, Storage, Network, Hardware, ZFS, Security, and Service Health, with priorities P1 Urgent through P4 Low. Full list: [forge.glassmkr.com/docs/alerts](https://forge.glassmkr.com/docs/alerts).
62
108
 
63
109
  ## Requirements
64
110
 
65
111
  - Linux (any distribution: Ubuntu, Debian, RHEL, Rocky, Alma, Arch, Alpine)
66
112
  - Node.js 18+
67
- - Root access (for SMART, IPMI, and system metrics)
68
- - Optional: `smartmontools` (SMART data), `ipmitool` (IPMI data)
69
-
70
- ## How It Works
71
-
72
- 1. Crucible runs as a systemd service
73
- 2. Every 5 minutes, it collects a complete health snapshot
74
- 3. The snapshot is pushed to Forge via HTTPS (POST /api/v1/ingest)
75
- 4. Forge evaluates alert rules and sends notifications
76
- 5. Data appears in the Forge dashboard within seconds of push
113
+ - Root access (for SMART, IPMI, dmesg, and `/proc` access)
114
+ - Optional: `smartmontools` for SMART data, `ipmitool` for IPMI data, `zfsutils-linux` for ZFS pools
77
115
 
78
116
  ## Documentation
79
117
 
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseCliArgs, helpText, DEFAULT_CONFIG_PATH } from "../cli.js";
3
+ describe("parseCliArgs", () => {
4
+ it("--version returns version string and mode=version", () => {
5
+ const { result, output } = parseCliArgs(["--version"], "1.2.3");
6
+ expect(result.mode).toBe("version");
7
+ expect(output).toBe("glassmkr-crucible v1.2.3");
8
+ });
9
+ it("-v aliases --version", () => {
10
+ const { result, output } = parseCliArgs(["-v"], "1.2.3");
11
+ expect(result.mode).toBe("version");
12
+ expect(output).toBe("glassmkr-crucible v1.2.3");
13
+ });
14
+ it("--help returns help text and mode=help", () => {
15
+ const { result, output } = parseCliArgs(["--help"], "1.2.3");
16
+ expect(result.mode).toBe("help");
17
+ expect(output).toContain("glassmkr-crucible v1.2.3");
18
+ expect(output).toContain("Usage:");
19
+ expect(output).toContain("--version");
20
+ expect(output).toContain("--help");
21
+ expect(output).toContain("--config");
22
+ });
23
+ it("-h aliases --help", () => {
24
+ const { result } = parseCliArgs(["-h"], "1.2.3");
25
+ expect(result.mode).toBe("help");
26
+ });
27
+ it("no args returns mode=run with the default config path", () => {
28
+ const { result, output } = parseCliArgs([], "1.2.3");
29
+ expect(result.mode).toBe("run");
30
+ expect(result.configPath).toBe(DEFAULT_CONFIG_PATH);
31
+ expect(output).toBeNull();
32
+ });
33
+ it("-c accepts a path in the next argument", () => {
34
+ const { result } = parseCliArgs(["-c", "/tmp/a.yaml"], "1.2.3");
35
+ expect(result.configPath).toBe("/tmp/a.yaml");
36
+ });
37
+ it("--config accepts a path in the next argument", () => {
38
+ const { result } = parseCliArgs(["--config", "/tmp/b.yaml"], "1.2.3");
39
+ expect(result.configPath).toBe("/tmp/b.yaml");
40
+ });
41
+ it("--config=PATH form works", () => {
42
+ const { result } = parseCliArgs(["--config=/tmp/c.yaml"], "1.2.3");
43
+ expect(result.configPath).toBe("/tmp/c.yaml");
44
+ });
45
+ it("legacy positional argument still sets config path", () => {
46
+ const { result } = parseCliArgs(["/tmp/legacy.yaml"], "1.2.3");
47
+ expect(result.configPath).toBe("/tmp/legacy.yaml");
48
+ });
49
+ it("--version wins over a provided config path (no collector start)", () => {
50
+ const { result } = parseCliArgs(["--config", "/tmp/x.yaml", "--version"], "1.2.3");
51
+ expect(result.mode).toBe("version");
52
+ });
53
+ });
54
+ describe("helpText", () => {
55
+ it("mentions the binary name, default config path, and both flags", () => {
56
+ const txt = helpText("0.6.1");
57
+ expect(txt).toContain("glassmkr-crucible v0.6.1");
58
+ expect(txt).toContain(DEFAULT_CONFIG_PATH);
59
+ expect(txt).toContain("-v, --version");
60
+ expect(txt).toContain("-h, --help");
61
+ expect(txt).toContain("-c, --config");
62
+ });
63
+ });
64
+ //# sourceMappingURL=cli.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.test.js","sourceRoot":"","sources":["../../src/__tests__/cli.test.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export interface CliArgs {
2
+ mode: "version" | "help" | "run";
3
+ configPath: string;
4
+ }
5
+ export declare const DEFAULT_CONFIG_PATH = "/etc/glassmkr/collector.yaml";
6
+ export declare function parseCliArgs(argv: string[], version: string): {
7
+ result: CliArgs;
8
+ output: string | null;
9
+ };
10
+ export declare function helpText(version: string): string;
package/dist/cli.js ADDED
@@ -0,0 +1,53 @@
1
+ // CLI argument handling for the Crucible binary. Runs before any config load
2
+ // or collector initialization so --version and --help exit cleanly even when
3
+ // the config file is missing or the host lacks the tools the collectors need.
4
+ export const DEFAULT_CONFIG_PATH = "/etc/glassmkr/collector.yaml";
5
+ export function parseCliArgs(argv, version) {
6
+ // argv is typically process.argv.slice(2)
7
+ let configPath = DEFAULT_CONFIG_PATH;
8
+ for (let i = 0; i < argv.length; i++) {
9
+ const arg = argv[i];
10
+ if (arg === "--version" || arg === "-v") {
11
+ return { result: { mode: "version", configPath: "" }, output: `glassmkr-crucible v${version}` };
12
+ }
13
+ if (arg === "--help" || arg === "-h") {
14
+ return { result: { mode: "help", configPath: "" }, output: helpText(version) };
15
+ }
16
+ // -c <path> or --config <path>
17
+ if (arg === "-c" || arg === "--config") {
18
+ const next = argv[i + 1];
19
+ if (next) {
20
+ configPath = next;
21
+ i++;
22
+ }
23
+ continue;
24
+ }
25
+ // --config=<path>
26
+ if (arg.startsWith("--config=")) {
27
+ configPath = arg.slice("--config=".length);
28
+ continue;
29
+ }
30
+ // Legacy positional argument: first non-flag token
31
+ if (!arg.startsWith("-")) {
32
+ configPath = arg;
33
+ }
34
+ }
35
+ return { result: { mode: "run", configPath }, output: null };
36
+ }
37
+ export function helpText(version) {
38
+ return [
39
+ `glassmkr-crucible v${version} - Bare metal server monitoring agent`,
40
+ "",
41
+ "Usage:",
42
+ " glassmkr-crucible [options]",
43
+ "",
44
+ "Options:",
45
+ " -v, --version Print version and exit",
46
+ " -h, --help Print this help and exit",
47
+ ` -c, --config Path to config file (default: ${DEFAULT_CONFIG_PATH})`,
48
+ "",
49
+ "Without options, starts the collector daemon using the config file.",
50
+ "Docs: https://github.com/glassmkr/crucible",
51
+ ].join("\n");
52
+ }
53
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,6EAA6E;AAC7E,8EAA8E;AAO9E,MAAM,CAAC,MAAM,mBAAmB,GAAG,8BAA8B,CAAC;AAElE,MAAM,UAAU,YAAY,CAAC,IAAc,EAAE,OAAe;IAC1D,0CAA0C;IAC1C,IAAI,UAAU,GAAG,mBAAmB,CAAC;IAErC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,IAAI,GAAG,KAAK,WAAW,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YACxC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,sBAAsB,OAAO,EAAE,EAAE,CAAC;QAClG,CAAC;QACD,IAAI,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YACrC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QACjF,CAAC;QACD,+BAA+B;QAC/B,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,UAAU,EAAE,CAAC;YACvC,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACzB,IAAI,IAAI,EAAE,CAAC;gBACT,UAAU,GAAG,IAAI,CAAC;gBAClB,CAAC,EAAE,CAAC;YACN,CAAC;YACD,SAAS;QACX,CAAC;QACD,kBAAkB;QAClB,IAAI,GAAG,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YAChC,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;YAC3C,SAAS;QACX,CAAC;QACD,mDAAmD;QACnD,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,UAAU,GAAG,GAAG,CAAC;QACnB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,OAAO;QACL,sBAAsB,OAAO,uCAAuC;QACpE,EAAE;QACF,QAAQ;QACR,+BAA+B;QAC/B,EAAE;QACF,UAAU;QACV,2CAA2C;QAC3C,6CAA6C;QAC7C,oDAAoD,mBAAmB,GAAG;QAC1E,EAAE;QACF,qEAAqE;QACrE,4CAA4C;KAC7C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { classifySensor, deriveSelSeverity, parseSelTimestamp, parseFanStatus } from "../ipmi.js";
3
+ describe("classifySensor", () => {
4
+ it("recognizes memory sensors", () => {
5
+ expect(classifySensor("DIMM_A1")).toBe("memory");
6
+ expect(classifySensor("Memory ECC")).toBe("memory");
7
+ });
8
+ it("recognizes power supplies", () => {
9
+ expect(classifySensor("PSU1 Status")).toBe("power");
10
+ expect(classifySensor("Power Supply 1")).toBe("power");
11
+ });
12
+ it("recognizes fans, watchdog, processors, temps, voltage, storage, chassis", () => {
13
+ expect(classifySensor("Fan1")).toBe("fan");
14
+ expect(classifySensor("Watchdog")).toBe("watchdog");
15
+ expect(classifySensor("Processor 0")).toBe("processor");
16
+ // CPU-named temperature sensors classify as processor (cpu check wins over temp).
17
+ expect(classifySensor("CPU1 Temp")).toBe("processor");
18
+ expect(classifySensor("Inlet Temp")).toBe("temperature");
19
+ expect(classifySensor("VCore Voltage")).toBe("voltage");
20
+ expect(classifySensor("Drive Slot 1")).toBe("storage");
21
+ expect(classifySensor("Chassis Intrusion")).toBe("chassis");
22
+ });
23
+ it("falls back to 'other'", () => {
24
+ expect(classifySensor("Weird Sensor")).toBe("other");
25
+ });
26
+ });
27
+ describe("deriveSelSeverity", () => {
28
+ it("treats uncorrectable, thermal trip, AC lost as critical", () => {
29
+ expect(deriveSelSeverity("Uncorrectable ECC", "memory")).toBe("critical");
30
+ expect(deriveSelSeverity("Thermal trip", "processor")).toBe("critical");
31
+ expect(deriveSelSeverity("AC lost", "power")).toBe("critical");
32
+ expect(deriveSelSeverity("Machine check", "processor")).toBe("critical");
33
+ });
34
+ it("treats correctable ECC and redundancy lost as warning", () => {
35
+ expect(deriveSelSeverity("Correctable ECC", "memory")).toBe("warning");
36
+ expect(deriveSelSeverity("Redundancy lost", "power")).toBe("warning");
37
+ });
38
+ it("treats presence detected as info", () => {
39
+ expect(deriveSelSeverity("Presence detected", "memory")).toBe("info");
40
+ });
41
+ it("defaults to warning for memory/power/fan/processor sensor types", () => {
42
+ expect(deriveSelSeverity("Some odd event", "memory")).toBe("warning");
43
+ expect(deriveSelSeverity("Some odd event", "fan")).toBe("warning");
44
+ });
45
+ it("defaults to info for other sensor types", () => {
46
+ expect(deriveSelSeverity("Some odd event", "other")).toBe("info");
47
+ });
48
+ });
49
+ describe("parseSelTimestamp", () => {
50
+ it("formats a known date/time", () => {
51
+ expect(parseSelTimestamp("04/05/2026", "14:23:05")).toBe("2026-04-05T14:23:05Z");
52
+ });
53
+ it("pads single digit month/day", () => {
54
+ expect(parseSelTimestamp("4/5/2026", "09:00:00")).toBe("2026-04-05T09:00:00Z");
55
+ });
56
+ it("returns an ISO string for bad input (does not crash)", () => {
57
+ const out = parseSelTimestamp("", "");
58
+ expect(typeof out).toBe("string");
59
+ expect(out.length).toBeGreaterThan(10);
60
+ });
61
+ });
62
+ describe("parseFanStatus", () => {
63
+ it("parses healthy fan output", () => {
64
+ const raw = [
65
+ "FAN1 | 30h | ok | 7.1 | 5000 RPM",
66
+ "FAN2 | 31h | ok | 7.2 | 5100 RPM",
67
+ ].join("\n");
68
+ const fans = parseFanStatus(raw);
69
+ expect(fans).toHaveLength(2);
70
+ expect(fans[0]).toMatchObject({ name: "FAN1", rpm: 5000, status: "ok" });
71
+ expect(fans[1].rpm).toBe(5100);
72
+ });
73
+ it("marks critical fans (cr/nr) as critical", () => {
74
+ const raw = "FAN1 | 30h | cr | 7.1 | 0 RPM";
75
+ const fans = parseFanStatus(raw);
76
+ expect(fans[0].status).toBe("critical");
77
+ });
78
+ it("marks absent/no-reading fans as absent", () => {
79
+ const raw = "FAN3 | 30h | ns | 7.1 | no reading";
80
+ const fans = parseFanStatus(raw);
81
+ expect(fans[0].status).toBe("absent");
82
+ expect(fans[0].rpm).toBe(0);
83
+ });
84
+ it("treats 0 RPM with no explicit status as critical", () => {
85
+ const raw = "FAN1 | 30h | 7.1 | 0 RPM";
86
+ const fans = parseFanStatus(raw);
87
+ expect(fans[0].status).toBe("critical");
88
+ });
89
+ });
90
+ //# sourceMappingURL=ipmi.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ipmi.test.js","sourceRoot":"","sources":["../../../src/collect/__tests__/ipmi.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseSmartctlJson } from "../smart.js";
3
+ describe("parseSmartctlJson", () => {
4
+ it("parses a healthy SATA SSD", () => {
5
+ const data = {
6
+ model_name: "Samsung SSD 970 EVO 1TB",
7
+ smart_status: { passed: true },
8
+ temperature: { current: 38 },
9
+ power_on_time: { hours: 9000 },
10
+ ata_smart_attributes: {
11
+ table: [
12
+ { id: 5, name: "Reallocated_Sector_Ct", raw: { value: 0 } },
13
+ { id: 197, name: "Current_Pending_Sector", raw: { value: 0 } },
14
+ ],
15
+ },
16
+ };
17
+ const info = parseSmartctlJson(data, "/dev/sda");
18
+ expect(info).toMatchObject({
19
+ device: "/dev/sda",
20
+ model: "Samsung SSD 970 EVO 1TB",
21
+ health: "PASSED",
22
+ temperature_c: 38,
23
+ power_on_hours: 9000,
24
+ reallocated_sectors: 0,
25
+ pending_sectors: 0,
26
+ });
27
+ });
28
+ it("parses a failing SATA drive with reallocated sectors", () => {
29
+ const data = {
30
+ model_name: "WD Red 4TB",
31
+ smart_status: { passed: false },
32
+ ata_smart_attributes: {
33
+ table: [
34
+ { id: 5, raw: { value: 12 } },
35
+ { id: 197, raw: { value: 3 } },
36
+ ],
37
+ },
38
+ };
39
+ const info = parseSmartctlJson(data, "/dev/sdb");
40
+ expect(info.health).toBe("FAILED");
41
+ expect(info.reallocated_sectors).toBe(12);
42
+ expect(info.pending_sectors).toBe(3);
43
+ });
44
+ it("parses an NVMe drive with percentage_used", () => {
45
+ const data = {
46
+ model_name: "Samsung 980 PRO",
47
+ smart_status: { passed: true },
48
+ nvme_smart_health_information_log: { percentage_used: 22, temperature: 41 },
49
+ };
50
+ const info = parseSmartctlJson(data, "/dev/nvme0n1");
51
+ expect(info.percentage_used).toBe(22);
52
+ expect(info.temperature_c).toBe(41);
53
+ expect(info.health).toBe("PASSED");
54
+ });
55
+ it("falls back to 'unknown' model when absent", () => {
56
+ const info = parseSmartctlJson({ smart_status: { passed: true } }, "/dev/sdc");
57
+ expect(info.model).toBe("unknown");
58
+ });
59
+ it("treats missing smart_status as FAILED (safer default)", () => {
60
+ const info = parseSmartctlJson({}, "/dev/sdd");
61
+ expect(info.health).toBe("FAILED");
62
+ });
63
+ });
64
+ //# sourceMappingURL=smart.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smart.test.js","sourceRoot":"","sources":["../../../src/collect/__tests__/smart.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,68 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseZpoolStatus } from "../zfs.js";
3
+ describe("parseZpoolStatus", () => {
4
+ it("parses a healthy pool", () => {
5
+ const raw = ` pool: tank
6
+ state: ONLINE
7
+ scan: scrub repaired 0B in 01:23:45 with 0 errors on Sun Apr 5 12:34:56 2026
8
+ config:
9
+
10
+ NAME STATE READ WRITE CKSUM
11
+ tank ONLINE 0 0 0
12
+ mirror-0 ONLINE 0 0 0
13
+
14
+ errors: No known data errors
15
+ `;
16
+ const pools = parseZpoolStatus(raw);
17
+ expect(pools).toHaveLength(1);
18
+ expect(pools[0]).toMatchObject({
19
+ name: "tank",
20
+ state: "ONLINE",
21
+ errors_text: "No known data errors",
22
+ scrub_errors: 0,
23
+ scrub_repaired: "0B",
24
+ });
25
+ expect(pools[0].last_scrub_date).toContain("2026");
26
+ });
27
+ it("parses a DEGRADED pool", () => {
28
+ const raw = ` pool: tank
29
+ state: DEGRADED
30
+ scan: scrub repaired 16K in 02:00:00 with 3 errors on Sun Apr 5 12:34:56 2026
31
+
32
+ errors: 3 data errors, use '-v' for a list
33
+ `;
34
+ const [p] = parseZpoolStatus(raw);
35
+ expect(p.state).toBe("DEGRADED");
36
+ expect(p.scrub_errors).toBe(3);
37
+ expect(p.scrub_repaired).toBe("16K");
38
+ });
39
+ it("flags never-scrubbed pools", () => {
40
+ const raw = ` pool: tank
41
+ state: ONLINE
42
+ scan: none requested
43
+
44
+ errors: No known data errors
45
+ `;
46
+ const [p] = parseZpoolStatus(raw);
47
+ expect(p.scrub_never_run).toBe(true);
48
+ expect(p.scrub_errors).toBeUndefined();
49
+ });
50
+ it("returns empty for no pools", () => {
51
+ expect(parseZpoolStatus("no pools available")).toEqual([]);
52
+ });
53
+ it("parses multiple pools", () => {
54
+ const raw = ` pool: tank
55
+ state: ONLINE
56
+ scan: none requested
57
+ errors: No known data errors
58
+ pool: data
59
+ state: FAULTED
60
+ scan: none requested
61
+ errors: 2 data errors
62
+ `;
63
+ const pools = parseZpoolStatus(raw);
64
+ expect(pools.map((p) => p.name)).toEqual(["tank", "data"]);
65
+ expect(pools[1].state).toBe("FAULTED");
66
+ });
67
+ });
68
+ //# sourceMappingURL=zfs.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zfs.test.js","sourceRoot":"","sources":["../../../src/collect/__tests__/zfs.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;QAC/B,MAAM,GAAG,GAAG;;;;;;;;;;CAUf,CAAC;QACE,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;QACpC,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC;YAC7B,IAAI,EAAE,MAAM;YACZ,KAAK,EAAE,QAAQ;YACf,WAAW,EAAE,sBAAsB;YACnC,YAAY,EAAE,CAAC;YACf,cAAc,EAAE,IAAI;SACrB,CAAC,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,MAAM,GAAG,GAAG;;;;;CAKf,CAAC;QACE,MAAM,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;QAClC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACjC,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,GAAG,GAAG;;;;;CAKf,CAAC;QACE,MAAM,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;QAClC,MAAM,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrC,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,aAAa,EAAE,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,gBAAgB,CAAC,oBAAoB,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;QAC/B,MAAM,GAAG,GAAG;;;;;;;;CAQf,CAAC;QACE,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;QACpC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QAC3D,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -1,2 +1,6 @@
1
- import type { IpmiInfo } from "../lib/types.js";
1
+ import type { IpmiInfo, FanStatus } from "../lib/types.js";
2
2
  export declare function collectIpmi(): Promise<IpmiInfo>;
3
+ export declare function parseSelTimestamp(date: string, time: string): string;
4
+ export declare function classifySensor(sensor: string): string;
5
+ export declare function deriveSelSeverity(event: string, sensorType: string): string;
6
+ export declare function parseFanStatus(output: string): FanStatus[];
@@ -89,7 +89,7 @@ async function collectSelEvents() {
89
89
  // Return last 20 events, most recent first
90
90
  return events.slice(-20).reverse();
91
91
  }
92
- function parseSelTimestamp(date, time) {
92
+ export function parseSelTimestamp(date, time) {
93
93
  if (!date || !time)
94
94
  return new Date().toISOString();
95
95
  // Format: "04/05/2026" and "14:23:05"
@@ -99,7 +99,7 @@ function parseSelTimestamp(date, time) {
99
99
  const [month, day, year] = parts;
100
100
  return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}T${time}Z`;
101
101
  }
102
- function classifySensor(sensor) {
102
+ export function classifySensor(sensor) {
103
103
  const lower = sensor.toLowerCase();
104
104
  if (lower.includes("memory") || lower.includes("dimm"))
105
105
  return "memory";
@@ -121,7 +121,7 @@ function classifySensor(sensor) {
121
121
  return "chassis";
122
122
  return "other";
123
123
  }
124
- function deriveSelSeverity(event, sensorType) {
124
+ export function deriveSelSeverity(event, sensorType) {
125
125
  const lower = event.toLowerCase();
126
126
  // Critical events
127
127
  if (lower.includes("uncorrectable"))
@@ -174,6 +174,9 @@ async function collectFanStatus() {
174
174
  const output = await run("ipmitool", ["sdr", "type", "Fan"]);
175
175
  if (!output)
176
176
  return [];
177
+ return parseFanStatus(output);
178
+ }
179
+ export function parseFanStatus(output) {
177
180
  const fans = [];
178
181
  const lines = output.trim().split("\n");
179
182
  for (const line of lines) {