feat: Disk-Space Banner im Diagnostic mit Cleanup-Command zum Kopieren

Server:
- checkDiskSpace() prueft alle 30s 'df -B1 /shared' (zeigt Host-Disk
  da /shared ein Volume auf dem Docker-FS ist)
- 4 Stufen: ok (<70%), info (70%), warn (85%), critical (95%)
- Broadcastet disk_status nur bei Aenderung (Level oder Prozent)
- currentDiskStatus wird gecached → neu verbundene Clients bekommen
  den aktuellen Stand sofort beim 'init'

UI:
- Sticky Banner ganz oben, versteckt wenn Disk ok
- Farbe nach Level: gelb (info), orange (warn), rot (critical)
- Zeigt Prozent, Used/Total/Avail in GB, konkrete Situation
- Cleanup-Command als monospace Code mit Copy-Button ('docker system
  prune -a --volumes -f') — Click auf Code oder Button kopiert ins
  Clipboard, Fallback auf Range-Selektion
- 'Schliessen' Button fuer temporaeres Ausblenden (kommt aber wieder
  bei naechster Aenderung)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 23:36:36 +02:00
parent 402bddc18a
commit f15b3f583f
2 changed files with 115 additions and 0 deletions
+49
View File
@@ -1148,6 +1148,53 @@ function updateAgentActivity() {
watchdogWarned = false;
}
// ── Disk-Space Monitor ───────────────────────────────
// Prueft regelmaessig die Host-Disk (via gemountetem /shared) und
// broadcastet bei kritischen Schwellwerten ein disk_status Event.
let lastDiskStatus = null;
let currentDiskStatus = null; // Vollstaendig fuer neu verbundene Clients
function checkDiskSpace() {
const { exec } = require("child_process");
exec("df -B1 /shared", (err, stdout) => {
if (err) return;
const lines = stdout.trim().split("\n");
if (lines.length < 2) return;
const cols = lines[1].split(/\s+/);
// Filesystem Size Used Avail Use% MountedOn
const total = parseInt(cols[1], 10);
const used = parseInt(cols[2], 10);
const avail = parseInt(cols[3], 10);
if (!total) return;
const pct = Math.round((used / total) * 100);
let level = "ok";
if (pct >= 95) level = "critical";
else if (pct >= 85) level = "warn";
else if (pct >= 70) level = "info";
const status = {
type: "disk_status",
level,
percent: pct,
usedBytes: used,
totalBytes: total,
availBytes: avail,
};
currentDiskStatus = status;
// Nur broadcasten wenn sich was geaendert hat (oder alle 60s Refresh)
const key = `${level}-${pct}`;
if (lastDiskStatus !== key) {
lastDiskStatus = key;
broadcast(status);
if (level !== "ok") {
log(level === "critical" ? "error" : "warn", "server",
`Disk ${pct}% belegt (${(used/1024/1024/1024).toFixed(1)}GB von ${(total/1024/1024/1024).toFixed(1)}GB)`);
}
}
});
}
// Beim Start + alle 30s
setTimeout(checkDiskSpace, 2000);
setInterval(checkDiskSpace, 30000);
// Watchdog prüft alle 30s ob ARIA nach einer gesendeten Nachricht reagiert
setInterval(async () => {
if (pendingMessageTime === 0) return; // Keine Nachricht gesendet
@@ -1281,6 +1328,8 @@ wss.on("connection", (ws) => {
browserClients.add(ws);
// Initialen State + letzte Logs senden
ws.send(JSON.stringify({ type: "init", state, logs: logs.slice(-100) }));
// Letzten Disk-Status mitgeben damit der Client sofort weiss wie's um Platz steht
if (currentDiskStatus) ws.send(JSON.stringify(currentDiskStatus));
ws.on("message", (raw) => {
try {