diff --git a/app/lab/preview_fan.py b/app/lab/preview_fan.py
new file mode 100644
index 0000000..ff42c67
--- /dev/null
+++ b/app/lab/preview_fan.py
@@ -0,0 +1,72 @@
+# Vorschau der Ventilator-Kandidaten ohne Browser: Bezier-Segmente abtasten,
+# rotieren, mit Pillow fuellen. Rechts jeweils die 22-px-Miniatur (Originalgroesse).
+import math
+from PIL import Image, ImageDraw
+
+BG, FG, CASE = "#1d222a", "#e8804c", "#6f5344"
+
+
+def bez(p0, p1, p2, p3, n=80):
+ for i in range(n + 1):
+ t = i / n
+ u = 1 - t
+ yield (u**3 * p0[0] + 3 * u * u * t * p1[0] + 3 * u * t * t * p2[0] + t**3 * p3[0],
+ u**3 * p0[1] + 3 * u * u * t * p1[1] + 3 * u * t * t * p2[1] + t**3 * p3[1])
+
+
+def outline(segs):
+ pts = []
+ for s in segs:
+ pts.extend(bez(*s))
+ return pts
+
+
+def rot(p, deg, c=12.0):
+ a = math.radians(deg)
+ x, y = p[0] - c, p[1] - c
+ return (c + x * math.cos(a) - y * math.sin(a), c + x * math.sin(a) + y * math.cos(a))
+
+
+def draw(segs, blades, case_r, hub_r, size):
+ S = size / 24
+ img = Image.new("RGB", (size, size), BG)
+ d = ImageDraw.Draw(img)
+ if case_r:
+ d.ellipse([(12 - case_r) * S, (12 - case_r) * S, (12 + case_r) * S, (12 + case_r) * S],
+ outline=CASE, width=max(1, int(1.4 * S)))
+ pts = outline(segs)
+ step = 360 / blades
+ for i in range(blades):
+ d.polygon([(x * S, y * S) for x, y in (rot(p, i * step) for p in pts)], fill=FG)
+ d.ellipse([(12 - hub_r) * S, (12 - hub_r) * S, (12 + hub_r) * S, (12 + hub_r) * S],
+ fill=BG, outline=FG, width=max(1, int(1.5 * S)))
+ return img
+
+
+CANDS = {
+ "A_komma4": (4, [((12, 12), (11.4, 8.6), (11.4, 5.4), (12.7, 3.1)),
+ ((12.7, 3.1), (15.6, 4.2), (18.2, 6.6), (18.9, 9.4)),
+ ((18.9, 9.4), (17.2, 11.2), (14.6, 12), (12, 12))]),
+ "B_schlank4": (4, [((12, 12), (10.6, 9), (10.4, 5.6), (12.2, 3.4)),
+ ((12.2, 3.4), (14.8, 4.6), (17.2, 7.2), (17.6, 10)),
+ ((17.6, 10), (16, 11.6), (13.8, 12.2), (12, 12))]),
+ "C_breit3": (3, [((12, 12), (10.9, 8.4), (11.2, 4.8), (13.3, 2.9)),
+ ((13.3, 2.9), (16.6, 4.4), (18.8, 7.4), (19.2, 10.6)),
+ ((19.2, 10.6), (17, 12), (14.2, 12.4), (12, 12))]),
+ "D_sichel5": (5, [((12, 12), (11.2, 9), (11.3, 5.8), (12.9, 3.6)),
+ ((12.9, 3.6), (15.1, 5), (16.9, 7.4), (17.3, 10.1)),
+ ((17.3, 10.1), (15.8, 11.4), (13.9, 12.1), (12, 12))]),
+}
+
+pad = 12
+big, small = 200, 22
+W = pad + len(CANDS) * (big + pad)
+sheet = Image.new("RGB", (W, big + small + 3 * pad), BG)
+dr = ImageDraw.Draw(sheet)
+for i, (name, (blades, segs)) in enumerate(CANDS.items()):
+ x = pad + i * (big + pad)
+ sheet.paste(draw(segs, blades, 10.4, 2.1, big), (x, pad))
+ sheet.paste(draw(segs, blades, 10.4, 2.1, small), (x + big // 2 - small // 2, big + 2 * pad))
+ dr.text((x + 4, big + 2 * pad + small + 2), name, fill="#98a2b3")
+sheet.save("fan_preview.png")
+print("ok")
diff --git a/app/server.js b/app/server.js
index 694586a..66400d3 100644
--- a/app/server.js
+++ b/app/server.js
@@ -377,9 +377,9 @@ const wrap = fn => (req, res) => fn(req, res).catch(e => res.status(500).json({
app.get('/api/health', (req, res) => res.json({ ok: true, ...rec.status() }));
-// Kopfzeilen-Anzeige: Platte, CPU, RAM, Temperatur. Reine /proc-Lesevorgänge,
-// deshalb ohne wrap() und ohne Warteschlange.
-app.get('/api/sysstat', (req, res) => res.json(sysstat.status()));
+// Kopfzeilen-Anzeige: Platte, CPU, RAM, Temperatur. /proc + statfs, dazu ein
+// gepufferter Größenscan über recordings/ — kein df/du-Prozess.
+app.get('/api/sysstat', wrap(async (req, res) => res.json(await sysstat.status())));
// --- Bluetooth ---
app.post('/api/bt/scan', wrap(async (req, res) => {
diff --git a/app/sysstat.js b/app/sysstat.js
index 6094a95..ce709d2 100644
--- a/app/sysstat.js
+++ b/app/sysstat.js
@@ -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
};
}
diff --git a/app/web/src/main.js b/app/web/src/main.js
index aa8dcb8..201af35 100644
--- a/app/web/src/main.js
+++ b/app/web/src/main.js
@@ -83,8 +83,11 @@ function render() {
// Zahlen auf (CPU, RAM, Temperatur bzw. belegt/frei/gesamt).
const sysmon = { timer: null, last: null };
-const TURBINE_BLADES = [0, 72, 144, 216, 288].map(a =>
- ``).join('');
+// Ventilator: vier gebogene Flügel im feststehenden Gehäusering. Nur die
+// Flügelgruppe dreht sich, der Ring bleibt stehen — sonst sähe man die
+// Drehung am runden Gehäuse gar nicht.
+const FAN_BLADES = [0, 90, 180, 270].map(a =>
+ ``).join('');
function sysmonMarkup() {
return `
@@ -96,16 +99,20 @@ function sysmonMarkup() {
–
-
Speicherplatz
-
Belegt–
-
Frei–
-
Gesamt–
+
Speicher für Aufnahmen
+
Aufnahmen–
+
+
Noch frei–
+
Gesamt für Aufnahmen–
+
100 % = freier Platz + alle vorhandenen Aufnahmen
(also der Platz eines frisch aufgesetzten Systems)
+
Partition gesamt–
-