Files
Aime/app/sysstat.js
T
Claude 47288c0479 feat(gui): Systemanzeige in der Kopfzeile (Speicherring + Turbine)
Zwei Symbole zwischen Bluetooth-Tab und Hell/Dunkel-Schalter: ein Ring mit
der Plattenbelegung in Prozent und eine Turbine, deren Drehzahl der CPU-Last
folgt. Beide klappen bei Mauskontakt eine Karte mit Zahlen auf (belegt/frei/
gesamt bzw. CPU, RAM, Temperatur).

Neues Modul sysstat.js liest alles aus /proc bzw. fs.statfsSync, damit die
GUI im Sekundentakt fragen kann, ohne df/top-Prozesse zu starten. CPU-% aus
zwei /proc/stat-Messpunkten mit eigenem 2-s-Ticker, RAM ueber MemAvailable,
Temperatur als waermste SoC-/Core-Zone.

Doku: Kapitel 07 + Changelog.
2026-08-17 21:52:21 +02:00

106 lines
4.0 KiB
JavaScript

// Systemwerte für die Kopfzeile: Plattenbelegung, CPU-Last, RAM, Temperatur.
// Bewusst ohne Shell-Aufrufe (kein df/top) — alles aus /proc bzw. statfs, damit
// die GUI im Sekundentakt fragen darf, ohne Prozesse zu starten.
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DISK_PATH = path.join(__dirname, 'recordings');
// ---- CPU ----------------------------------------------------------------
// Auslastung braucht zwei Messpunkte. Ein eigener Ticker hält den Wert bereit,
// sonst wäre die erste Antwort nach jedem Start immer 0 %.
let prevCpu = null;
let cpuPct = 0;
function sampleCpu() {
try {
const line = fs.readFileSync('/proc/stat', 'utf8').split('\n', 1)[0];
const v = line.trim().split(/\s+/).slice(1).map(Number).filter(n => Number.isFinite(n));
if (v.length < 5) return;
const idle = v[3] + v[4]; // idle + iowait
const total = v.reduce((a, b) => a + b, 0);
if (prevCpu) {
const dTotal = total - prevCpu.total;
const dIdle = idle - prevCpu.idle;
if (dTotal > 0) cpuPct = Math.max(0, Math.min(100, (1 - dIdle / dTotal) * 100));
}
prevCpu = { total, idle };
} catch { /* ohne /proc bleibt der letzte Wert stehen */ }
}
sampleCpu();
setInterval(sampleCpu, 2000).unref();
// ---- Temperatur ---------------------------------------------------------
// Interessant sind die SoC-/Kern-Zonen; WLAN-Karte & Co. bleiben draußen, sonst
// zeigt die Anzeige eine Temperatur, die nichts mit der Rechenlast zu tun hat.
const TEMP_ZONES = (() => {
try {
return fs.readdirSync('/sys/class/thermal')
.filter(d => /^thermal_zone\d+$/.test(d))
.map(d => {
const dir = `/sys/class/thermal/${d}`;
let type = d;
try { type = fs.readFileSync(`${dir}/type`, 'utf8').trim(); } catch { }
return { type, file: `${dir}/temp` };
})
.filter(z => /soc|core|center|npu|gpu/i.test(z.type));
} catch { return []; }
})();
function readTemp() {
let best = null;
for (const z of TEMP_ZONES) {
try {
const c = Number(fs.readFileSync(z.file, 'utf8').trim()) / 1000;
if (Number.isFinite(c) && (!best || c > best.c)) best = { c, zone: z.type };
} catch { }
}
return best;
}
// ---- RAM ----------------------------------------------------------------
// MemAvailable statt MemFree: Cache zählt nicht als belegt, sonst stünde das
// Board dauernd auf 95 %.
function readMem() {
try {
const txt = fs.readFileSync('/proc/meminfo', 'utf8');
const kb = k => {
const m = txt.match(new RegExp(`^${k}:\\s+(\\d+) kB`, 'm'));
return m ? Number(m[1]) * 1024 : null;
};
const total = kb('MemTotal');
const avail = kb('MemAvailable') ?? kb('MemFree');
if (!total || avail == null) return null;
return { totalBytes: total, freeBytes: avail, usedBytes: total - avail, usedPct: (1 - avail / total) * 100 };
} catch { return null; }
}
// ---- Platte -------------------------------------------------------------
// Gemessen wird die Partition, auf der die Aufnahmen liegen — das ist der
// Platz, der der App tatsächlich ausgeht. Die Prozentzahl folgt df: der für
// root reservierte Anteil zählt als belegt.
function readDisk() {
try {
const st = fs.statfsSync(fs.existsSync(DISK_PATH) ? DISK_PATH : __dirname);
const bs = st.bsize;
const totalBytes = st.blocks * bs;
const freeBytes = st.bavail * bs;
const usedBytes = (st.blocks - st.bfree) * bs;
const denom = usedBytes + freeBytes;
return { totalBytes, freeBytes, usedBytes, usedPct: denom > 0 ? (usedBytes / denom) * 100 : 0 };
} catch { return null; }
}
export function status() {
const t = readTemp();
return {
cpu: { pct: Math.round(cpuPct * 10) / 10, cores: os.cpus().length, load1: Math.round(os.loadavg()[0] * 100) / 100 },
ram: readMem(),
disk: readDisk(),
temp: t ? { c: Math.round(t.c * 10) / 10, zone: t.zone } : null
};
}