fix(gui): Ventilator-Icon und Speicheranzeige auf Aufnahme-Vorrat

Das erste Icon las sich als Kleeblatt. Jetzt vier gebogene Pinwheel-Fluegel
in einem feststehenden Gehaeusering; nur die Fluegel drehen. Form vorab ohne
Browser geprueft (lab/preview_fan.py rendert die Bezier-Kandidaten als PNG).

Die Prozentzahl misst nicht mehr die Partition, sondern den Aufnahme-Vorrat:
100 % = freier Platz + alle Audiodateien unter recordings/, belegt = diese
Dateien. System, Modelle und node_modules fallen aus Zaehler und Nenner
heraus; der df-Wert steht als Fusszeile in der Klappkarte.

sysstat.js scannt recordings/ per readdir+stat (kein du-Prozess) und puffert
60 s; status() ist dadurch async, die Route laeuft ueber wrap().

Doku: Kapitel 07 + Changelog.
This commit is contained in:
Claude
2026-08-17 22:03:17 +02:00
parent 47288c0479
commit db50c9616d
7 changed files with 215 additions and 43 deletions
+61 -15
View File
@@ -78,28 +78,74 @@ function readMem() {
} 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() {
// ---- Aufnahmen ----------------------------------------------------------
// Summe aller Audiodateien unter recordings/. Der Scan ist billig (readdir +
// stat, keine Lesevorgänge), wird aber gepuffert, damit ein Poll im
// Sekundentakt nicht dauernd über das Verzeichnis läuft.
const AUDIO_EXT = /\.(mp3|wav|m4a|opus|ogg|flac|aac)$/i;
const REC_SCAN_TTL_MS = 60_000;
let recCache = { bytes: 0, files: 0, at: 0 };
let recScan = null;
async function scanRecordings() {
let bytes = 0, files = 0;
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; }
const entries = await fs.promises.readdir(DISK_PATH, { withFileTypes: true, recursive: true });
for (const e of entries) {
if (!e.isFile() || !AUDIO_EXT.test(e.name)) continue;
try {
const st = await fs.promises.stat(path.join(e.parentPath || e.path || DISK_PATH, e.name));
bytes += st.size; files++;
} catch { /* zwischenzeitlich gelöscht */ }
}
} catch { return recCache; }
recCache = { bytes, files, at: Date.now() };
return recCache;
}
export function status() {
function recordingsSize() {
const fresh = Date.now() - recCache.at < REC_SCAN_TTL_MS;
if (!fresh && !recScan) recScan = scanRecordings().finally(() => { recScan = null; });
// Beim allerersten Aufruf gibt es noch keine Zahl — dann auf den Scan warten,
// sonst zeigt die Anzeige direkt nach dem Start einmal 0 % Belegung.
return recCache.at === 0 ? recScan : recCache;
}
// ---- Platte -------------------------------------------------------------
// Zwei Sichtweisen auf denselben Datenträger:
// partition: was df sagt (inklusive System, Modelle, node_modules)
// budget: der Platz, um den es dem Nutzer geht — freier Platz PLUS die
// Aufnahmen, die schon daliegen. Das ist genau das, was ein frisch
// aufgesetztes System dieser Ausbaustufe für Aufnahmen übrig
// hätte. Die Prozentzahl im Ring = Aufnahmen / Budget.
async function readDisk() {
let st;
try { st = fs.statfsSync(fs.existsSync(DISK_PATH) ? DISK_PATH : __dirname); } catch { return null; }
const bs = st.bsize;
const totalBytes = st.blocks * bs;
const freeBytes = st.bavail * bs;
const usedBytes = (st.blocks - st.bfree) * bs;
const partDenom = usedBytes + freeBytes;
const rec = await recordingsSize();
const recBytes = rec ? rec.bytes : 0;
const budgetBytes = freeBytes + recBytes;
return {
totalBytes, freeBytes, usedBytes,
partPct: partDenom > 0 ? (usedBytes / partDenom) * 100 : 0,
recBytes, recFiles: rec ? rec.files : 0,
budgetBytes,
usedPct: budgetBytes > 0 ? (recBytes / budgetBytes) * 100 : 0
};
}
export async 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(),
disk: await readDisk(),
temp: t ? { c: Math.round(t.c * 10) / 10, zone: t.zone } : null
};
}