/* ── Backup Monitor – Frontend Logic ────────────────────────── */ const API = ''; let refreshTimer; // ── Init ────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { loadAll(); refreshTimer = setInterval(loadAll, 30000); }); async function loadAll() { await Promise.all([loadSummary(), loadHosts()]); document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString('de-DE'); } // ── Summary ─────────────────────────────────────────────────── async function loadSummary() { const r = await fetch(`${API}/api/summary`); const d = await r.json(); document.getElementById('valTotal').textContent = d.total_hosts; document.getElementById('valOk').textContent = d.ok; document.getElementById('valStale').textContent = d.stale; document.getElementById('valError').textContent = d.error; document.getElementById('valToday').textContent = d.today_backups; document.getElementById('valSize').textContent = fmtBytes(d.today_size); const pulse = document.getElementById('globalPulse'); pulse.className = 'pulse-dot'; if (d.error > 0) pulse.classList.add('error'); else if (d.stale > 0) pulse.classList.add('warn'); } // ── Host Grid ───────────────────────────────────────────────── async function loadHosts() { const r = await fetch(`${API}/api/hosts`); const hosts = await r.json(); const grid = document.getElementById('hostGrid'); grid.innerHTML = hosts.map(h => `
${h.name} ${statusLabel(h.status)}
Letztes Backup ${h.last_backup ? timeAgo(h.last_backup) : 'Nie'}
7-Tage ${h.backup_count_7d} Backups
Ø Dauer ${fmtDuration(h.avg_duration_7d)}
7-Tage Volumen ${fmtBytes(h.total_size_7d)}
`).join(''); // Load minibars for (const h of hosts) { loadMinibar(h.name); } } async function loadMinibar(host) { const r = await fetch(`${API}/api/calendar/${host}?days=14`); const cal = await r.json(); const el = document.getElementById(`mini-${host}`); if (!el) return; const days = []; for (let i = 13; i >= 0; i--) { const d = new Date(); d.setDate(d.getDate() - i); const key = d.toISOString().split('T')[0]; days.push({ key, data: cal[key] || null }); } const maxSize = Math.max(...days.map(d => d.data?.total_size || 0), 1); el.innerHTML = days.map(d => { if (!d.data) return `
`; const h = Math.max(15, (d.data.total_size / maxSize) * 100); const cls = d.data.has_error ? 'error' : 'ok'; return `
`; }).join(''); } // ── Host Detail Drawer ──────────────────────────────────────── async function openHost(name) { document.getElementById('drawerTitle').textContent = name; document.getElementById('drawerOverlay').classList.add('open'); document.getElementById('drawer').classList.add('open'); const body = document.getElementById('drawerBody'); body.innerHTML = '
Lade...
'; const [histR, calR, hostsR] = await Promise.all([ fetch(`${API}/api/history/${name}?days=30`), fetch(`${API}/api/calendar/${name}?days=30`), fetch(`${API}/api/hosts`), ]); const history = await histR.json(); const calendar = await calR.json(); const hosts = await hostsR.json(); const host = hosts.find(h => h.name === name) || {}; // Stats const totalSize = history.reduce((s, e) => s + e.original_size, 0); const avgDuration = history.length ? Math.round(history.reduce((s, e) => s + e.duration_sec, 0) / history.length) : 0; const successRate = history.length ? Math.round(history.filter(e => e.status === 'ok').length / history.length * 100) : 0; body.innerHTML = `
${history.length}
Backups (30d)
${successRate}%
Erfolgsrate
${fmtDuration(avgDuration)}
Ø Dauer
Kalender (30 Tage)
${buildCalendar(calendar)}
Datenvolumen (30 Tage)
${buildSizeChart(history)}
Letzte Backups
${history.slice(0, 20).map(e => ` `).join('')}
DatumStatusDauerGrößeDateien
${new Date(e.timestamp).toLocaleString('de-DE', {day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'})} ${e.status} ${fmtDuration(e.duration_sec)} ${fmtBytes(e.original_size)} ${e.nfiles_new ? `+${e.nfiles_new}` : '–'} ${e.nfiles_changed ? `/ ~${e.nfiles_changed}` : ''}
`; } function buildCalendar(cal) { const days = []; const now = new Date(); 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 dayNum = d.getDate(); if (!data) { days.push(`
${dayNum}
${key}
Kein Backup
`); } else { const cls = data.has_error ? 'error' : 'ok'; days.push(`
${dayNum}
${key}
${data.count}x Backup
${fmtBytes(data.total_size)}
Ø ${fmtDuration(data.avg_duration)}
`); } } // Future days to fill the row for (let i = 1; i <= 5; i++) { const d = new Date(); d.setDate(d.getDate() + i); days.push(`
${d.getDate()}
`); } return days.join(''); } function buildSizeChart(history) { // Group by day, last 30 days const byDay = {}; history.forEach(e => { const day = e.timestamp.split('T')[0]; if (!byDay[day]) byDay[day] = 0; byDay[day] += e.original_size; }); 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]; days.push({ key, size: byDay[key] || 0 }); } const maxSize = Math.max(...days.map(d => d.size), 1); return days.map(d => { const h = d.size ? Math.max(4, (d.size / maxSize) * 100) : 2; const opacity = d.size ? '' : 'opacity:0.2;'; return `
${d.key}
${fmtBytes(d.size)}
`; }).join(''); } function closeDrawer() { document.getElementById('drawerOverlay').classList.remove('open'); document.getElementById('drawer').classList.remove('open'); } // ── Add/Edit Host Modal ─────────────────────────────────────── function openAddHost() { document.getElementById('modalTitle').textContent = 'Host hinzufügen'; document.getElementById('formMode').value = 'add'; document.getElementById('formName').value = ''; document.getElementById('formName').disabled = false; document.getElementById('formKumaUrl').value = ''; document.getElementById('formEnabled').checked = true; openModal(); } async function openEditHost(name) { closeDrawer(); const r = await fetch(`${API}/api/hosts`); const hosts = await r.json(); const h = hosts.find(x => x.name === name); if (!h) return; document.getElementById('modalTitle').textContent = `${name} bearbeiten`; document.getElementById('formMode').value = 'edit'; document.getElementById('formName').value = h.name; document.getElementById('formName').disabled = true; document.getElementById('formKumaUrl').value = h.kuma_push_url || ''; document.getElementById('formEnabled').checked = h.enabled; openModal(); } async function saveHost(e) { e.preventDefault(); const mode = document.getElementById('formMode').value; const name = document.getElementById('formName').value.trim(); const kuma = document.getElementById('formKumaUrl').value.trim(); const enabled = document.getElementById('formEnabled').checked; if (mode === 'add') { await fetch(`${API}/api/hosts`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ name, kuma_push_url: kuma }) }); toast(`${name} hinzugefügt`, 'success'); } else { await fetch(`${API}/api/hosts/${name}`, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ kuma_push_url: kuma, enabled }) }); toast(`${name} aktualisiert`, 'success'); } closeModal(); loadAll(); } async function confirmDelete(name) { if (!confirm(`Host "${name}" und alle History wirklich löschen?`)) return; await fetch(`${API}/api/hosts/${name}`, { method: 'DELETE' }); toast(`${name} gelöscht`, 'success'); closeDrawer(); loadAll(); } function openModal() { document.getElementById('modalOverlay').classList.add('open'); document.getElementById('modal').classList.add('open'); } function closeModal() { document.getElementById('modalOverlay').classList.remove('open'); document.getElementById('modal').classList.remove('open'); } // ── Toast ───────────────────────────────────────────────────── function toast(msg, type = 'success') { const c = document.getElementById('toastContainer'); const t = document.createElement('div'); t.className = `toast ${type}`; t.innerHTML = `${type === 'success' ? '✓' : '✕'} ${msg}`; c.appendChild(t); setTimeout(() => t.remove(), 4000); } // ── Helpers ─────────────────────────────────────────────────── function fmtBytes(b) { if (!b || b === 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(b) / Math.log(1024)); return (b / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0) + ' ' + units[i]; } function fmtDuration(sec) { if (!sec || sec === 0) return '–'; if (sec < 60) return `${sec}s`; if (sec < 3600) return `${Math.floor(sec/60)}m ${sec%60}s`; return `${Math.floor(sec/3600)}h ${Math.floor((sec%3600)/60)}m`; } function timeAgo(iso) { const diff = (Date.now() - new Date(iso).getTime()) / 1000; if (diff < 60) return 'gerade eben'; if (diff < 3600) return `vor ${Math.floor(diff/60)}m`; if (diff < 86400) return `vor ${Math.floor(diff/3600)}h`; return `vor ${Math.floor(diff/86400)}d`; } function statusLabel(s) { return { ok: 'OK', stale: 'Überfällig', error: 'Fehler', disabled: 'Deaktiviert' }[s] || s; }