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:
@@ -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")
|
||||
+3
-3
@@ -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) => {
|
||||
|
||||
+61
-15
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
+23
-12
@@ -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
@@ -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); }
|
||||
|
||||
+21
-6
@@ -88,11 +88,24 @@ Boards ohne SSH zeigen:
|
||||
|
||||
| Symbol | Zeigt an | Klappkarte bei Mauskontakt |
|
||||
|---|---|---|
|
||||
| Ring + Prozentzahl | Belegung der Partition mit den Aufnahmen (`/`, eMMC) | Belegt / Frei / Gesamt in GB |
|
||||
| Turbine | CPU-Last — je höher, desto schneller die Drehung | CPU % (+ Kernzahl), RAM % mit GB, Temperatur mit Zone |
|
||||
| Ring + Prozentzahl | Anteil des **Aufnahme-Vorrats**, den die Aufnahmen schon belegen | Aufnahmen / Noch frei / Gesamt in GB, dazu die Partition als Fußzeile |
|
||||
| Ventilator | CPU-Last — je höher, desto schneller die Drehung | CPU % (+ Kernzahl), RAM % mit GB, Temperatur mit Zone |
|
||||
|
||||
- Datenquelle: `GET /api/sysstat` (`app/sysstat.js`). Alles aus `/proc` bzw. `fs.statfsSync` — **kein**
|
||||
`df`/`top`-Aufruf, damit die GUI im Sekundentakt fragen darf, ohne Prozesse zu starten.
|
||||
**Bezugsgröße der Prozentzahl** (Wunsch des Nutzers, 2026-08-17): nicht die Partition, sondern der Platz,
|
||||
den ein *frisch aufgesetztes System dieser Ausbaustufe* für Aufnahmen übrig hätte:
|
||||
|
||||
```
|
||||
100 % = freier Platz + Summe aller Audiodateien unter recordings/
|
||||
belegt = Summe aller Audiodateien unter recordings/
|
||||
```
|
||||
|
||||
System, Modelle und `node_modules` zählen also weder oben noch unten mit — die Anzeige beantwortet
|
||||
„wie viel von meinem Aufnahme-Vorrat ist weg?“ und nicht „wie voll ist die eMMC?“. Der `df`-Wert steht
|
||||
weiterhin klein in der Fußzeile der Klappkarte (`partPct`).
|
||||
|
||||
- Datenquelle: `GET /api/sysstat` (`app/sysstat.js`). `/proc`, `fs.statfsSync` und ein **gepufferter**
|
||||
Größenscan über `recordings/` (`readdir` rekursiv + `stat`, TTL 60 s, nur Audio-Endungen) — **kein**
|
||||
`df`/`du`/`top`-Prozess, damit die GUI im Sekundentakt fragen darf.
|
||||
- CPU-% aus der Differenz zweier `/proc/stat`-Messpunkte. `sysstat.js` misst dafür **selbst** alle 2 s
|
||||
(`setInterval(...).unref()`); ohne diesen eigenen Ticker wäre die erste Antwort nach jedem Start 0 %.
|
||||
- RAM rechnet mit `MemAvailable`, nicht `MemFree` — sonst stünde das Board wegen Cache dauernd bei 95 %.
|
||||
@@ -102,10 +115,12 @@ Boards ohne SSH zeigen:
|
||||
Hintergrund-Tab (`document.hidden`) ausgesetzt. Ein einziger Ticker für alle `render()`-Durchläufe;
|
||||
nach jedem `render()` werden die zuletzt geholten Werte sofort zurückgemalt, weil `render()` die
|
||||
Kopfzeile komplett neu schreibt.
|
||||
- Drehzahl der Turbine läuft in **Stufen** (`spinSeconds()`: 7 s bei Leerlauf … 0,35 s ab 90 %). Eine
|
||||
- Drehzahl des Ventilators läuft in **Stufen** (`spinSeconds()`: 7 s bei Leerlauf … 0,35 s ab 90 %). Eine
|
||||
stufenlos gesetzte `animation-duration` würde die laufende Drehung bei jedem Poll neu ansetzen — das
|
||||
ruckt sichtbar. `prefers-reduced-motion` stoppt die Animation ganz.
|
||||
- Farbe: grün < 75 %, gelb ab 75 %, rot ab 90 %; bei der Turbine zählt der schlimmste Wert aus CPU, RAM
|
||||
- Icon: vier gebogene Flügel (Pinwheel) im **feststehenden** Gehäusering — nur die Flügelgruppe dreht
|
||||
sich, sonst wäre an einem runden Ring keine Drehung zu sehen.
|
||||
- Farbe: grün < 75 %, gelb ab 75 %, rot ab 90 %; beim Ventilator zählt der schlimmste Wert aus CPU, RAM
|
||||
und Temperatur (Temperatur skaliert 45 °C → 0 %, 90 °C → 100 %).
|
||||
|
||||
## Deploy-Ablauf (von Windows aus)
|
||||
|
||||
@@ -3,6 +3,29 @@
|
||||
Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum,
|
||||
Was, Warum, betroffene Dateien/Backups).
|
||||
|
||||
## 2026-08-17 (Nachtrag) — Ventilator statt Turbine, Speicher relativ zum Aufnahme-Vorrat
|
||||
|
||||
**Zwei Korrekturen des Nutzers am selben Tag:**
|
||||
|
||||
1. **Icon soll wie ein Ventilator aussehen.** Der erste Entwurf (fünf schmale Tropfen ohne Gehäuse) las
|
||||
sich als Kleeblatt. Jetzt: vier gebogene Pinwheel-Flügel in einem feststehenden Gehäusering; nur die
|
||||
Flügelgruppe dreht. Die Form wurde vor dem Ausrollen ohne Browser geprüft — die Bézier-Segmente der
|
||||
Kandidaten wurden mit einem kleinen Python/Pillow-Skript abgetastet, rotiert und als PNG gerendert
|
||||
(`lab/preview_fan.py`), weil die Chrome-Anbindung an diesem Tag nicht verfügbar war.
|
||||
2. **Prozentzahl soll den Aufnahme-Vorrat messen, nicht die Partition.** Neue Bezugsgröße:
|
||||
`100 % = freier Platz + Summe aller Audiodateien unter recordings/`, belegt = die Audiodateien.
|
||||
Das ist genau der Platz, den ein frisch aufgesetztes System dieser Ausbaustufe für Aufnahmen hätte;
|
||||
System, Modelle und `node_modules` fallen aus Zähler **und** Nenner heraus. Der `df`-Wert bleibt als
|
||||
Fußzeile („Partition gesamt“) in der Klappkarte.
|
||||
|
||||
**Umsetzung:** `sysstat.js` scannt `recordings/` mit rekursivem `readdir` + `stat` (nur Audio-Endungen,
|
||||
kein `du`-Prozess) und puffert das Ergebnis 60 s; nur der allererste Aufruf wartet auf den Scan, sonst
|
||||
zeigte die Anzeige direkt nach dem Start einmal 0 %. Neue Felder `recBytes`, `recFiles`, `budgetBytes`,
|
||||
`partPct`; `usedPct` = `recBytes / budgetBytes`. `status()` ist dadurch `async` → die Route läuft jetzt
|
||||
über `wrap()`. GUI: Balken + Datei-Anzahl in der Karte, erklärende Fußnote, Fußzeile mit dem
|
||||
Partitionswert. Gegenprobe: Scan meldet 1 957 445 371 Bytes, `du -sb` sagt exakt dasselbe (103 Dateien,
|
||||
52 mp3 + 51 wav) ⇒ 1,96 GB von 29,6 GB Vorrat = **6,6 %** (Partition dagegen 52 %).
|
||||
|
||||
## 2026-08-17 — Systemanzeige in der Kopfzeile: Speicherring + Turbine
|
||||
|
||||
**Wunsch:** zwischen dem Bluetooth-Tab und dem Hell/Dunkel-Schalter eine Anzeige, die den belegten
|
||||
|
||||
Reference in New Issue
Block a user