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:
duffyduck 2026-04-19 23:36:36 +02:00
parent 402bddc18a
commit f15b3f583f
2 changed files with 115 additions and 0 deletions

View File

@ -127,6 +127,18 @@
</style> </style>
</head> </head>
<body> <body>
<!-- Disk-Space Warnung (dynamisch gesetzt) -->
<div id="disk-banner" style="display:none;position:sticky;top:0;z-index:500;padding:10px 14px;border-radius:0;margin:-16px -16px 12px -16px;font-size:13px;">
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<span id="disk-banner-icon" style="font-size:18px;">&#x26A0;&#xFE0F;</span>
<span id="disk-banner-text" style="flex:1;min-width:200px;font-weight:600;"></span>
<code id="disk-banner-cmd" onclick="copyDiskCmd()" style="font-family:monospace;font-size:12px;padding:4px 8px;background:rgba(0,0,0,0.3);border-radius:4px;cursor:pointer;white-space:nowrap;">
docker system prune -a --volumes -f
</code>
<button onclick="copyDiskCmd()" class="btn secondary" style="padding:4px 10px;font-size:11px;">Kopieren</button>
<button onclick="document.getElementById('disk-banner').style.display='none'" class="btn secondary" style="padding:4px 10px;font-size:11px;">Schliessen</button>
</div>
</div>
<h1>ARIA Diagnostic</h1> <h1>ARIA Diagnostic</h1>
<!-- Haupt-Navigation --> <!-- Haupt-Navigation -->
@ -753,6 +765,11 @@
return; return;
} }
if (msg.type === 'disk_status') {
updateDiskBanner(msg);
return;
}
if (msg.type === 'mode' && msg.payload) { if (msg.type === 'mode' && msg.payload) {
// Bridge hat den Modus geaendert (evtl. von anderer App/Diagnostic) — UI syncen // Bridge hat den Modus geaendert (evtl. von anderer App/Diagnostic) — UI syncen
const mode = (msg.payload.mode || '').toLowerCase(); const mode = (msg.payload.mode || '').toLowerCase();
@ -2155,6 +2172,55 @@
const ttsToggleEl = document.getElementById('tts-debug-toggle'); const ttsToggleEl = document.getElementById('tts-debug-toggle');
if (ttsToggleEl) ttsToggleEl.checked = showTtsDebug; if (ttsToggleEl) ttsToggleEl.checked = showTtsDebug;
// Disk-Space Banner aktualisieren (wird vom Server via disk_status gepusht)
function updateDiskBanner(status) {
const banner = document.getElementById('disk-banner');
const icon = document.getElementById('disk-banner-icon');
const text = document.getElementById('disk-banner-text');
if (!banner) return;
if (!status || status.level === 'ok') {
banner.style.display = 'none';
return;
}
const gb = (n) => (n / 1024 / 1024 / 1024).toFixed(1);
const pct = status.percent;
const used = gb(status.usedBytes);
const total = gb(status.totalBytes);
const avail = gb(status.availBytes);
let bg, col, msg;
if (status.level === 'critical') {
bg = '#5C1A1A'; col = '#FF6B6B'; icon.innerHTML = '&#x1F6A8;'; // 🚨
msg = `KRITISCH: Platte ${pct}% voll (${used}GB von ${total}GB, nur noch ${avail}GB frei). aria-core kann bald nicht mehr schreiben — sofort aufraeumen!`;
} else if (status.level === 'warn') {
bg = '#5C3A1A'; col = '#FFAA55'; icon.innerHTML = '&#x26A0;&#xFE0F;'; // ⚠️
msg = `Warnung: Platte ${pct}% voll (${avail}GB frei). Bald aufraeumen.`;
} else {
bg = '#4A3A1A'; col = '#FFD60A'; icon.innerHTML = '&#x2139;&#xFE0F;'; //
msg = `Hinweis: Platte ${pct}% voll (${avail}GB frei).`;
}
banner.style.background = bg;
banner.style.color = col;
banner.style.borderBottom = `2px solid ${col}`;
text.textContent = msg;
banner.style.display = 'block';
}
function copyDiskCmd() {
const cmd = document.getElementById('disk-banner-cmd').textContent.trim();
navigator.clipboard.writeText(cmd).then(() => {
const btn = event.target;
const old = btn.textContent;
btn.textContent = 'Kopiert!';
setTimeout(() => { btn.textContent = old; }, 1500);
}).catch(() => {
// Fallback: selektieren
const range = document.createRange();
range.selectNode(document.getElementById('disk-banner-cmd'));
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
});
}
connectWS(); connectWS();
</script> </script>
</body> </body>

View File

@ -1148,6 +1148,53 @@ function updateAgentActivity() {
watchdogWarned = false; 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 // Watchdog prüft alle 30s ob ARIA nach einer gesendeten Nachricht reagiert
setInterval(async () => { setInterval(async () => {
if (pendingMessageTime === 0) return; // Keine Nachricht gesendet if (pendingMessageTime === 0) return; // Keine Nachricht gesendet
@ -1281,6 +1328,8 @@ wss.on("connection", (ws) => {
browserClients.add(ws); browserClients.add(ws);
// Initialen State + letzte Logs senden // Initialen State + letzte Logs senden
ws.send(JSON.stringify({ type: "init", state, logs: logs.slice(-100) })); 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) => { ws.on("message", (raw) => {
try { try {