@glassmkr/crucible 0.13.13 → 0.13.15

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/README.md CHANGED
@@ -14,16 +14,16 @@ Open source. MIT licensed. Built by [Glassmkr](https://glassmkr.com). Crucible i
14
14
 
15
15
  ## Screenshots
16
16
 
17
- ![Dashboard alert with copy-pasteable fix commands](https://glassmkr.com/screenshots/alerts.png)
17
+ ![Dashboard alert with copy-pasteable fix commands](https://glassmkr.com/screenshots/alerts.png?v=20260701)
18
18
  *A P1 alert showing the rule trigger, evidence, and the exact remediation
19
19
  commands. Each rule ships pre-written fix content; the agent does not write
20
20
  to your server.*
21
21
 
22
- ![Storage and SMART drive health](https://glassmkr.com/screenshots/hardware.png)
22
+ ![Storage and SMART drive health](https://glassmkr.com/screenshots/hardware.png?v=20260701)
23
23
  *Per-mount capacity and per-disk SMART status. Drives are checked against
24
24
  SMART attributes, NVMe Critical Warning bits, and ZFS pool state.*
25
25
 
26
- ![Server fleet overview](https://glassmkr.com/screenshots/overview.png)
26
+ ![Server fleet overview](https://glassmkr.com/screenshots/overview.png?v=20260701)
27
27
  *Fleet view with per-server status, distro, IP, and last-seen timestamp.
28
28
  Alerted servers surface a counter at a glance.*
29
29
 
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ // Tests for parseWtmp: the clean-shutdown detection that feeds the dashboard's
2
+ // unexpected_reboot rule. Regression for the round-3 false positive where a
3
+ // deliberate `sudo reboot` was reported as an unclean shutdown because the old
4
+ // `last shutdown -F` command returns nothing on modern systemd/util-linux.
5
+ import { describe, expect, it } from "vitest";
6
+ import { parseWtmp } from "../reboot-evidence.js";
7
+ // Real `last -x -F` shapes (most-recent-first).
8
+ const CLEAN = `reboot system boot 6.17.0-35-generic Sat Jun 27 19:22:28 2026 still running
9
+ shutdown system down 6.17.0-23-generic Sat Jun 27 19:20:39 2026 - Sat Jun 27 19:22:28 2026 (00:01)
10
+ reboot system boot 6.17.0-23-generic Wed May 20 21:32:36 2026 - Sat Jun 27 19:20:39 2026 (37+21:48)
11
+
12
+ wtmp begins Tue Apr 7 22:14:51 2026`;
13
+ const UNCLEAN = `reboot system boot 6.17.0-35-generic Sat Jun 27 19:22:28 2026 still running
14
+ reboot system boot 6.17.0-23-generic Wed May 20 21:32:36 2026 - Sat Jun 27 19:22:00 2026 (37+21:49)
15
+
16
+ wtmp begins Tue Apr 7 22:14:51 2026`;
17
+ // runlevel + user-login records interleave in real output; the parser must
18
+ // filter to reboot/shutdown system lines before checking adjacency.
19
+ const CLEAN_INTERLEAVED = `simon pts/0 198.51.100.4 Sat Jun 27 20:00:00 2026 still logged in
20
+ runlevel (to lvl 5) 6.17.0-35-generic Sat Jun 27 19:22:30 2026 still running
21
+ reboot system boot 6.17.0-35-generic Sat Jun 27 19:22:28 2026 still running
22
+ shutdown system down 6.17.0-23-generic Sat Jun 27 19:20:39 2026 - Sat Jun 27 19:22:28 2026 (00:01)
23
+ reboot system boot 6.17.0-23-generic Wed May 20 21:32:36 2026 - Sat Jun 27 19:20:39 2026 (37+21:48)`;
24
+ describe("parseWtmp", () => {
25
+ it("clean: a shutdown record immediately before the most recent boot", () => {
26
+ const r = parseWtmp(CLEAN);
27
+ expect(r.prior_shutdown_clean).toBe(true);
28
+ expect(r.last_reboot_raw).toContain("reboot");
29
+ expect(r.last_reboot_raw).toContain("6.17.0-35");
30
+ });
31
+ it("unclean: two boots back-to-back with no shutdown between (hard reset)", () => {
32
+ const r = parseWtmp(UNCLEAN);
33
+ expect(r.prior_shutdown_clean).toBe(false);
34
+ expect(r.last_reboot_raw).toContain("6.17.0-35");
35
+ });
36
+ it("clean even when runlevel and login records interleave", () => {
37
+ expect(parseWtmp(CLEAN_INTERLEAVED).prior_shutdown_clean).toBe(true);
38
+ });
39
+ it("empty / no system records: not clean, null reboot", () => {
40
+ expect(parseWtmp("")).toEqual({ last_reboot_raw: null, prior_shutdown_clean: false });
41
+ expect(parseWtmp("wtmp begins Tue Apr 7 22:14:51 2026").prior_shutdown_clean).toBe(false);
42
+ });
43
+ it("first boot ever (single reboot, no prior shutdown) is not clean", () => {
44
+ const r = parseWtmp("reboot system boot 6.17.0-35-generic Sat Jun 27 19:22:28 2026 still running");
45
+ expect(r.prior_shutdown_clean).toBe(false);
46
+ expect(r.last_reboot_raw).toContain("reboot");
47
+ });
48
+ });
49
+ //# sourceMappingURL=reboot-evidence.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reboot-evidence.test.js","sourceRoot":"","sources":["../../../src/collect/__tests__/reboot-evidence.test.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,4EAA4E;AAC5E,+EAA+E;AAC/E,2EAA2E;AAE3E,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD,gDAAgD;AAChD,MAAM,KAAK,GAAG;;;;qCAIuB,CAAC;AAEtC,MAAM,OAAO,GAAG;;;qCAGqB,CAAC;AAEtC,2EAA2E;AAC3E,oEAAoE;AACpE,MAAM,iBAAiB,GAAG;;;;uGAI6E,CAAC;AAExG,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC1E,MAAM,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;QAC3B,MAAM,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1C,MAAM,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC9C,MAAM,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uEAAuE,EAAE,GAAG,EAAE;QAC/E,MAAM,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;QAC7B,MAAM,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3C,MAAM,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,eAAe,EAAE,IAAI,EAAE,oBAAoB,EAAE,KAAK,EAAE,CAAC,CAAC;QACtF,MAAM,CAAC,SAAS,CAAC,sCAAsC,CAAC,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,MAAM,CAAC,GAAG,SAAS,CAAC,kFAAkF,CAAC,CAAC;QACxG,MAAM,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3C,MAAM,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -84,5 +84,49 @@ describe("parseSmartctlJson", () => {
84
84
  const info = parseSmartctlJson({}, "/dev/sdd");
85
85
  expect(info.health).toBe("FAILED");
86
86
  });
87
+ it("derives percentage_used from a SATA SSD wear attribute (Crucial MX500 ID 202)", () => {
88
+ // Percent_Lifetime_Remain normalized value 25 = 25% life left = 75% used.
89
+ const info = parseSmartctlJson({
90
+ model_name: "CT500MX500SSD1",
91
+ smart_status: { passed: true },
92
+ ata_smart_attributes: {
93
+ table: [
94
+ { id: 5, name: "Reallocated_Sector_Ct", value: 100, raw: { value: 0 } },
95
+ { id: 202, name: "Percent_Lifetime_Remain", value: 25, raw: { value: 75 } },
96
+ ],
97
+ },
98
+ }, "/dev/sdb");
99
+ expect(info.percentage_used).toBe(75);
100
+ expect(info.reallocated_sectors).toBe(0);
101
+ });
102
+ it("reads a fresh SATA SSD wear attribute as 0% used", () => {
103
+ const info = parseSmartctlJson({
104
+ model_name: "Samsung SSD 870 EVO",
105
+ smart_status: { passed: true },
106
+ ata_smart_attributes: { table: [{ id: 177, name: "Wear_Leveling_Count", value: 100, raw: { value: 0 } }] },
107
+ }, "/dev/sda");
108
+ expect(info.percentage_used).toBe(0);
109
+ });
110
+ it("does not treat ID 231 Temperature as a wear attribute", () => {
111
+ const info = parseSmartctlJson({
112
+ model_name: "Some SSD",
113
+ smart_status: { passed: true },
114
+ ata_smart_attributes: { table: [{ id: 231, name: "Temperature_Celsius", value: 65, raw: { value: 35 } }] },
115
+ }, "/dev/sda");
116
+ expect(info.percentage_used).toBeUndefined();
117
+ });
118
+ it("takes the most-worn wear attribute when several are present", () => {
119
+ const info = parseSmartctlJson({
120
+ model_name: "Intel SSD",
121
+ smart_status: { passed: true },
122
+ ata_smart_attributes: {
123
+ table: [
124
+ { id: 233, name: "Media_Wearout_Indicator", value: 60, raw: { value: 40 } },
125
+ { id: 173, name: "Ave_Block-Erase_Count", value: 30, raw: { value: 70 } },
126
+ ],
127
+ },
128
+ }, "/dev/sda");
129
+ expect(info.percentage_used).toBe(70);
130
+ });
87
131
  });
88
132
  //# sourceMappingURL=smart.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"smart.test.js","sourceRoot":"","sources":["../../../src/collect/__tests__/smart.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"smart.test.js","sourceRoot":"","sources":["../../../src/collect/__tests__/smart.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1,28 @@
1
1
  import type { RebootEvidence } from "../lib/types.js";
2
+ interface WtmpRebootRecord {
3
+ /** Last `reboot` line, e.g. "reboot system boot 5.15.0 Wed May 13 05:21:17 2026 still running". */
4
+ last_reboot_raw: string | null;
5
+ /** True if the wtmp log shows a `shutdown` record immediately before
6
+ * the most recent reboot; false if only the reboot is present
7
+ * (suggests power loss or hard reset). */
8
+ prior_shutdown_clean: boolean;
9
+ }
10
+ /**
11
+ * Parse `last -x -F` output into the reboot record. Pure; exported for tests.
12
+ *
13
+ * The `-x` flag is essential: it surfaces the `shutdown` and `runlevel` system
14
+ * pseudo-records. Plain `last reboot` / `last shutdown` do NOT show the
15
+ * shutdown record on modern systemd + util-linux (verified on Ubuntu, 6.17
16
+ * kernels): `last shutdown` returns nothing even after a clean `sudo reboot`,
17
+ * which made prior_shutdown_clean false on EVERY clean reboot and escalated
18
+ * deliberate reboots to a critical "unclean-shutdown" alert (false positive).
19
+ *
20
+ * Output is most-recent-first. We keep only the `reboot`/`shutdown` system
21
+ * lines so interleaved `runlevel` and user-login records do not break
22
+ * adjacency. The most recent boot was clean iff a `shutdown` record sits
23
+ * immediately before it (the next system record). No shutdown before the boot
24
+ * means a hard reset / power loss / panic.
25
+ */
26
+ export declare function parseWtmp(output: string): WtmpRebootRecord;
2
27
  export declare function collectRebootEvidence(): Promise<RebootEvidence>;
28
+ export {};
@@ -61,33 +61,38 @@ function vmcorePresent() {
61
61
  return false;
62
62
  }
63
63
  }
64
+ /**
65
+ * Parse `last -x -F` output into the reboot record. Pure; exported for tests.
66
+ *
67
+ * The `-x` flag is essential: it surfaces the `shutdown` and `runlevel` system
68
+ * pseudo-records. Plain `last reboot` / `last shutdown` do NOT show the
69
+ * shutdown record on modern systemd + util-linux (verified on Ubuntu, 6.17
70
+ * kernels): `last shutdown` returns nothing even after a clean `sudo reboot`,
71
+ * which made prior_shutdown_clean false on EVERY clean reboot and escalated
72
+ * deliberate reboots to a critical "unclean-shutdown" alert (false positive).
73
+ *
74
+ * Output is most-recent-first. We keep only the `reboot`/`shutdown` system
75
+ * lines so interleaved `runlevel` and user-login records do not break
76
+ * adjacency. The most recent boot was clean iff a `shutdown` record sits
77
+ * immediately before it (the next system record). No shutdown before the boot
78
+ * means a hard reset / power loss / panic.
79
+ */
80
+ export function parseWtmp(output) {
81
+ const sysLines = output
82
+ .split("\n")
83
+ .map((l) => l.trim())
84
+ .filter((l) => l.startsWith("reboot") || l.startsWith("shutdown"));
85
+ const lastReboot = sysLines[0]?.startsWith("reboot") ? sysLines[0] : null;
86
+ const prior_shutdown_clean = sysLines[0]?.startsWith("reboot") === true &&
87
+ sysLines[1]?.startsWith("shutdown") === true;
88
+ return { last_reboot_raw: lastReboot, prior_shutdown_clean };
89
+ }
64
90
  async function readWtmp() {
65
- const output = await run("last", ["reboot", "-F"], 5000);
91
+ const output = await run("last", ["-x", "-F"], 5000);
66
92
  if (!output) {
67
93
  return { last_reboot_raw: null, prior_shutdown_clean: false };
68
94
  }
69
- const lines = output.split("\n").filter((l) => l.trim().length > 0);
70
- // The first non-empty line is the most recent reboot record.
71
- const lastReboot = lines[0] ?? null;
72
- // Look for a `shutdown` record between the most recent reboot and
73
- // the second-most-recent reboot. `last -F` interleaves shutdown
74
- // records when present; on a clean shutdown they sit immediately
75
- // before the boot record.
76
- const shutdownOutput = await run("last", ["shutdown", "-F"], 5000);
77
- let priorShutdownClean = false;
78
- if (shutdownOutput && lastReboot) {
79
- // Extract a date marker from the lastReboot line; if any shutdown
80
- // record predates this reboot by <= 5 minutes, treat as clean.
81
- // Conservative: if any shutdown record exists in the wtmp at all,
82
- // assume the most recent one was clean. Defensive against parsing
83
- // glitches; refined by the dashboard evaluator.
84
- const sLines = shutdownOutput.split("\n").filter((l) => l.trim().length > 0 && l.startsWith("shutdown"));
85
- priorShutdownClean = sLines.length > 0;
86
- }
87
- return {
88
- last_reboot_raw: lastReboot,
89
- prior_shutdown_clean: priorShutdownClean,
90
- };
95
+ return parseWtmp(output);
91
96
  }
92
97
  export async function collectRebootEvidence() {
93
98
  const records = pstoreRecords();
@@ -1 +1 @@
1
- {"version":3,"file":"reboot-evidence.js","sourceRoot":"","sources":["../../src/collect/reboot-evidence.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"reboot-evidence.js","sourceRoot":"","sources":["../../src/collect/reboot-evidence.ts"],"names":[],"mappings":"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"}
@@ -26,6 +26,7 @@ export declare function parseSmartctlJson(data: Record<string, unknown> & {
26
26
  table?: Array<{
27
27
  id?: number;
28
28
  name?: string;
29
+ value?: number;
29
30
  raw?: {
30
31
  value?: number;
31
32
  };
@@ -60,6 +60,18 @@ export function parseSmartctlJson(data, device) {
60
60
  }
61
61
  // SATA specific
62
62
  if (data.ata_smart_attributes?.table) {
63
+ // SSD endurance/wear. Unlike NVMe (which has a dedicated percentage_used
64
+ // field), a SATA SSD reports wear through a vendor-specific attribute whose
65
+ // NORMALIZED value is "% life remaining" (100 = new, counts down toward the
66
+ // failure threshold). Convert the most-worn such attribute into
67
+ // percentage_used so a SATA SSD flows the SAME wear field NVMe does, which
68
+ // is what the dashboard wear rule + trend engine read. Without this a worn
69
+ // SATA SSD (e.g. a Crucial MX500 at 25% life remaining) is invisible to
70
+ // wear detection. Match by attribute NAME (authoritative, and disambiguates
71
+ // ID 231, which is wear on some drives and temperature on others) with a
72
+ // known-ID fallback (Micron/Crucial 202, Intel 233, Samsung 177, others
73
+ // 173/231); skip anything that looks like a temperature attribute.
74
+ let ssdWearUsedPct = null;
63
75
  for (const attr of data.ata_smart_attributes.table) {
64
76
  if (attr.id === 5 || attr.name === "Reallocated_Sector_Ct") {
65
77
  info.reallocated_sectors = attr.raw?.value || 0;
@@ -67,6 +79,23 @@ export function parseSmartctlJson(data, device) {
67
79
  if (attr.id === 197 || attr.name === "Current_Pending_Sector") {
68
80
  info.pending_sectors = attr.raw?.value || 0;
69
81
  }
82
+ const name = (attr.name || "").toLowerCase();
83
+ const isWearName = /wear.?level|wearout|life.?left|life.?time|percent.?life|ssd.?life|endurance/.test(name);
84
+ const isWearId = attr.id === 202 || attr.id === 233 || attr.id === 177 || attr.id === 173 || attr.id === 231;
85
+ const looksTemperature = name.includes("temp");
86
+ if ((isWearName || isWearId) && !looksTemperature && typeof attr.value === "number") {
87
+ // Normalized value is life remaining; used = 100 - remaining. Take the
88
+ // most-worn (highest used) across candidates: conservative for a
89
+ // plan-replacement signal.
90
+ const used = Math.min(100, Math.max(0, 100 - attr.value));
91
+ if (ssdWearUsedPct == null || used > ssdWearUsedPct)
92
+ ssdWearUsedPct = used;
93
+ }
94
+ }
95
+ // Only fill percentage_used for a SATA drive; NVMe already set it from its
96
+ // own health log above.
97
+ if (ssdWearUsedPct != null && info.percentage_used == null) {
98
+ info.percentage_used = ssdWearUsedPct;
70
99
  }
71
100
  }
72
101
  return info;
@@ -1 +1 @@
1
- {"version":3,"file":"smart.js","sourceRoot":"","sources":["../../src/collect/smart.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"smart.js","sourceRoot":"","sources":["../../src/collect/smart.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassmkr/crucible",
3
- "version": "0.13.13",
3
+ "version": "0.13.15",
4
4
  "description": "Lightweight bare metal server monitoring. IPMI, SMART, OS, network. Opinionated alerts.",
5
5
  "type": "module",
6
6
  "engines": {