diff --git a/admin.html b/admin.html index 9a55cfe..d87d764 100644 --- a/admin.html +++ b/admin.html @@ -81,6 +81,6 @@ - + diff --git a/admin.js b/admin.js index 3232812..2fc17d3 100644 --- a/admin.js +++ b/admin.js @@ -469,8 +469,7 @@ async function runWatchtowerOnce() { async function pollWatchtowerRun(runId, attempt = 0) { try { - const cleanup = attempt > 0 ? '&cleanup=1' : ''; - const response = await fetch(`/api/watchtower/runs/${encodeURIComponent(runId)}?${cleanup}`, { + const response = await fetch(`/api/watchtower/runs/${encodeURIComponent(runId)}`, { cache: 'no-store', headers: { Authorization: `Bearer ${tokenSet.id_token}` diff --git a/server.js b/server.js index 50701db..e5ad192 100644 --- a/server.js +++ b/server.js @@ -62,6 +62,7 @@ const serviceUrls = [ let jwksCache = null; let jwksCacheUntil = 0; +const manualWatchtowerRuns = new Map(); function json(res, status, payload) { res.writeHead(status, { @@ -288,6 +289,8 @@ async function restartContainer(containerName) { } async function runWatchtowerOnce() { + cleanupManualWatchtowerContainers().catch(() => {}); + const name = `mischlabs-watchtower-run-once-${Date.now()}`; const body = JSON.stringify({ Image: WATCHTOWER_RUN_IMAGE, @@ -315,20 +318,54 @@ async function runWatchtowerOnce() { }); await dockerRequest(`/containers/${created.Id}/start`, { method: 'POST' }); - return { + const payload = { id: created.Id.slice(0, 12), fullId: created.Id, name, image: WATCHTOWER_RUN_IMAGE, - startedAt: new Date().toISOString() + state: 'running', + running: true, + exitCode: null, + startedAt: new Date().toISOString(), + finishedAt: null, + session: null, + tail: [] }; + + manualWatchtowerRuns.set(created.Id, payload); + monitorWatchtowerRun(created.Id).catch((error) => { + manualWatchtowerRuns.set(created.Id, { + ...payload, + state: 'error', + running: false, + exitCode: 1, + finishedAt: new Date().toISOString(), + error: error.message + }); + }); + + return payload; } async function removeContainer(id) { await dockerRequest(`/containers/${encodeURIComponent(id)}?force=1`, { method: 'DELETE' }); } -async function getWatchtowerRun(runId, cleanup = false) { +async function waitContainer(id) { + return dockerRequest(`/containers/${encodeURIComponent(id)}/wait`, { method: 'POST' }); +} + +async function cleanupManualWatchtowerContainers() { + const containers = await dockerRequest('/containers/json?all=1'); + await Promise.all((containers || []) + .filter((container) => ( + container.Labels?.['com.mischlabs.role'] === 'manual-watchtower-run' + && container.State !== 'running' + )) + .map((container) => removeContainer(container.Id).catch(() => {}))); +} + +async function readWatchtowerRunContainer(runId) { const id = normalizeContainerName(runId); if (!id || !/^[a-fA-F0-9]{12,64}$/.test(id)) { const error = new Error('Ungueltige Watchtower-Run-ID.'); @@ -363,13 +400,34 @@ async function getWatchtowerRun(runId, cleanup = false) { session: parsed, tail: Array.isArray(logs) ? logs.slice(-20) : logs }; +} - if (cleanup && !payload.running) { - removeContainer(inspect.Id).catch(() => {}); - payload.cleanedUp = true; +async function monitorWatchtowerRun(runId) { + await waitContainer(runId).catch(() => null); + + const payload = await readWatchtowerRunContainer(runId); + manualWatchtowerRuns.set(runId, payload); + await removeContainer(runId).catch(() => {}); +} + +async function getWatchtowerRun(runId) { + const id = normalizeContainerName(runId); + if (!id || !/^[a-fA-F0-9]{12,64}$/.test(id)) { + const error = new Error('Ungueltige Watchtower-Run-ID.'); + error.statusCode = 400; + throw error; } - return payload; + if (manualWatchtowerRuns.has(id)) { + return manualWatchtowerRuns.get(id); + } + + try { + return await readWatchtowerRunContainer(id); + } catch (error) { + error.statusCode = error.statusCode || 404; + throw error; + } } async function getDisks() { @@ -607,9 +665,7 @@ const server = http.createServer(async (req, res) => { try { await verifyAdminRequest(req); - const requestUrl = new URL(req.url, `http://${req.headers.host}`); - const cleanup = requestUrl.searchParams.get('cleanup') === '1'; - json(res, 200, await getWatchtowerRun(decodeURIComponent(watchtowerRunMatch[1]), cleanup)); + json(res, 200, await getWatchtowerRun(decodeURIComponent(watchtowerRunMatch[1]))); } catch (error) { json(res, error.statusCode || 500, { error: error.message }); } @@ -621,4 +677,7 @@ const server = http.createServer(async (req, res) => { server.listen(PORT, () => { console.log(`MischLabs dashboard listening on ${PORT}`); + cleanupManualWatchtowerContainers().catch((error) => { + console.warn(`Manual Watchtower cleanup failed: ${error.message}`); + }); });