@glassmkr/crucible 0.6.4 → 0.6.6

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/.dockerignore ADDED
@@ -0,0 +1,13 @@
1
+ node_modules
2
+ .git
3
+ .github
4
+ *.md
5
+ !README.md
6
+ .svelte-kit
7
+ dist
8
+ coverage
9
+ .env*
10
+ .vscode
11
+ .idea
12
+ *.log
13
+ .DS_Store
@@ -0,0 +1,53 @@
1
+ name: Build and publish Docker image
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ['v*']
7
+
8
+ permissions:
9
+ contents: read
10
+ packages: write
11
+
12
+ jobs:
13
+ docker:
14
+ name: Build and push to ghcr.io and docker.io
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: docker/setup-buildx-action@v3
20
+
21
+ - uses: docker/login-action@v3
22
+ with:
23
+ registry: ghcr.io
24
+ username: ${{ github.actor }}
25
+ password: ${{ secrets.GITHUB_TOKEN }}
26
+
27
+ - uses: docker/login-action@v3
28
+ with:
29
+ registry: docker.io
30
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
31
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
32
+
33
+ - uses: docker/metadata-action@v5
34
+ id: meta
35
+ with:
36
+ images: |
37
+ ghcr.io/glassmkr/crucible
38
+ docker.io/glassmkr/crucible
39
+ tags: |
40
+ type=semver,pattern={{version}}
41
+ type=semver,pattern={{major}}.{{minor}}
42
+ type=raw,value=latest,enable={{is_default_branch}}
43
+ type=sha,prefix=sha-,format=short
44
+
45
+ - uses: docker/build-push-action@v6
46
+ with:
47
+ context: .
48
+ platforms: linux/amd64
49
+ push: true
50
+ tags: ${{ steps.meta.outputs.tags }}
51
+ labels: ${{ steps.meta.outputs.labels }}
52
+ cache-from: type=gha
53
+ cache-to: type=gha,mode=max
package/Dockerfile ADDED
@@ -0,0 +1,59 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ # Multi-stage build for Glassmkr Crucible monitoring agent.
3
+
4
+ # ---------- Stage 1: build TypeScript to dist/ ----------
5
+ FROM node:24-slim AS builder
6
+ WORKDIR /build
7
+ COPY package.json package-lock.json* ./
8
+ RUN npm install --include=dev --no-audit --no-fund
9
+ COPY tsconfig.json ./
10
+ COPY src ./src
11
+ RUN npm run build
12
+
13
+ # ---------- Stage 2: production runtime ----------
14
+ FROM node:24-slim AS runtime
15
+
16
+ # Hardware monitoring tools. Crucible shells out to these; they must be on PATH.
17
+ RUN apt-get update && apt-get install -y --no-install-recommends \
18
+ smartmontools \
19
+ ipmitool \
20
+ dmidecode \
21
+ lm-sensors \
22
+ ethtool \
23
+ util-linux \
24
+ procps \
25
+ net-tools \
26
+ iproute2 \
27
+ ca-certificates \
28
+ && rm -rf /var/lib/apt/lists/*
29
+
30
+ WORKDIR /app
31
+
32
+ # Production node_modules only.
33
+ COPY package.json package-lock.json* ./
34
+ RUN npm ci --omit=dev --no-audit --no-fund && npm cache clean --force
35
+
36
+ # Built code.
37
+ COPY --from=builder /build/dist ./dist
38
+
39
+ # Create a non-root user for future use. IPMI and SMART typically require root,
40
+ # so the container is expected to run with --privileged or cap_add DAC_READ_SEARCH etc.
41
+ # Keeping the user available lets operators drop privileges when hardware access is not needed.
42
+ RUN useradd --system --no-create-home --shell /usr/sbin/nologin glassmkr
43
+
44
+ # Crucible reads /etc/glassmkr/collector.yaml by default.
45
+ # Mount the host config directory at this path.
46
+ RUN mkdir -p /etc/glassmkr
47
+
48
+ # Container health: verify the Node process is actually running and hasn't crashed.
49
+ HEALTHCHECK --interval=60s --timeout=10s --start-period=30s --retries=3 \
50
+ CMD pgrep -f "node /app/dist/index.js" > /dev/null || exit 1
51
+
52
+ LABEL org.opencontainers.image.source="https://github.com/glassmkr/crucible" \
53
+ org.opencontainers.image.description="Glassmkr Crucible - bare metal server monitoring agent" \
54
+ org.opencontainers.image.licenses="MIT" \
55
+ org.opencontainers.image.title="Crucible" \
56
+ org.opencontainers.image.vendor="Glassmkr"
57
+
58
+ # Crucible does not listen on any port; data flows outbound to Forge.
59
+ ENTRYPOINT ["node", "/app/dist/index.js"]
package/README.md CHANGED
@@ -3,10 +3,25 @@
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 every 5 minutes and pushes snapshots to a [Forge](https://forge.glassmkr.com) dashboard, which 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 38 alert rules and sends notifications.
7
7
 
8
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
+ **Resource usage:** ~90MB RSS memory (varies by hardware: servers with more IPMI sensors use more), <0.1% CPU at 5-minute collection interval. Collects IPMI, SMART, ZFS, network bonds, security posture, conntrack, systemd, NTP, and file descriptors.
11
+
12
+ **Security:** See [glassmkr.com/security](https://glassmkr.com/security) for the full list of what Crucible does and does not collect.
13
+
14
+ ## Screenshots
15
+
16
+ ![Forge alerts with fix commands](https://glassmkr.com/screenshots/alerts.png)
17
+ *Alerts grouped by server, with AI-generated fix commands for each rule.*
18
+
19
+ ![Storage, SMART health, and network bonds](https://glassmkr.com/screenshots/hardware.png)
20
+ *Per-disk SMART status, storage capacity, and network interface bonding.*
21
+
22
+ ![Security posture and server overview](https://glassmkr.com/screenshots/overview.png)
23
+ *Security posture, server overview, and active alerts.*
24
+
10
25
  ## Install
11
26
 
12
27
  ```bash
@@ -19,6 +34,35 @@ Or use the bootstrap script:
19
34
  curl -sf https://forge.glassmkr.com/install | bash
20
35
  ```
21
36
 
37
+ ## Docker
38
+
39
+ ```bash
40
+ # Create config directory
41
+ sudo mkdir -p /etc/glassmkr
42
+
43
+ # Create config (replace with your Forge credentials)
44
+ sudo tee /etc/glassmkr/collector.yaml << 'EOF'
45
+ server_name: "web-01"
46
+ collection:
47
+ interval_seconds: 300
48
+ ipmi: true
49
+ smart: true
50
+ forge:
51
+ enabled: true
52
+ url: "https://forge.glassmkr.com"
53
+ api_key: "col_YOUR_KEY_HERE"
54
+ EOF
55
+
56
+ # Run with docker compose
57
+ curl -O https://raw.githubusercontent.com/glassmkr/crucible/main/docker-compose.yml
58
+ docker compose up -d
59
+
60
+ # Check logs
61
+ docker compose logs -f crucible
62
+ ```
63
+
64
+ Images are published to [ghcr.io/glassmkr/crucible](https://github.com/glassmkr/crucible/pkgs/container/crucible) on every tag release. The container needs `--privileged` and `network_mode: host` for IPMI, SMART, and accurate host network monitoring. Details in the [compose file](./docker-compose.yml).
65
+
22
66
  ## Quick Start
23
67
 
24
68
  1. Create an API key in the Forge dashboard (Servers, then Add server).
@@ -104,7 +148,7 @@ sudo systemctl status glassmkr-crucible
104
148
  | NTP | Sync state and source |
105
149
  | File descriptors | System-wide allocation |
106
150
 
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).
151
+ Forge evaluates 38 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).
108
152
 
109
153
  ## Requirements
110
154
 
@@ -1,6 +1,16 @@
1
1
  import { readProcFile, sleep } from "../lib/parse.js";
2
- import { readFileSync } from "fs";
2
+ import { readFileSync, readdirSync } from "fs";
3
3
  const previousCounters = new Map();
4
+ function readStatCounter(iface, name) {
5
+ try {
6
+ const raw = readFileSync(`/sys/class/net/${iface}/statistics/${name}`, "utf-8").trim();
7
+ const val = parseInt(raw, 10);
8
+ return Number.isFinite(val) ? val : undefined;
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ }
4
14
  function parseNetDev() {
5
15
  const raw = readProcFile("/proc/net/dev") || "";
6
16
  const result = {};
@@ -30,6 +40,36 @@ function getSpeed(iface) {
30
40
  return 0;
31
41
  }
32
42
  }
43
+ function getOperstate(iface) {
44
+ try {
45
+ return readFileSync(`/sys/class/net/${iface}/operstate`, "utf-8").trim();
46
+ }
47
+ catch {
48
+ return "unknown";
49
+ }
50
+ }
51
+ function getBondMaster(iface) {
52
+ try {
53
+ const bonds = readdirSync("/proc/net/bonding/");
54
+ for (const bond of bonds) {
55
+ const content = readFileSync(`/proc/net/bonding/${bond}`, "utf-8");
56
+ if (content.includes(`Slave Interface: ${iface}`))
57
+ return bond;
58
+ }
59
+ }
60
+ catch {
61
+ // No bonds or /proc/net/bonding doesn't exist
62
+ }
63
+ return undefined;
64
+ }
65
+ function isBondMaster(iface) {
66
+ try {
67
+ return readdirSync("/proc/net/bonding/").includes(iface);
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ }
33
73
  // Compute delta, handling counter wraps (current < previous means reset, use current as delta)
34
74
  function delta(current, previous) {
35
75
  if (current >= previous)
@@ -48,16 +88,39 @@ export async function collectNetwork() {
48
88
  continue;
49
89
  currentIfaces.add(name);
50
90
  const prev = previousCounters.get(name);
91
+ // /sys/class/net/*/statistics/ exposes finer-grained RX/TX subtype
92
+ // counters than /proc/net/dev. Read cumulative values here; delta is
93
+ // derived below against the previous cycle's snapshot.
94
+ const rxCrcCum = readStatCounter(name, "rx_crc_errors");
95
+ const rxFrameCum = readStatCounter(name, "rx_frame_errors");
96
+ const rxLenCum = readStatCounter(name, "rx_length_errors");
97
+ const txCarrierCum = readStatCounter(name, "tx_carrier_errors");
51
98
  // Compute error/drop deltas (0 on first cycle after start or new interface)
52
99
  let rxErrorsDelta = 0;
53
100
  let txErrorsDelta = 0;
54
101
  let rxDropsDelta = 0;
55
102
  let txDropsDelta = 0;
103
+ let rxPacketsDelta = 0;
104
+ let txPacketsDelta = 0;
105
+ let rxCrcDelta;
106
+ let rxFrameDelta;
107
+ let rxLenDelta;
108
+ let txCarrierDelta;
56
109
  if (prev) {
57
110
  rxErrorsDelta = delta(s2.rx_errors, prev.rx_errors);
58
111
  txErrorsDelta = delta(s2.tx_errors, prev.tx_errors);
59
112
  rxDropsDelta = delta(s2.rx_drops, prev.rx_drops);
60
113
  txDropsDelta = delta(s2.tx_drops, prev.tx_drops);
114
+ rxPacketsDelta = delta(s2.rx_packets, prev.rx_packets);
115
+ txPacketsDelta = delta(s2.tx_packets, prev.tx_packets);
116
+ if (rxCrcCum != null && prev.rx_crc_errors != null)
117
+ rxCrcDelta = delta(rxCrcCum, prev.rx_crc_errors);
118
+ if (rxFrameCum != null && prev.rx_frame_errors != null)
119
+ rxFrameDelta = delta(rxFrameCum, prev.rx_frame_errors);
120
+ if (rxLenCum != null && prev.rx_length_errors != null)
121
+ rxLenDelta = delta(rxLenCum, prev.rx_length_errors);
122
+ if (txCarrierCum != null && prev.tx_carrier_errors != null)
123
+ txCarrierDelta = delta(txCarrierCum, prev.tx_carrier_errors);
61
124
  }
62
125
  // Store current cumulative values for next cycle
63
126
  previousCounters.set(name, {
@@ -65,8 +128,14 @@ export async function collectNetwork() {
65
128
  tx_errors: s2.tx_errors,
66
129
  rx_drops: s2.rx_drops,
67
130
  tx_drops: s2.tx_drops,
131
+ rx_packets: s2.rx_packets,
132
+ tx_packets: s2.tx_packets,
133
+ rx_crc_errors: rxCrcCum,
134
+ rx_frame_errors: rxFrameCum,
135
+ rx_length_errors: rxLenCum,
136
+ tx_carrier_errors: txCarrierCum,
68
137
  });
69
- results.push({
138
+ const entry = {
70
139
  interface: name,
71
140
  speed_mbps: getSpeed(name),
72
141
  rx_bytes_sec: s2.rx_bytes - s1.rx_bytes, // already a 1-second delta
@@ -75,7 +144,25 @@ export async function collectNetwork() {
75
144
  tx_errors: txErrorsDelta,
76
145
  rx_drops: rxDropsDelta,
77
146
  tx_drops: txDropsDelta,
78
- });
147
+ rx_packets: rxPacketsDelta,
148
+ tx_packets: txPacketsDelta,
149
+ operstate: getOperstate(name),
150
+ };
151
+ if (rxCrcDelta !== undefined)
152
+ entry.rx_crc_errors = rxCrcDelta;
153
+ if (rxFrameDelta !== undefined)
154
+ entry.rx_frame_errors = rxFrameDelta;
155
+ if (rxLenDelta !== undefined)
156
+ entry.rx_length_errors = rxLenDelta;
157
+ if (txCarrierDelta !== undefined)
158
+ entry.tx_carrier_errors = txCarrierDelta;
159
+ const master = getBondMaster(name);
160
+ if (master)
161
+ entry.bond_master = master;
162
+ // Identify bond masters (have at least one slave pointing at them).
163
+ if (isBondMaster(name))
164
+ entry.is_bond_master = true;
165
+ results.push(entry);
79
166
  }
80
167
  // Remove stale interfaces that disappeared
81
168
  for (const name of previousCounters.keys()) {
@@ -1 +1 @@
1
- {"version":3,"file":"network.js","sourceRoot":"","sources":["../../src/collect/network.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"network.js","sourceRoot":"","sources":["../../src/collect/network.ts"],"names":[],"mappings":"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"}
@@ -158,10 +158,23 @@ export interface NetworkInfo {
158
158
  speed_mbps: number;
159
159
  rx_bytes_sec: number;
160
160
  tx_bytes_sec: number;
161
+ /** Delta over the collection interval (rx_errors + any subtype counter). */
161
162
  rx_errors: number;
162
163
  tx_errors: number;
163
164
  rx_drops: number;
164
165
  tx_drops: number;
166
+ /** Delta over the collection interval. Null if counter not available on this NIC. */
167
+ rx_packets?: number;
168
+ tx_packets?: number;
169
+ /** Fine-grained RX hardware-error subtypes (deltas). Null if unavailable. */
170
+ rx_crc_errors?: number;
171
+ rx_frame_errors?: number;
172
+ rx_length_errors?: number;
173
+ /** TX physical-layer fault counter (delta). Null if unavailable. */
174
+ tx_carrier_errors?: number;
175
+ operstate?: string;
176
+ bond_master?: string;
177
+ is_bond_master?: boolean;
165
178
  }
166
179
  export interface RaidInfo {
167
180
  device: string;
@@ -0,0 +1,26 @@
1
+ # Glassmkr Crucible - docker compose deployment
2
+ #
3
+ # Before starting, create /etc/glassmkr/collector.yaml on the host with your
4
+ # Forge collector key. See https://forge.glassmkr.com/docs/getting-started.
5
+ #
6
+ # Why privileged + host network:
7
+ # - privileged: true gives access to /dev/ipmi0 (IPMI sensors) and raw disk devices (SMART)
8
+ # - network_mode: host lets the agent read the real host network interfaces and bond state
9
+ # - /proc and /sys are mounted so the agent monitors the host, not the container
10
+
11
+ services:
12
+ crucible:
13
+ image: ghcr.io/glassmkr/crucible:latest
14
+ container_name: glassmkr-crucible
15
+ restart: unless-stopped
16
+ privileged: true
17
+ network_mode: host
18
+ volumes:
19
+ - /etc/glassmkr:/etc/glassmkr:ro
20
+ - /proc:/host/proc:ro
21
+ - /sys:/host/sys:ro
22
+ - /dev:/dev:ro
23
+ - /run/dbus:/run/dbus:ro
24
+ environment:
25
+ - HOST_PROC=/host/proc
26
+ - HOST_SYS=/host/sys
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassmkr/crucible",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "Lightweight bare metal server monitoring. IPMI, SMART, OS, network. Opinionated alerts.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,5 +1,5 @@
1
1
  import { readProcFile, sleep } from "../lib/parse.js";
2
- import { readFileSync } from "fs";
2
+ import { readFileSync, readdirSync } from "fs";
3
3
  import type { NetworkInfo } from "../lib/types.js";
4
4
 
5
5
  interface IfaceStats {
@@ -13,10 +13,26 @@ interface PreviousCounters {
13
13
  tx_errors: number;
14
14
  rx_drops: number;
15
15
  tx_drops: number;
16
+ rx_packets: number;
17
+ tx_packets: number;
18
+ rx_crc_errors?: number;
19
+ rx_frame_errors?: number;
20
+ rx_length_errors?: number;
21
+ tx_carrier_errors?: number;
16
22
  }
17
23
 
18
24
  const previousCounters = new Map<string, PreviousCounters>();
19
25
 
26
+ function readStatCounter(iface: string, name: string): number | undefined {
27
+ try {
28
+ const raw = readFileSync(`/sys/class/net/${iface}/statistics/${name}`, "utf-8").trim();
29
+ const val = parseInt(raw, 10);
30
+ return Number.isFinite(val) ? val : undefined;
31
+ } catch {
32
+ return undefined;
33
+ }
34
+ }
35
+
20
36
  function parseNetDev(): Record<string, IfaceStats> {
21
37
  const raw = readProcFile("/proc/net/dev") || "";
22
38
  const result: Record<string, IfaceStats> = {};
@@ -45,6 +61,35 @@ function getSpeed(iface: string): number {
45
61
  }
46
62
  }
47
63
 
64
+ function getOperstate(iface: string): string {
65
+ try {
66
+ return readFileSync(`/sys/class/net/${iface}/operstate`, "utf-8").trim();
67
+ } catch {
68
+ return "unknown";
69
+ }
70
+ }
71
+
72
+ function getBondMaster(iface: string): string | undefined {
73
+ try {
74
+ const bonds = readdirSync("/proc/net/bonding/");
75
+ for (const bond of bonds) {
76
+ const content = readFileSync(`/proc/net/bonding/${bond}`, "utf-8");
77
+ if (content.includes(`Slave Interface: ${iface}`)) return bond;
78
+ }
79
+ } catch {
80
+ // No bonds or /proc/net/bonding doesn't exist
81
+ }
82
+ return undefined;
83
+ }
84
+
85
+ function isBondMaster(iface: string): boolean {
86
+ try {
87
+ return readdirSync("/proc/net/bonding/").includes(iface);
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
48
93
  // Compute delta, handling counter wraps (current < previous means reset, use current as delta)
49
94
  function delta(current: number, previous: number): number {
50
95
  if (current >= previous) return current - previous;
@@ -66,17 +111,37 @@ export async function collectNetwork(): Promise<NetworkInfo[]> {
66
111
 
67
112
  const prev = previousCounters.get(name);
68
113
 
114
+ // /sys/class/net/*/statistics/ exposes finer-grained RX/TX subtype
115
+ // counters than /proc/net/dev. Read cumulative values here; delta is
116
+ // derived below against the previous cycle's snapshot.
117
+ const rxCrcCum = readStatCounter(name, "rx_crc_errors");
118
+ const rxFrameCum = readStatCounter(name, "rx_frame_errors");
119
+ const rxLenCum = readStatCounter(name, "rx_length_errors");
120
+ const txCarrierCum = readStatCounter(name, "tx_carrier_errors");
121
+
69
122
  // Compute error/drop deltas (0 on first cycle after start or new interface)
70
123
  let rxErrorsDelta = 0;
71
124
  let txErrorsDelta = 0;
72
125
  let rxDropsDelta = 0;
73
126
  let txDropsDelta = 0;
127
+ let rxPacketsDelta = 0;
128
+ let txPacketsDelta = 0;
129
+ let rxCrcDelta: number | undefined;
130
+ let rxFrameDelta: number | undefined;
131
+ let rxLenDelta: number | undefined;
132
+ let txCarrierDelta: number | undefined;
74
133
 
75
134
  if (prev) {
76
135
  rxErrorsDelta = delta(s2.rx_errors, prev.rx_errors);
77
136
  txErrorsDelta = delta(s2.tx_errors, prev.tx_errors);
78
137
  rxDropsDelta = delta(s2.rx_drops, prev.rx_drops);
79
138
  txDropsDelta = delta(s2.tx_drops, prev.tx_drops);
139
+ rxPacketsDelta = delta(s2.rx_packets, prev.rx_packets);
140
+ txPacketsDelta = delta(s2.tx_packets, prev.tx_packets);
141
+ if (rxCrcCum != null && prev.rx_crc_errors != null) rxCrcDelta = delta(rxCrcCum, prev.rx_crc_errors);
142
+ if (rxFrameCum != null && prev.rx_frame_errors != null) rxFrameDelta = delta(rxFrameCum, prev.rx_frame_errors);
143
+ if (rxLenCum != null && prev.rx_length_errors != null) rxLenDelta = delta(rxLenCum, prev.rx_length_errors);
144
+ if (txCarrierCum != null && prev.tx_carrier_errors != null) txCarrierDelta = delta(txCarrierCum, prev.tx_carrier_errors);
80
145
  }
81
146
 
82
147
  // Store current cumulative values for next cycle
@@ -85,9 +150,15 @@ export async function collectNetwork(): Promise<NetworkInfo[]> {
85
150
  tx_errors: s2.tx_errors,
86
151
  rx_drops: s2.rx_drops,
87
152
  tx_drops: s2.tx_drops,
153
+ rx_packets: s2.rx_packets,
154
+ tx_packets: s2.tx_packets,
155
+ rx_crc_errors: rxCrcCum,
156
+ rx_frame_errors: rxFrameCum,
157
+ rx_length_errors: rxLenCum,
158
+ tx_carrier_errors: txCarrierCum,
88
159
  });
89
160
 
90
- results.push({
161
+ const entry: NetworkInfo = {
91
162
  interface: name,
92
163
  speed_mbps: getSpeed(name),
93
164
  rx_bytes_sec: s2.rx_bytes - s1.rx_bytes, // already a 1-second delta
@@ -96,7 +167,19 @@ export async function collectNetwork(): Promise<NetworkInfo[]> {
96
167
  tx_errors: txErrorsDelta,
97
168
  rx_drops: rxDropsDelta,
98
169
  tx_drops: txDropsDelta,
99
- });
170
+ rx_packets: rxPacketsDelta,
171
+ tx_packets: txPacketsDelta,
172
+ operstate: getOperstate(name),
173
+ };
174
+ if (rxCrcDelta !== undefined) entry.rx_crc_errors = rxCrcDelta;
175
+ if (rxFrameDelta !== undefined) entry.rx_frame_errors = rxFrameDelta;
176
+ if (rxLenDelta !== undefined) entry.rx_length_errors = rxLenDelta;
177
+ if (txCarrierDelta !== undefined) entry.tx_carrier_errors = txCarrierDelta;
178
+ const master = getBondMaster(name);
179
+ if (master) entry.bond_master = master;
180
+ // Identify bond masters (have at least one slave pointing at them).
181
+ if (isBondMaster(name)) entry.is_bond_master = true;
182
+ results.push(entry);
100
183
  }
101
184
 
102
185
  // Remove stale interfaces that disappeared
package/src/lib/types.ts CHANGED
@@ -139,10 +139,23 @@ export interface NetworkInfo {
139
139
  speed_mbps: number;
140
140
  rx_bytes_sec: number;
141
141
  tx_bytes_sec: number;
142
+ /** Delta over the collection interval (rx_errors + any subtype counter). */
142
143
  rx_errors: number;
143
144
  tx_errors: number;
144
145
  rx_drops: number;
145
146
  tx_drops: number;
147
+ /** Delta over the collection interval. Null if counter not available on this NIC. */
148
+ rx_packets?: number;
149
+ tx_packets?: number;
150
+ /** Fine-grained RX hardware-error subtypes (deltas). Null if unavailable. */
151
+ rx_crc_errors?: number;
152
+ rx_frame_errors?: number;
153
+ rx_length_errors?: number;
154
+ /** TX physical-layer fault counter (delta). Null if unavailable. */
155
+ tx_carrier_errors?: number;
156
+ operstate?: string; // "up", "down", "unknown", etc. from /sys/class/net/{iface}/operstate
157
+ bond_master?: string; // if this interface is a bond slave, the bond name
158
+ is_bond_master?: boolean; // true when this entry represents the bond aggregate
146
159
  }
147
160
 
148
161
  export interface RaidInfo {