fix(diagnostic): 'Sicher aufraeumen' fuehrt wirklich auf statt nur zu kopieren

Der Button rief copyDiskCmd() → kopierte nur den docker-Befehl in die
Zwischenablage (ueber http scheitert navigator.clipboard oft still →
'keine Funktion'). Jetzt fuehrt runDiskCleanup() das Aufraeumen echt aus:
Server-seitig ueber die Docker-Daemon-API (Socket ist gemountet) —
/build/prune?all=true + /images/prune (dangling=false), ohne Volumes =
keine Daten weg. 'Aggressiv' (zusaetzlich Container+Volumes) als eigener
'Jetzt ausfuehren'-Button mit Warnung. Rueckmeldung: wieviel frei wurde;
Banner aktualisiert sich per periodischem disk_status.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 15:08:49 +02:00
co-authored by Claude Opus 4.8
parent 74c7ea0a2d
commit 18eb94e942
2 changed files with 94 additions and 1 deletions
+28 -1
View File
@@ -198,7 +198,7 @@
<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>
<button onclick="copyDiskCmd('safe')" class="btn secondary" style="padding:4px 10px;font-size:11px;" title="docker builder prune -a -f && docker image prune -a -f">
<button id="disk-clean-btn" onclick="runDiskCleanup('safe')" class="btn secondary" style="padding:4px 10px;font-size:11px;" title="Fuehrt aus: docker builder prune -a && docker image prune -a (ohne Volumes)">
Sicher aufraeumen
</button>
<button onclick="document.getElementById('disk-banner-aggressive').style.display=(document.getElementById('disk-banner-aggressive').style.display==='none'?'flex':'none')"
@@ -216,6 +216,7 @@
<div style="color:#FFAA55;">
<b>Aggressiv</b> — zusaetzlich ungenutzte Volumes. <b>Nur wenn alle ARIA-Container laufen</b>, sonst riskierst du Daten-Verlust (Sessions, SSH-Keys, Shared):<br>
<code style="font-family:monospace;">docker system prune -a --volumes -f</code>
<button onclick="runDiskCleanup('aggressive')" class="btn secondary" style="padding:2px 8px;font-size:10px;margin-left:6px;border-color:#FF3B30;color:#FF3B30;">Jetzt ausfuehren</button>
<button onclick="copyDiskCmd('aggressive')" class="btn secondary" style="padding:2px 8px;font-size:10px;margin-left:6px;">Kopieren</button>
</div>
</div>
@@ -1739,6 +1740,20 @@
return;
}
if (msg.type === 'disk_cleanup') {
const btn = document.getElementById('disk-clean-btn');
if (msg.status === 'running') {
if (btn) { btn.disabled = true; btn.textContent = 'Raeume auf...'; }
} else if (msg.status === 'done') {
if (btn) { btn.disabled = false; btn.textContent = 'Sicher aufraeumen'; }
alert('Aufgeraeumt: ' + (msg.freed || '0 MB') + ' frei\n(' + (msg.steps || []).join(', ') + ')');
} else if (msg.status === 'error') {
if (btn) { btn.disabled = false; btn.textContent = 'Sicher aufraeumen'; }
alert('Aufraeumen fehlgeschlagen: ' + (msg.error || ''));
}
return;
}
if (msg.type === 'mode' && msg.payload) {
// Bridge hat den Modus geaendert (evtl. von anderer App/Diagnostic) — UI syncen
const mode = (msg.payload.mode || '').toLowerCase();
@@ -6734,6 +6749,18 @@
banner.style.display = 'block';
}
// Fuehrt das Aufraeumen WIRKLICH aus (Server-seitig via Docker-API), statt
// nur den Befehl zu kopieren.
function runDiskCleanup(variant) {
const aggressive = variant === 'aggressive';
const q = aggressive
? 'AGGRESSIV aufraeumen? Loescht zusaetzlich ungenutzte Volumes — nur wenn ALLE ARIA-Container laufen, sonst Datenverlust!'
: 'Sicher aufraeumen? Loescht Build-Cache + ungenutzte Images (keine Volumes, keine Daten gehen verloren).';
if (!confirm(q)) return;
const btn = document.getElementById('disk-clean-btn');
if (btn && !aggressive) { btn.disabled = true; btn.textContent = 'Raeume auf...'; }
send({ action: 'disk_cleanup', variant });
}
function copyDiskCmd(variant) {
const cmd = variant === 'aggressive'
? 'docker system prune -a --volumes -f'
+66
View File
@@ -1559,6 +1559,70 @@ function dockerExec(containerName, cmd) {
});
}
// POST gegen die Docker-Daemon-API (via gemountetem Socket). Fuer prune-
// Endpoints — die geben SpaceReclaimed (Bytes) zurueck.
function dockerApiPost(apiPath) {
return new Promise((resolve, reject) => {
const req = http.request({
socketPath: "/var/run/docker.sock",
path: apiPath,
method: "POST",
headers: { "Content-Type": "application/json", "Content-Length": 0 },
}, (res) => {
let data = "";
res.on("data", (c) => data += c);
res.on("end", () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try { resolve(JSON.parse(data || "{}")); } catch { resolve({}); }
} else {
reject(new Error(`Docker API ${apiPath}: HTTP ${res.statusCode}${String(data).slice(0, 200)}`));
}
});
});
req.on("error", reject);
req.end();
});
}
// "Sicher aufraeumen": Build-Cache + ungenutzte Images prunen — OHNE Volumes
// (keine Daten weg). "aggressive": zusaetzlich gestoppte Container + ungenutzte
// Volumes (kann Daten kosten → nur auf ausdrueckliche Wahl). Fuehrt es WIRKLICH
// aus (frueher kopierte der Button nur den Befehl in die Zwischenablage).
async function handleDiskCleanup(clientWs, variant) {
const aggressive = variant === "aggressive";
const send = (o) => { try { clientWs.send(JSON.stringify(o)); } catch (_) {} };
send({ type: "disk_cleanup", status: "running", variant });
log("warn", "server", `Disk-Cleanup gestartet (${aggressive ? "aggressive" : "safe"})`);
try {
let reclaimed = 0;
const steps = [];
const bp = await dockerApiPost("/build/prune?all=true");
reclaimed += (bp.SpaceReclaimed || 0);
steps.push("Build-Cache");
// dangling=false → ALLE ungenutzten Images (nicht nur dangling).
// Docker-API-Filterformat: map[string][]string.
const imgFilter = encodeURIComponent(JSON.stringify({ dangling: ["false"] }));
const ip = await dockerApiPost("/images/prune?filters=" + imgFilter);
reclaimed += (ip.SpaceReclaimed || 0);
steps.push("ungenutzte Images");
if (aggressive) {
const cp = await dockerApiPost("/containers/prune");
reclaimed += (cp.SpaceReclaimed || 0);
steps.push("gestoppte Container");
const vp = await dockerApiPost("/volumes/prune");
reclaimed += (vp.SpaceReclaimed || 0);
steps.push("ungenutzte Volumes");
}
const mb = (reclaimed / (1024 * 1024));
const freed = mb >= 1024 ? (mb / 1024).toFixed(2) + " GB" : mb.toFixed(0) + " MB";
log("info", "server", `Disk-Cleanup fertig: ${freed} frei (${steps.join(", ")})`);
send({ type: "disk_cleanup", status: "done", variant, reclaimedBytes: reclaimed, freed, steps });
} catch (err) {
log("error", "server", `Disk-Cleanup fehlgeschlagen: ${err.message}`);
send({ type: "disk_cleanup", status: "error", variant, error: String(err && err.message || err) });
}
}
// ── Hilfsfunktionen ─────────────────────────────────────
function waitForMessage(ws, timeoutMs) {
@@ -2463,6 +2527,8 @@ wss.on("connection", (ws) => {
sendToRVS_raw({ type: "interject", payload: { projectId: msg.projectId || "", text: t }, timestamp: Date.now() });
log("info", "server", "Zwischenruf an RVS (project=" + (msg.projectId || "(main)") + "): " + t.slice(0, 60));
}
} else if (msg.action === "disk_cleanup") {
handleDiskCleanup(ws, msg.variant === "aggressive" ? "aggressive" : "safe");
} else if (msg.action === "reconnect_gateway") {
connectGateway();
} else if (msg.action === "reconnect_rvs") {