/* ── 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 => `
`).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
${buildCalendar(calendar)}
${buildSizeChart(history)}
Datum Status Dauer Größe Dateien
${history.slice(0, 20).map(e => `
${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}` : ''}
`).join('')}
⚙ Bearbeiten
🗑 Löschen
`;
}
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;
}