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.
This commit is contained in:
@@ -55,6 +55,7 @@ function render() {
|
||||
${['recordings', 'projects', 'search', 'voices', 'glossary', 'bluetooth'].map(t =>
|
||||
`<button class="tab ${state.tab === t ? 'active' : ''}" data-tab="${t}">${({ bluetooth: 'Bluetooth', recordings: 'Aufnahmen', projects: 'Projektansicht', search: 'Suche', voices: 'Stimmen', glossary: 'Glossar' })[t]}</button>`).join('')}
|
||||
</nav>
|
||||
${sysmonMarkup()}
|
||||
<button id="themetoggle" class="btn icon theme" title="Hell/Dunkel umschalten">${theme === 'dark' ? '☀️' : '🌙'}</button>
|
||||
</header>
|
||||
<main id="view"></main>`;
|
||||
@@ -68,11 +69,122 @@ function render() {
|
||||
app.querySelector('#themetoggle').onclick = () =>
|
||||
applyTheme(document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark');
|
||||
app.querySelector('#recbtn').onclick = toggleRecord;
|
||||
paintSysmon(sysmon.last); // Werte sofort zurückmalen, render() leert das DOM
|
||||
startSysmon();
|
||||
bindMarkerInput();
|
||||
if (state.recording) startTimerUI();
|
||||
({ bluetooth: viewBluetooth, recordings: viewRecordings, projects: viewProjects, search: viewSearch, voices: viewVoices, glossary: viewGlossary })[state.tab]();
|
||||
}
|
||||
|
||||
// ---- Systemanzeige in der Kopfzeile -------------------------------------
|
||||
// Zwei Symbole zwischen den Tabs und dem Hell/Dunkel-Schalter: ein Ring mit der
|
||||
// Plattenbelegung in Prozent und eine Turbine, die sich um so schneller dreht,
|
||||
// je höher die CPU-Last ist. Bei Mauskontakt klappt jeweils eine Karte mit den
|
||||
// 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('');
|
||||
|
||||
function sysmonMarkup() {
|
||||
return `
|
||||
<div class="sysmon">
|
||||
<div class="sys-chip" id="sysdisk" tabindex="0" role="group" aria-label="Speicherbelegung">
|
||||
<svg class="ring" viewBox="0 0 36 36" aria-hidden="true">
|
||||
<circle class="ring-bg" cx="18" cy="18" r="15.9155"/>
|
||||
<circle class="ring-fg" cx="18" cy="18" r="15.9155" stroke-dasharray="0 100" data-disk-ring/>
|
||||
</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>
|
||||
</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>
|
||||
<div class="sys-pop">
|
||||
<div class="sys-pop-h">Systemlast</div>
|
||||
<div class="sys-row"><span>CPU</span><b data-cpu-pct>–</b></div>
|
||||
<div class="sys-bar"><i data-cpu-bar style="width:0%"></i></div>
|
||||
<div class="sys-row"><span>RAM</span><b data-ram-pct>–</b></div>
|
||||
<div class="sys-bar"><i data-ram-bar style="width:0%"></i></div>
|
||||
<div class="sys-row"><span>Temperatur</span><b data-temp>–</b></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function sysLevel(pct) { return pct >= 90 ? 'danger' : pct >= 75 ? 'warn' : 'ok'; }
|
||||
function sysGb(bytes) { return (bytes / 1073741824).toFixed(1).replace('.', ',') + ' GB'; }
|
||||
|
||||
// Drehzahl in Stufen statt stufenlos: ein geänderter animation-duration setzt
|
||||
// die laufende Drehung neu an, das ruckt. Grobe Stufen = seltener Sprung.
|
||||
function spinSeconds(cpuPct) {
|
||||
const steps = [[5, 7], [15, 4.5], [30, 3], [45, 2], [60, 1.3], [75, .85], [90, .55]];
|
||||
for (const [limit, sec] of steps) if (cpuPct < limit) return sec;
|
||||
return .35;
|
||||
}
|
||||
|
||||
function paintSysmon(s) {
|
||||
const disk = document.querySelector('#sysdisk');
|
||||
const load = document.querySelector('#sysload');
|
||||
if (!disk || !load) return;
|
||||
const set = (sel, txt) => { const el = document.querySelector(sel); if (el) el.textContent = txt; };
|
||||
|
||||
if (!s) { // Server nicht erreichbar: Anzeige grau, Turbine steht
|
||||
disk.className = 'sys-chip off'; load.className = 'sys-chip off';
|
||||
return;
|
||||
}
|
||||
|
||||
if (s.disk) {
|
||||
const p = Math.round(s.disk.usedPct);
|
||||
disk.className = 'sys-chip ' + sysLevel(p);
|
||||
disk.title = `Speicher ${p} % belegt`;
|
||||
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));
|
||||
set('[data-disk-free]', sysGb(s.disk.freeBytes));
|
||||
set('[data-disk-total]', sysGb(s.disk.totalBytes));
|
||||
}
|
||||
|
||||
const cpu = s.cpu ? s.cpu.pct : 0;
|
||||
const ram = s.ram ? s.ram.usedPct : 0;
|
||||
const tc = s.temp ? s.temp.c : null;
|
||||
// Die Farbe der Turbine folgt dem schlimmsten der drei Werte; Temperatur ab
|
||||
// 80 °C gelb, ab 90 °C rot (darüber drosselt der RK3588 sichtbar).
|
||||
const worst = Math.max(cpu, ram, tc == null ? 0 : (tc - 45) / 45 * 100);
|
||||
load.className = 'sys-chip ' + sysLevel(worst);
|
||||
load.title = `CPU ${Math.round(cpu)} % · RAM ${Math.round(ram)} %${tc == null ? '' : ` · ${tc.toFixed(0)} °C`}`;
|
||||
set('[data-cpu-pct]', `${cpu.toFixed(0)} %${s.cpu && s.cpu.cores ? ` (${s.cpu.cores} Kerne)` : ''}`);
|
||||
set('[data-ram-pct]', s.ram ? `${ram.toFixed(0)} % — ${sysGb(s.ram.usedBytes)} von ${sysGb(s.ram.totalBytes)}` : '–');
|
||||
set('[data-temp]', tc == null ? '–' : `${tc.toFixed(1)} °C${s.temp.zone ? ` (${s.temp.zone})` : ''}`);
|
||||
const cb = document.querySelector('[data-cpu-bar]'); if (cb) cb.style.width = Math.min(100, cpu) + '%';
|
||||
const rb = document.querySelector('[data-ram-bar]'); if (rb) rb.style.width = Math.min(100, ram) + '%';
|
||||
const tb = document.querySelector('[data-turbine]');
|
||||
if (tb) tb.style.setProperty('--spin', spinSeconds(cpu) + 's');
|
||||
}
|
||||
|
||||
async function tickSysmon() {
|
||||
if (document.hidden) return; // im Hintergrund kein Polling
|
||||
try {
|
||||
sysmon.last = await api.get('/api/sysstat');
|
||||
} catch (err) { sysmon.last = null; }
|
||||
paintSysmon(sysmon.last);
|
||||
}
|
||||
|
||||
function startSysmon() {
|
||||
if (sysmon.timer) return; // ein Ticker reicht für alle render()
|
||||
tickSysmon();
|
||||
sysmon.timer = setInterval(tickSysmon, 5000);
|
||||
document.addEventListener('visibilitychange', () => { if (!document.hidden) tickSysmon(); });
|
||||
}
|
||||
|
||||
// Header record button: start = defaults only (default source = CallRecMix,
|
||||
// codec auto, speaker count auto — the old options tab is gone on purpose).
|
||||
// A live placeholder card appears at the top of the recordings list instantly.
|
||||
|
||||
Reference in New Issue
Block a user