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
+23 -12
View File
@@ -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 =>
`<path transform="rotate(${a} 12 12)" d="M12 12 C10.1 8.7 9.9 5.2 12 2.3 C14.1 5.2 13.9 8.7 12 12 Z"/>`).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 =>
`<path transform="rotate(${a} 12 12)" d="M12 12 C11.4 8.6 11.4 5.4 12.7 3.1 C15.6 4.2 18.2 6.6 18.9 9.4 C17.2 11.2 14.6 12 12 12 Z"/>`).join('');
function sysmonMarkup() {
return `
@@ -96,16 +99,20 @@ function sysmonMarkup() {
</svg>
<span class="sys-val mono" data-disk-pct>–</span>
<div class="sys-pop">
<div class="sys-pop-h">Speicherplatz</div>
<div class="sys-row"><span>Belegt</span><b data-disk-used>–</b></div>
<div class="sys-row"><span>Frei</span><b data-disk-free>–</b></div>
<div class="sys-row"><span>Gesamt</span><b data-disk-total>–</b></div>
<div class="sys-pop-h">Speicher für Aufnahmen</div>
<div class="sys-row"><span>Aufnahmen</span><b data-disk-used>–</b></div>
<div class="sys-bar"><i data-disk-bar style="width:0%"></i></div>
<div class="sys-row"><span>Noch frei</span><b data-disk-free>–</b></div>
<div class="sys-row"><span>Gesamt für Aufnahmen</span><b data-disk-total>–</b></div>
<div class="sys-note">100 % = freier Platz + alle vorhandenen Aufnahmen<br>(also der Platz eines frisch aufgesetzten Systems)</div>
<div class="sys-row sys-sub"><span>Partition gesamt</span><b data-disk-part>–</b></div>
</div>
</div>
<div class="sys-chip" id="sysload" tabindex="0" role="group" aria-label="Systemlast">
<svg class="turbine" viewBox="0 0 24 24" aria-hidden="true" data-turbine>
<g class="tb-blades">${TURBINE_BLADES}</g>
<circle class="tb-hub" cx="12" cy="12" r="2.2"/>
<svg class="fan" viewBox="0 0 24 24" aria-hidden="true">
<circle class="fan-case" cx="12" cy="12" r="10.4"/>
<g class="fan-blades" data-turbine>${FAN_BLADES}</g>
<circle class="fan-hub" cx="12" cy="12" r="2.1"/>
</svg>
<div class="sys-pop">
<div class="sys-pop-h">Systemlast</div>
@@ -142,15 +149,19 @@ function paintSysmon(s) {
}
if (s.disk) {
// Prozent = Aufnahmen / (freier Platz + Aufnahmen). Das System selbst zählt
// bewusst nicht mit: gezeigt wird, wie viel vom Aufnahme-Vorrat weg ist.
const p = Math.round(s.disk.usedPct);
disk.className = 'sys-chip ' + sysLevel(p);
disk.title = `Speicher ${p} % belegt`;
disk.title = `Aufnahmen belegen ${p} % des verfügbaren Platzes`;
set('[data-disk-pct]', p + '%');
const ring = document.querySelector('[data-disk-ring]');
if (ring) ring.setAttribute('stroke-dasharray', `${Math.max(0, Math.min(100, s.disk.usedPct)).toFixed(1)} 100`);
set('[data-disk-used]', sysGb(s.disk.usedBytes));
const db = document.querySelector('[data-disk-bar]'); if (db) db.style.width = Math.min(100, s.disk.usedPct) + '%';
set('[data-disk-used]', `${sysGb(s.disk.recBytes)}${s.disk.recFiles ? ` (${s.disk.recFiles} Dateien)` : ''}`);
set('[data-disk-free]', sysGb(s.disk.freeBytes));
set('[data-disk-total]', sysGb(s.disk.totalBytes));
set('[data-disk-total]', sysGb(s.disk.budgetBytes));
set('[data-disk-part]', `${sysGb(s.disk.totalBytes)} — ${Math.round(s.disk.partPct)} % belegt`);
}
const cpu = s.cpu ? s.cpu.pct : 0;
+12 -7
View File
@@ -148,18 +148,20 @@ nav { display: flex; gap: 6px; flex-wrap: wrap; }
.ring-bg { stroke: var(--line); }
.ring-fg { stroke: currentColor; stroke-linecap: round; transition: stroke-dasharray .4s ease; }
/* Turbine: Drehdauer kommt als --spin aus main.js (je mehr Last, desto kürzer) */
.turbine { width: 21px; height: 21px; flex: none; }
.tb-blades { fill: currentColor; transform-origin: 12px 12px; animation: tbspin var(--spin, 7s) linear infinite; }
.tb-hub { fill: var(--surface); stroke: currentColor; stroke-width: 1.4; }
/* Ventilator: nur die Flügel drehen, das Gehäuse steht (sonst wäre an einem
runden Ring keine Drehung zu sehen). Drehdauer kommt als --spin aus main.js. */
.fan { width: 22px; height: 22px; flex: none; }
.fan-case { fill: none; stroke: currentColor; stroke-width: 1.4; opacity: .45; }
.fan-blades { fill: currentColor; transform-origin: 12px 12px; animation: tbspin var(--spin, 7s) linear infinite; }
.fan-hub { fill: var(--surface); stroke: currentColor; stroke-width: 1.5; }
@keyframes tbspin { to { transform: rotate(360deg); } }
.sys-chip.off .tb-blades { animation: none; }
@media (prefers-reduced-motion: reduce) { .tb-blades { animation: none; } }
.sys-chip.off .fan-blades { animation: none; }
@media (prefers-reduced-motion: reduce) { .fan-blades { animation: none; } }
/* Klappkarte bei Mauskontakt (oder Tastaturfokus) */
.sys-pop {
display: none; position: absolute; top: calc(100% + 8px); right: 0; z-index: 60;
min-width: 215px; padding: 10px 12px; color: var(--fg);
min-width: 215px; max-width: 300px; padding: 10px 12px; color: var(--fg);
background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, .28);
}
@@ -171,6 +173,9 @@ nav { display: flex; gap: 6px; flex-wrap: wrap; }
.sys-row { display: flex; justify-content: space-between; gap: 14px; font-size: 13px; padding: 2px 0; }
.sys-row span { color: var(--muted); }
.sys-row b { font-weight: 600; font-variant-numeric: tabular-nums; }
.sys-note { font-size: 11px; line-height: 1.35; color: var(--muted); margin: 6px 0 4px; }
.sys-row.sys-sub { border-top: 1px solid var(--line); margin-top: 4px; padding-top: 5px; font-size: 12px; }
.sys-row.sys-sub b { font-weight: 500; color: var(--muted); }
.sys-bar { height: 5px; border-radius: 3px; background: var(--inset); overflow: hidden; margin: 3px 0 7px; }
.sys-bar i { display: block; height: 100%; background: var(--accent); transition: width .4s ease; }
.sys-chip.ok .sys-bar i { background: var(--ok); }