diff --git a/admin.html b/admin.html index 7282d9e..326ad74 100644 --- a/admin.html +++ b/admin.html @@ -83,6 +83,6 @@ - + diff --git a/admin.js b/admin.js index 4030059..b275c65 100644 --- a/admin.js +++ b/admin.js @@ -35,6 +35,7 @@ let latestContainers = []; let dangerToken = null; let dangerExpiresAt = 0; let dangerTimer = null; +let opsAutoRefreshTimer = null; function base64UrlEncode(buffer) { return btoa(String.fromCharCode(...new Uint8Array(buffer))) @@ -322,14 +323,21 @@ function renderDisks(disks = []) { function renderWatchtower(watchtower) { latestWatchtower = watchtower; const officialSession = watchtower?.lastSession; + const statusGenerated = watchtower?.generatedAt + ? new Date(watchtower.generatedAt).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) + : '--'; const officialHtml = officialSession ? `
Offizieller Lauf${officialSession.finishedAt ? new Date(officialSession.finishedAt).toLocaleString('de-DE') : '--'}
Geprueft${officialSession.scanned ?? '--'}
Aktualisiert${officialSession.updated ?? '--'}
Fehler${officialSession.failed ?? '--'}
+
Status aktualisiert${statusGenerated}
` - : `
Offizieller Lauf${watchtower?.tail?.slice(-2)?.join(' | ') || 'Noch nicht gefunden'}
`; + : ` +
Offizieller Lauf${watchtower?.tail?.slice(-2)?.join(' | ') || 'Noch nicht gefunden'}
+
Status aktualisiert${statusGenerated}
+ `; if (lastManualWatchtowerRun) { const run = lastManualWatchtowerRun; @@ -551,10 +559,13 @@ async function pollWatchtowerRun(runId, attempt = 0) { } } -async function refreshOps() { - diskStatus.textContent = 'Lade...'; - watchtowerStatus.textContent = 'Lade...'; - containerStatus.textContent = 'Lade...'; +async function refreshOps(options = {}) { + const silent = Boolean(options.silent); + if (!silent) { + diskStatus.textContent = 'Lade...'; + watchtowerStatus.textContent = 'Lade...'; + containerStatus.textContent = 'Lade...'; + } try { const response = await fetch('/api/status', { cache: 'no-store' }); @@ -570,6 +581,15 @@ async function refreshOps() { } } +function startOpsAutoRefresh() { + window.clearInterval(opsAutoRefreshTimer); + opsAutoRefreshTimer = window.setInterval(() => { + if (document.visibilityState === 'visible') { + refreshOps({ silent: true }); + } + }, 60000); +} + async function boot() { await handleCallback(); tokenSet = getStoredTokens(); @@ -593,6 +613,7 @@ async function boot() { adminUser.textContent = `Angemeldet als ${claims.preferred_username || claims.email}`; renderEditor(); refreshOps(); + startOpsAutoRefresh(); } loginButton.addEventListener('click', login); diff --git a/server.js b/server.js index 6b8d8b6..33bd47a 100644 --- a/server.js +++ b/server.js @@ -13,6 +13,7 @@ const DOCKER_SOCKET = process.env.DOCKER_SOCKET || '/var/run/docker.sock'; const WATCHTOWER_CONTAINER = process.env.WATCHTOWER_CONTAINER || 'watchtower'; const WATCHTOWER_RUN_IMAGE = process.env.WATCHTOWER_RUN_IMAGE || 'containrrr/watchtower'; const WATCHTOWER_CONFIG_PATH = process.env.WATCHTOWER_CONFIG_PATH || '/volume2/docker/watchtower/config.json'; +const WATCHTOWER_LOG_TAIL = Number(process.env.WATCHTOWER_LOG_TAIL || 1200); const AUTHORITY = process.env.AUTHORITY || 'https://auth.mischlabs.de/realms/mischlabs'; const ADMIN_CLIENT_ID = process.env.ADMIN_CLIENT_ID || 'mischlabs-admin'; const ADMIN_USERS = (process.env.ADMIN_USERS || 'mrdiderot').split(',').map((user) => user.trim().toLowerCase()).filter(Boolean); @@ -523,7 +524,7 @@ function parseWatchtower(lines) { current.warnings.push(line.replace(/^.*msg="/, '').replace(/"$/, '')); } - const done = line.match(/Session done(?:\\"|") Failed=(\d+) Scanned=(\d+) Updated=(\d+)/); + const done = line.match(/Session done.*Failed=(\d+)\s+Scanned=(\d+)\s+Updated=(\d+)/); if (done) { current = current || { startedAt: time, found: [], warnings: [] }; current.finishedAt = time; @@ -538,7 +539,7 @@ function parseWatchtower(lines) { if (sessions.length) return sessions.at(-1); const lastDoneLine = [...lines].reverse().find((line) => line.includes('Session done')); - const done = lastDoneLine?.match(/Failed=(\d+) Scanned=(\d+) Updated=(\d+)/); + const done = lastDoneLine?.match(/Failed=(\d+)\s+Scanned=(\d+)\s+Updated=(\d+)/); if (done) { const time = lastDoneLine.match(/time="([^"]+)"/)?.[1] || null; return { @@ -599,7 +600,7 @@ async function statusPayload() { const [containers, disks, logs, services] = await Promise.all([ getContainers().catch((error) => ({ error: error.message })), getDisks(), - dockerLogs(WATCHTOWER_CONTAINER).catch((error) => [`watchtower logs unavailable: ${error.message}`]), + dockerLogs(WATCHTOWER_CONTAINER, WATCHTOWER_LOG_TAIL).catch((error) => [`watchtower logs unavailable: ${error.message}`]), getServices() ]); @@ -609,6 +610,8 @@ async function statusPayload() { disks, watchtower: { container: WATCHTOWER_CONTAINER, + generatedAt: new Date().toISOString(), + logTail: WATCHTOWER_LOG_TAIL, lastSession: Array.isArray(logs) ? parseWatchtower(logs) : null, tail: Array.isArray(logs) ? logs.slice(-20) : logs },