diff --git a/backup-monitor/static/js/app.js b/backup-monitor/static/js/app.js index 73ccfd8..970cf5e 100644 --- a/backup-monitor/static/js/app.js +++ b/backup-monitor/static/js/app.js @@ -1,268 +1,323 @@ -/* ── The Sentinel – Backup Monitor Frontend ────────────────── */ +/* The Sentinel – Backup Monitor */ const API = ''; let apiKey = localStorage.getItem('bm_api_key') || ''; let allHosts = []; let currentPage = 'dashboard'; -// ── Init ────────────────────────────────────────────────── +const statusLabels = { + ok: 'OK', + stale: 'überfällig', + error: 'Fehler', + disabled: 'aus', +}; + document.addEventListener('DOMContentLoaded', () => { loadAll(); setInterval(loadAll, 30000); }); +document.addEventListener('keydown', (event) => { + if (event.key === 'Escape') { + closeDrawer(); + closeModal(); + } +}); + function authHeaders() { - const h = {'Content-Type': 'application/json'}; - if (apiKey) h['X-API-Key'] = apiKey; - return h; + const headers = {'Content-Type': 'application/json'}; + if (apiKey) headers['X-API-Key'] = apiKey; + return headers; } -async function apiFetch(url, opts = {}) { - if (!opts.headers) opts.headers = {}; - if (apiKey) opts.headers['X-API-Key'] = apiKey; - const r = await fetch(url, opts); - if (r.status === 401) { - const key = prompt('🔑 API-Key eingeben:'); - if (key) { apiKey = key; localStorage.setItem('bm_api_key', key); opts.headers['X-API-Key'] = key; return fetch(url, opts); } +async function apiFetch(url, options = {}) { + if (!options.headers) options.headers = {}; + if (apiKey) options.headers['X-API-Key'] = apiKey; + let response = await fetch(url, options); + if (response.status === 401) { + const key = prompt('API-Schlüssel eingeben:'); + if (key) { + apiKey = key; + localStorage.setItem('bm_api_key', key); + options.headers['X-API-Key'] = key; + response = await fetch(url, options); + } } - return r; + return response; } async function loadAll() { - const [sumR, hostsR] = await Promise.all([fetch(`${API}/api/summary`), fetch(`${API}/api/hosts`)]); - const sum = await sumR.json(); - allHosts = await hostsR.json(); - renderDashboard(sum); - renderAlerts(); - renderHostGrid(); - document.getElementById('lastScan').textContent = new Date().toLocaleTimeString('de-DE', {hour:'2-digit',minute:'2-digit'}); - // System status indicator - const ss = document.getElementById('sysStatus'); - if (sum.error > 0) { ss.innerHTML = 'errorErrors Active'; } - else if (sum.stale > 0) { ss.innerHTML = 'warningStale Hosts'; } - else { ss.innerHTML = 'cloud_doneAll Systems OK'; } + try { + const [summaryResponse, hostsResponse] = await Promise.all([ + fetch(`${API}/api/summary`), + fetch(`${API}/api/hosts`), + ]); + if (!summaryResponse.ok || !hostsResponse.ok) throw new Error('API nicht erreichbar'); + + const summary = await summaryResponse.json(); + allHosts = await hostsResponse.json(); + renderDashboard(summary); + renderAlerts(); + renderHostGrid(); + document.getElementById('lastScan').textContent = new Date().toLocaleTimeString('de-DE', { + hour: '2-digit', + minute: '2-digit', + }); + renderSystemState(summary); + } catch (error) { + const state = document.getElementById('sysStatus'); + state.className = 'system-state status--error'; + state.innerHTML = 'Nicht erreichbar'; + console.error('Sentinel konnte nicht aktualisiert werden:', error); + } } -// ── Dashboard ───────────────────────────────────────────── -function renderDashboard(sum) { - document.getElementById('mOk').textContent = `${sum.ok}/${sum.total_hosts}`; - document.getElementById('mSize').textContent = fmtBytes(sum.today_size); - document.getElementById('mWarn').textContent = sum.error + sum.stale; - const wc = document.getElementById('mWarnCard'); - wc.className = 'bg-surface-container-low p-6 rounded-xl' + ((sum.error + sum.stale > 0) ? ' border border-error/20' : ''); +function renderSystemState(summary) { + const state = document.getElementById('sysStatus'); + if (summary.error > 0) { + state.className = 'system-state status--error'; + state.innerHTML = 'Fehler aktiv'; + } else if (summary.stale > 0) { + state.className = 'system-state status--stale'; + state.innerHTML = 'Hosts überfällig'; + } else { + state.className = 'system-state status--ok'; + state.innerHTML = 'Betriebsbereit'; + } +} - // Latest backup - const sorted = [...allHosts].filter(h => h.last_backup).sort((a,b) => new Date(b.last_backup) - new Date(a.last_backup)); +function renderDashboard(summary) { + document.getElementById('mOk').textContent = `${summary.ok}/${summary.total_hosts}`; + document.getElementById('mSize').textContent = fmtBytes(summary.today_size); + const issueCount = summary.error + summary.stale; + document.getElementById('mWarn').textContent = issueCount; + document.getElementById('mWarnCard').classList.toggle('has-issues', issueCount > 0); + + const sorted = [...allHosts] + .filter(host => host.last_backup) + .sort((a, b) => new Date(b.last_backup) - new Date(a.last_backup)); if (sorted.length) { - document.getElementById('mLatest').textContent = sorted[0].last_status === 'ok' ? 'Success' : 'Error'; + document.getElementById('mLatest').textContent = sorted[0].last_status === 'ok' ? 'Erfolgreich' : 'Fehler'; document.getElementById('mLatestHost').textContent = sorted[0].name; + } else { + document.getElementById('mLatest').textContent = '–'; + document.getElementById('mLatestHost').textContent = 'Noch keine Meldung'; } - // Cluster list - const cl = document.getElementById('clusterList'); - const groups = { ok: [], stale: [], error: [], disabled: [] }; - allHosts.forEach(h => groups[h.status]?.push(h)); - let html = ''; - if (groups.error.length) { html += clusterGroup('ERRORS', groups.error, 'error'); } - if (groups.stale.length) { html += clusterGroup('STALE', groups.stale, 'tertiary'); } - if (groups.ok.length) { html += clusterGroup('OPERATIONAL', groups.ok, 'secondary'); } - if (groups.disabled.length) { html += clusterGroup('DISABLED', groups.disabled, 'outline'); } - cl.innerHTML = html; + const groups = {ok: [], stale: [], error: [], disabled: []}; + allHosts.forEach(host => groups[host.status]?.push(host)); + const parts = []; + if (groups.error.length) parts.push(clusterGroup('Fehler', groups.error, 'error')); + if (groups.stale.length) parts.push(clusterGroup('Überfällig', groups.stale, 'stale')); + if (groups.ok.length) parts.push(clusterGroup('In Ordnung', groups.ok, 'ok')); + if (groups.disabled.length) parts.push(clusterGroup('Deaktiviert', groups.disabled, 'disabled')); + document.getElementById('clusterList').innerHTML = parts.join(''); - // Live stream renderLiveStream(); loadVolumeChart(); } -function clusterGroup(label, hosts, color) { +function clusterGroup(label, hosts, status) { return ` -
Noch keine Backup-Meldungen.
'; + return; + } + list.innerHTML = sorted.map(host => { + const time = new Date(host.last_backup).toLocaleTimeString('de-DE', { + hour: '2-digit', minute: '2-digit', second: '2-digit', + }); + const failed = host.last_status !== 'ok'; return ` -Keine offenen Meldungen. Alle aktiven Hosts sind im Zeitfenster.
'; + return; + } - al.innerHTML = issues.map(h => { - const isCrit = h.status === 'error'; - const color = isCrit ? 'error' : 'tertiary'; - const icon = isCrit ? 'error' : 'warning'; - const label = isCrit ? 'CRITICAL' : 'STALE'; + list.innerHTML = issues.map(host => { + const failed = host.status === 'error'; return ` -Details werden geladen …
'; - const [histR, calR] = await Promise.all([fetch(`${API}/api/history/${name}?days=30`), fetch(`${API}/api/calendar/${name}?days=30`)]); - const history = await histR.json(); - const calendar = await calR.json(); - const host = allHosts.find(h => h.name === name) || {}; + try { + const [historyResponse, calendarResponse] = await Promise.all([ + fetch(`${API}/api/history/${encodeURIComponent(name)}?days=30`), + fetch(`${API}/api/calendar/${encodeURIComponent(name)}?days=30`), + ]); + const history = await historyResponse.json(); + const calendar = await calendarResponse.json(); + const totalSize = history.reduce((sum, entry) => sum + Number(entry.original_size || 0), 0); + const successRate = history.length + ? Math.round(history.filter(entry => entry.status === 'ok').length / history.length * 100) + : 0; - const totalSize = history.reduce((s,e) => s + e.original_size, 0); - const avgDur = history.length ? Math.round(history.reduce((s,e) => s + e.duration_sec, 0) / history.length) : 0; - const rate = history.length ? Math.round(history.filter(e => e.status === 'ok').length / history.length * 100) : 0; - - body.innerHTML = ` - -Keine Einträge im Zeitraum.
'} +Die Host-Details konnten nicht geladen werden.
'; + console.error('Host-Details konnten nicht geladen werden:', error); + } } -function buildCalendar(cal) { +function buildCalendar(calendar) { const days = []; - for (let i = 29; i >= 0; i--) { - const d = new Date(); d.setDate(d.getDate() - i); - const key = d.toISOString().split('T')[0]; - const data = cal[key]; - const num = d.getDate(); - if (!data) { days.push(`