diff --git a/static/css/style.css b/static/css/style.css
index c1f5373..9452692 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -1,486 +1 @@
-/* ── Backup Monitor – Dark Theme UI ─────────────────────────── */
-
-:root {
- --bg: #0f1117;
- --bg-card: #1a1d27;
- --bg-card-hover: #22263a;
- --bg-drawer: #161922;
- --border: #2a2e3d;
- --text: #e4e6f0;
- --text-dim: #8b8fa3;
- --text-muted: #5c6078;
- --accent: #6c5ce7;
- --accent-glow: rgba(108, 92, 231, 0.3);
- --ok: #00d68f;
- --ok-bg: rgba(0, 214, 143, 0.1);
- --ok-glow: rgba(0, 214, 143, 0.25);
- --warn: #ffaa00;
- --warn-bg: rgba(255, 170, 0, 0.1);
- --warn-glow: rgba(255, 170, 0, 0.25);
- --error: #ff3d71;
- --error-bg: rgba(255, 61, 113, 0.1);
- --error-glow: rgba(255, 61, 113, 0.25);
- --disabled: #4a4e69;
- --radius: 12px;
- --radius-sm: 8px;
- --shadow: 0 4px 24px rgba(0,0,0,0.3);
- --transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
-}
-
-* { margin: 0; padding: 0; box-sizing: border-box; }
-
-body {
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
- background: var(--bg);
- color: var(--text);
- min-height: 100vh;
- overflow-x: hidden;
-}
-
-/* ── Header ────────────────────────────────────────────────── */
-
-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 16px 24px;
- background: var(--bg-card);
- border-bottom: 1px solid var(--border);
- position: sticky;
- top: 0;
- z-index: 100;
- backdrop-filter: blur(12px);
-}
-
-.header-left { display: flex; align-items: center; gap: 12px; }
-.logo { font-size: 28px; }
-h1 { font-size: 20px; font-weight: 700; letter-spacing: -0.5px; }
-.subtitle { color: var(--text-dim); font-size: 13px; }
-
-.header-right { display: flex; align-items: center; gap: 12px; }
-.meta-text { color: var(--text-muted); font-size: 12px; }
-
-.pulse-dot {
- width: 10px; height: 10px;
- border-radius: 50%;
- background: var(--ok);
- box-shadow: 0 0 8px var(--ok-glow);
- animation: pulse 2s infinite;
-}
-.pulse-dot.warn { background: var(--warn); box-shadow: 0 0 8px var(--warn-glow); }
-.pulse-dot.error { background: var(--error); box-shadow: 0 0 8px var(--error-glow); }
-
-@keyframes pulse {
- 0%, 100% { opacity: 1; transform: scale(1); }
- 50% { opacity: 0.6; transform: scale(0.85); }
-}
-
-/* ── Buttons ───────────────────────────────────────────────── */
-
-.btn {
- padding: 8px 16px;
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- background: var(--bg-card);
- color: var(--text);
- cursor: pointer;
- font-size: 13px;
- font-weight: 500;
- transition: all var(--transition);
-}
-.btn:hover { background: var(--bg-card-hover); border-color: var(--accent); }
-.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
-.btn-primary:hover { background: #7c6cf0; box-shadow: 0 0 20px var(--accent-glow); }
-.btn-icon { padding: 8px 10px; font-size: 16px; line-height: 1; }
-.btn-danger { color: var(--error); }
-.btn-danger:hover { background: var(--error-bg); border-color: var(--error); }
-.btn-sm { padding: 4px 10px; font-size: 12px; }
-
-/* ── Summary Grid ──────────────────────────────────────────── */
-
-.summary-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
- gap: 16px;
- padding: 24px;
-}
-
-.summary-card {
- text-align: center;
- padding: 20px 16px;
- position: relative;
- overflow: hidden;
-}
-.summary-card::before {
- content: '';
- position: absolute;
- top: 0; left: 0; right: 0;
- height: 3px;
- background: var(--border);
- border-radius: var(--radius) var(--radius) 0 0;
-}
-.card-ok::before { background: var(--ok); box-shadow: 0 0 12px var(--ok-glow); }
-.card-warn::before { background: var(--warn); box-shadow: 0 0 12px var(--warn-glow); }
-.card-error::before { background: var(--error); box-shadow: 0 0 12px var(--error-glow); }
-
-.card-value { font-size: 32px; font-weight: 800; letter-spacing: -1px; }
-.card-ok .card-value { color: var(--ok); }
-.card-warn .card-value { color: var(--warn); }
-.card-error .card-value { color: var(--error); }
-.card-label { color: var(--text-dim); font-size: 12px; margin-top: 4px; text-transform: uppercase; letter-spacing: 1px; }
-
-/* ── Cards ─────────────────────────────────────────────────── */
-
-.card {
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- transition: all var(--transition);
-}
-.card:hover { border-color: var(--accent); box-shadow: var(--shadow); }
-
-/* ── Host Grid ─────────────────────────────────────────────── */
-
-.host-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
- gap: 16px;
- padding: 0 24px 24px;
-}
-
-.host-card {
- cursor: pointer;
- padding: 20px;
- position: relative;
- overflow: hidden;
-}
-.host-card::after {
- content: '';
- position: absolute;
- top: 0; bottom: 0; left: 0;
- width: 4px;
- border-radius: var(--radius) 0 0 var(--radius);
-}
-.host-card[data-status="ok"]::after { background: var(--ok); box-shadow: 0 0 8px var(--ok-glow); }
-.host-card[data-status="stale"]::after { background: var(--warn); box-shadow: 0 0 8px var(--warn-glow); }
-.host-card[data-status="error"]::after { background: var(--error); box-shadow: 0 0 8px var(--error-glow); }
-.host-card[data-status="disabled"]::after { background: var(--disabled); }
-.host-card[data-status="disabled"] { opacity: 0.5; }
-
-.host-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
-.host-name { font-size: 16px; font-weight: 700; letter-spacing: -0.3px; }
-.host-badge {
- padding: 3px 10px;
- border-radius: 20px;
- font-size: 11px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-.badge-ok { background: var(--ok-bg); color: var(--ok); }
-.badge-stale { background: var(--warn-bg); color: var(--warn); }
-.badge-error { background: var(--error-bg); color: var(--error); }
-.badge-disabled { background: rgba(74,78,105,0.2); color: var(--disabled); }
-
-.host-meta { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
-.meta-item { display: flex; flex-direction: column; }
-.meta-label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
-.meta-value { font-size: 14px; font-weight: 600; margin-top: 2px; }
-
-.host-minibar {
- display: flex;
- gap: 2px;
- margin-top: 14px;
- height: 24px;
- align-items: flex-end;
-}
-.minibar-day {
- flex: 1;
- border-radius: 2px;
- min-height: 3px;
- transition: all var(--transition);
- position: relative;
-}
-.minibar-day:hover { opacity: 0.8; transform: scaleY(1.15); }
-.minibar-day.ok { background: var(--ok); }
-.minibar-day.error { background: var(--error); }
-.minibar-day.empty { background: var(--border); min-height: 3px; }
-
-/* ── Drawer ────────────────────────────────────────────────── */
-
-.drawer-overlay {
- position: fixed; inset: 0;
- background: rgba(0,0,0,0.6);
- backdrop-filter: blur(4px);
- z-index: 200;
- opacity: 0;
- pointer-events: none;
- transition: opacity 0.3s;
-}
-.drawer-overlay.open { opacity: 1; pointer-events: all; }
-
-.drawer {
- position: fixed;
- top: 0; right: -520px; bottom: 0;
- width: 500px;
- max-width: 90vw;
- background: var(--bg-drawer);
- border-left: 1px solid var(--border);
- z-index: 201;
- transition: right 0.35s cubic-bezier(0.4, 0, 0.2, 1);
- display: flex;
- flex-direction: column;
- box-shadow: -8px 0 40px rgba(0,0,0,0.4);
-}
-.drawer.open { right: 0; }
-
-.drawer-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 20px 24px;
- border-bottom: 1px solid var(--border);
-}
-.drawer-header h2 { font-size: 18px; font-weight: 700; }
-
-.drawer-body {
- flex: 1;
- overflow-y: auto;
- padding: 24px;
-}
-
-/* ── Drawer: Calendar Heatmap ──────────────────────────────── */
-
-.calendar-grid {
- display: grid;
- grid-template-columns: repeat(7, 1fr);
- gap: 4px;
- margin: 16px 0;
-}
-.cal-day {
- aspect-ratio: 1;
- border-radius: 4px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 10px;
- color: var(--text-muted);
- cursor: default;
- transition: all var(--transition);
- position: relative;
-}
-.cal-day:hover { transform: scale(1.15); z-index: 1; }
-.cal-day.empty { background: var(--bg-card); }
-.cal-day.ok { background: var(--ok); color: #fff; font-weight: 700; }
-.cal-day.error { background: var(--error); color: #fff; font-weight: 700; }
-.cal-day.future { background: transparent; border: 1px dashed var(--border); }
-
-.cal-day .tooltip {
- display: none;
- position: absolute;
- bottom: calc(100% + 6px);
- left: 50%;
- transform: translateX(-50%);
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- padding: 8px 12px;
- font-size: 11px;
- white-space: nowrap;
- z-index: 10;
- box-shadow: var(--shadow);
- color: var(--text);
-}
-.cal-day:hover .tooltip { display: block; }
-
-/* ── Drawer: History Table ─────────────────────────────────── */
-
-.history-table {
- width: 100%;
- border-collapse: collapse;
- font-size: 13px;
- margin-top: 16px;
-}
-.history-table th {
- text-align: left;
- padding: 8px 12px;
- color: var(--text-muted);
- font-size: 11px;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- border-bottom: 1px solid var(--border);
-}
-.history-table td {
- padding: 10px 12px;
- border-bottom: 1px solid var(--border);
-}
-.history-table tr:hover td { background: var(--bg-card-hover); }
-
-/* ── Drawer: Stats Row ─────────────────────────────────────── */
-
-.stats-row {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 12px;
- margin: 16px 0;
-}
-.stat-box {
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- padding: 14px;
- text-align: center;
-}
-.stat-value { font-size: 22px; font-weight: 800; color: var(--accent); }
-.stat-label { font-size: 11px; color: var(--text-muted); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
-
-/* ── Drawer: Size Chart ────────────────────────────────────── */
-
-.size-chart {
- display: flex;
- align-items: flex-end;
- gap: 3px;
- height: 80px;
- margin: 16px 0;
- padding: 0 2px;
-}
-.size-bar {
- flex: 1;
- background: var(--accent);
- border-radius: 3px 3px 0 0;
- min-height: 2px;
- transition: all var(--transition);
- position: relative;
- opacity: 0.7;
-}
-.size-bar:hover { opacity: 1; }
-.size-bar .tooltip {
- display: none;
- position: absolute;
- bottom: calc(100% + 6px);
- left: 50%;
- transform: translateX(-50%);
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- padding: 6px 10px;
- font-size: 11px;
- white-space: nowrap;
- z-index: 10;
- box-shadow: var(--shadow);
- color: var(--text);
-}
-.size-bar:hover .tooltip { display: block; }
-
-/* ── Modal ─────────────────────────────────────────────────── */
-
-.modal-overlay {
- position: fixed; inset: 0;
- background: rgba(0,0,0,0.6);
- backdrop-filter: blur(4px);
- z-index: 300;
- opacity: 0;
- pointer-events: none;
- transition: opacity 0.3s;
-}
-.modal-overlay.open { opacity: 1; pointer-events: all; }
-
-.modal {
- position: fixed;
- top: 50%; left: 50%;
- transform: translate(-50%, -50%) scale(0.95);
- background: var(--bg-drawer);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- padding: 0;
- width: 440px;
- max-width: 90vw;
- z-index: 301;
- opacity: 0;
- pointer-events: none;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- box-shadow: 0 20px 60px rgba(0,0,0,0.5);
-}
-.modal.open { opacity: 1; pointer-events: all; transform: translate(-50%, -50%) scale(1); }
-
-.modal-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 20px 24px;
- border-bottom: 1px solid var(--border);
-}
-.modal-header h2 { font-size: 16px; font-weight: 700; }
-
-.modal form { padding: 24px; }
-.form-group { margin-bottom: 16px; }
-.form-group label { display: block; font-size: 12px; color: var(--text-dim); margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.5px; }
-.form-group input[type="text"],
-.form-group input[type="url"] {
- width: 100%;
- padding: 10px 14px;
- background: var(--bg);
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- color: var(--text);
- font-size: 14px;
- transition: border-color var(--transition);
-}
-.form-group input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
-.checkbox-label { display: flex !important; align-items: center; gap: 8px; cursor: pointer; font-size: 14px !important; text-transform: none !important; letter-spacing: 0 !important; color: var(--text) !important; }
-.form-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 24px; }
-
-/* ── Toast ─────────────────────────────────────────────────── */
-
-.toast-container { position: fixed; bottom: 24px; right: 24px; z-index: 400; display: flex; flex-direction: column; gap: 8px; }
-.toast {
- padding: 12px 20px;
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- font-size: 13px;
- box-shadow: var(--shadow);
- animation: slideIn 0.3s ease-out;
- display: flex;
- align-items: center;
- gap: 8px;
-}
-.toast.success { border-left: 3px solid var(--ok); }
-.toast.error { border-left: 3px solid var(--error); }
-
-@keyframes slideIn {
- from { transform: translateX(100px); opacity: 0; }
- to { transform: translateX(0); opacity: 1; }
-}
-
-/* ── Section Headers ───────────────────────────────────────── */
-
-.section-header {
- font-size: 13px;
- color: var(--text-muted);
- text-transform: uppercase;
- letter-spacing: 1px;
- margin: 20px 0 12px;
- padding-bottom: 8px;
- border-bottom: 1px solid var(--border);
-}
-
-/* ── Drawer Actions ────────────────────────────────────────── */
-
-.drawer-actions {
- display: flex;
- gap: 8px;
- margin-top: 24px;
- padding-top: 16px;
- border-top: 1px solid var(--border);
-}
-
-/* ── Responsive ────────────────────────────────────────────── */
-
-@media (max-width: 768px) {
- header { flex-direction: column; gap: 12px; }
- .summary-grid { grid-template-columns: repeat(3, 1fr); padding: 16px; }
- .host-grid { grid-template-columns: 1fr; padding: 0 16px 16px; }
- .drawer { width: 100%; max-width: 100vw; }
- .stats-row { grid-template-columns: 1fr; }
-}
+/* Sentinel Design System – all styles via Tailwind CSS */
diff --git a/static/js/app.js b/static/js/app.js
index fec0435..73ccfd8 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -1,13 +1,14 @@
-/* ── Backup Monitor – Frontend Logic ────────────────────────── */
+/* ── The Sentinel – Backup Monitor Frontend ────────────────── */
const API = '';
-let refreshTimer;
let apiKey = localStorage.getItem('bm_api_key') || '';
+let allHosts = [];
+let currentPage = 'dashboard';
-// ── Init ──────────────────────────────────────────────────────
+// ── Init ──────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
loadAll();
- refreshTimer = setInterval(loadAll, 30000);
+ setInterval(loadAll, 30000);
});
function authHeaders() {
@@ -22,232 +23,273 @@ async function apiFetch(url, opts = {}) {
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);
- }
+ if (key) { apiKey = key; localStorage.setItem('bm_api_key', key); opts.headers['X-API-Key'] = key; return fetch(url, opts); }
}
return r;
}
async function loadAll() {
- await Promise.all([loadSummary(), loadHosts()]);
- document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString('de-DE');
+ 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 = 'error Errors Active '; }
+ else if (sum.stale > 0) { ss.innerHTML = 'warning Stale Hosts '; }
+ else { ss.innerHTML = 'cloud_done All Systems OK '; }
}
-// ── 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);
+// ── 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' : '');
- 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');
+ // Latest backup
+ const sorted = [...allHosts].filter(h => h.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('mLatestHost').textContent = sorted[0].name;
+ }
+
+ // 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;
+
+ // Live stream
+ renderLiveStream();
+ loadVolumeChart();
}
-// ── 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 => `
-
-
-
-
+function clusterGroup(label, hosts, color) {
+ return `
+
+
+ ${hosts.map(h => `
+
+
+
${h.name}
+
schedule ${h.last_backup ? timeAgo(h.last_backup) : 'Never'}
+
+
${h.status.toUpperCase()}
- `).join('');
-
- // Load minibars
- for (const h of hosts) {
- loadMinibar(h.name);
- }
+ `).join('')}
+
`;
}
-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 `
`;
+function renderLiveStream() {
+ const sorted = [...allHosts].filter(h => h.last_backup).sort((a,b) => new Date(b.last_backup) - new Date(a.last_backup)).slice(0, 8);
+ const ls = document.getElementById('liveStream');
+ ls.innerHTML = sorted.map(h => {
+ const t = new Date(h.last_backup).toLocaleTimeString('de-DE', {hour:'2-digit',minute:'2-digit',second:'2-digit'});
+ const isErr = h.last_status !== 'ok';
+ return `
+
+
${t}
+
+
${isErr ? 'ERROR: ' : ''}Backup for ${h.name} ${isErr ? 'failed' : 'completed successfully'}.
+ ${h.last_message ? `
${h.last_message} ` : ''}
+
`;
}).join('');
}
-// ── Host Detail Drawer ────────────────────────────────────────
+async function loadVolumeChart() {
+ // Aggregate daily totals from all hosts
+ const days = [];
+ for (let i = 29; i >= 0; i--) { const d = new Date(); d.setDate(d.getDate() - i); days.push(d.toISOString().split('T')[0]); }
+ const dailyTotals = {};
+ days.forEach(d => dailyTotals[d] = 0);
+
+ // Fetch calendar for top hosts (limit to avoid too many requests)
+ const topHosts = allHosts.slice(0, 10);
+ const cals = await Promise.all(topHosts.map(h => fetch(`${API}/api/calendar/${h.name}?days=30`).then(r => r.json())));
+ cals.forEach(cal => { Object.entries(cal).forEach(([day, data]) => { if (dailyTotals[day] !== undefined) dailyTotals[day] += data.total_size; }); });
+
+ const values = days.map(d => dailyTotals[d]);
+ const max = Math.max(...values, 1);
+ const points = values.map((v, i) => `${(i / (values.length - 1)) * 100},${100 - (v / max) * 80}`);
+ const pathD = 'M' + points.join(' L');
+ const fillD = pathD + ` L100,100 L0,100 Z`;
+
+ document.getElementById('chartSvg').innerHTML = `
+
+ ${[20,40,60,80].map(y => `
`).join('')}
+
+
+ `;
+ document.getElementById('chartSvg').setAttribute('viewBox', '0 0 100 100');
+ document.getElementById('chartSvg').setAttribute('preserveAspectRatio', 'none');
+}
+
+// ── Alerts ────────────────────────────────────────────────
+function renderAlerts() {
+ const issues = allHosts.filter(h => h.status === 'error' || h.status === 'stale');
+ document.getElementById('aCrit').textContent = String(allHosts.filter(h => h.status === 'error').length).padStart(2, '0');
+ document.getElementById('aStale').textContent = String(allHosts.filter(h => h.status === 'stale').length).padStart(2, '0');
+
+ const al = document.getElementById('alertList');
+ if (!issues.length) { al.innerHTML = '
No active alerts – all systems operational ✓
'; 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';
+ return `
+
+
+
+ ${icon}
+
+
+
+
${isCrit ? 'Backup Failed' : 'Backup Overdue'} – ${h.name}
+ ${label}
+
+
+ dns ${h.name}
+ schedule ${h.last_backup ? timeAgo(h.last_backup) : 'Never'}
+ ${h.last_message ? `${h.last_message} ` : `${Math.round(h.age_hours)}h without backup `}
+
+
+
+ Details
+
+
+
`;
+ }).join('');
+}
+
+// ── Host Grid ─────────────────────────────────────────────
+function renderHostGrid() {
+ const grid = document.getElementById('hostGrid');
+ grid.innerHTML = allHosts.map(h => `
+
+
+
+
${h.name}
+
${h.status.toUpperCase()}
+
+
+
Last Backup ${h.last_backup ? timeAgo(h.last_backup) : 'Never'}
+
7d Backups ${h.backup_count_7d}
+
Avg Duration ${fmtDuration(h.avg_duration_7d)}
+
7d Volume ${fmtBytes(h.total_size_7d)}
+
+
+ `).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');
-
+ document.getElementById('drawerBg').classList.remove('opacity-0','pointer-events-none');
+ document.getElementById('drawer').classList.remove('translate-x-full');
const body = document.getElementById('drawerBody');
- body.innerHTML = '
Lade...
';
+ body.innerHTML = '
Loading...
';
- 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 [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 hosts = await hostsR.json();
- const host = hosts.find(h => h.name === name) || {};
+ const host = allHosts.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;
+ 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 = `
-
-
-
${history.length}
-
Backups (30d)
-
-
-
${successRate}%
-
Erfolgsrate
-
-
-
${fmtDuration(avgDuration)}
-
Ø Dauer
-
-
+
+
-
-
${buildCalendar(calendar)}
+
+
30-Day Calendar
+
${buildCalendar(calendar)}
-
-
${buildSizeChart(history)}
+
+
Data Volume
+
${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('')}
-
-
+
+
Recent Backups
+
+ ${history.slice(0, 15).map(e => `
+
+ ${new Date(e.timestamp).toLocaleString('de-DE',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'})}
+
+ ${fmtDuration(e.duration_sec)}
+ ${fmtBytes(e.original_size)}
+ ${e.nfiles_new ? `+${e.nfiles_new}` : ''}
+
+ `).join('')}
+
-
- ⚙ Bearbeiten
- 🗑 Löschen
-
+
+
+
+ settings Edit
+
+
+ delete Delete
+
+
`;
}
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 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)}
`);
+ const num = d.getDate();
+ if (!data) { days.push(`
${num}
`); }
+ else {
+ const cls = data.has_error ? 'bg-error/20 text-error' : 'bg-secondary/20 text-secondary';
+ days.push(`
${num}
`);
}
}
- // 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;
- });
-
+ history.forEach(e => { const d = e.timestamp.split('T')[0]; byDay[d] = (byDay[d]||0) + 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);
+ for (let i = 29; i >= 0; i--) { const d = new Date(); d.setDate(d.getDate()-i); days.push({key:d.toISOString().split('T')[0], size: byDay[d.toISOString().split('T')[0]]||0}); }
+ const max = 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)}
`;
+ const h = d.size ? Math.max(6, (d.size/max)*100) : 4;
+ return `
`;
}).join('');
}
function closeDrawer() {
- document.getElementById('drawerOverlay').classList.remove('open');
- document.getElementById('drawer').classList.remove('open');
+ document.getElementById('drawerBg').classList.add('opacity-0','pointer-events-none');
+ document.getElementById('drawer').classList.add('translate-x-full');
}
-// ── Add/Edit Host Modal ───────────────────────────────────────
+// ── Modal ─────────────────────────────────────────────────
function openAddHost() {
- document.getElementById('modalTitle').textContent = 'Host hinzufügen';
+ document.getElementById('modalTitle').textContent = 'Add Host';
document.getElementById('formMode').value = 'add';
- document.getElementById('formName').value = '';
- document.getElementById('formName').disabled = false;
+ document.getElementById('formName').value = ''; document.getElementById('formName').disabled = false;
document.getElementById('formKumaUrl').value = '';
document.getElementById('formEnabled').checked = true;
openModal();
@@ -255,15 +297,10 @@ function openAddHost() {
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`;
+ const h = allHosts.find(x => x.name === name); if (!h) return;
+ document.getElementById('modalTitle').textContent = `Edit: ${name}`;
document.getElementById('formMode').value = 'edit';
- document.getElementById('formName').value = h.name;
- document.getElementById('formName').disabled = true;
+ 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();
@@ -275,77 +312,49 @@ async function saveHost(e) {
const name = document.getElementById('formName').value.trim();
const kuma = document.getElementById('formKumaUrl').value.trim();
const enabled = document.getElementById('formEnabled').checked;
-
if (mode === 'add') {
- await apiFetch(`${API}/api/hosts`, {
- method: 'POST',
- headers: authHeaders(),
- body: JSON.stringify({ name, kuma_push_url: kuma })
- });
- toast(`${name} hinzugefügt`, 'success');
+ await apiFetch(`${API}/api/hosts`, { method:'POST', headers:authHeaders(), body:JSON.stringify({name, kuma_push_url:kuma}) });
+ toast(`${name} added`);
} else {
- await apiFetch(`${API}/api/hosts/${name}`, {
- method: 'PUT',
- headers: authHeaders(),
- body: JSON.stringify({ kuma_push_url: kuma, enabled })
- });
- toast(`${name} aktualisiert`, 'success');
+ await apiFetch(`${API}/api/hosts/${name}`, { method:'PUT', headers:authHeaders(), body:JSON.stringify({kuma_push_url:kuma, enabled}) });
+ toast(`${name} updated`);
}
-
- closeModal();
- loadAll();
+ closeModal(); loadAll();
}
async function confirmDelete(name) {
- if (!confirm(`Host "${name}" und alle History wirklich löschen?`)) return;
- await apiFetch(`${API}/api/hosts/${name}`, { method: 'DELETE', headers: authHeaders() });
- toast(`${name} gelöscht`, 'success');
- closeDrawer();
- loadAll();
+ if (!confirm(`Delete "${name}" and all history?`)) return;
+ await apiFetch(`${API}/api/hosts/${name}`, { method:'DELETE', headers:authHeaders() });
+ toast(`${name} deleted`); 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');
+function openModal() { document.getElementById('modalBg').classList.remove('opacity-0','pointer-events-none'); const m = document.getElementById('modal'); m.classList.remove('scale-95','opacity-0','pointer-events-none'); }
+function closeModal() { document.getElementById('modalBg').classList.add('opacity-0','pointer-events-none'); const m = document.getElementById('modal'); m.classList.add('scale-95','opacity-0','pointer-events-none'); }
+
+// ── Navigation ────────────────────────────────────────────
+function showPage(page) {
+ currentPage = page;
+ ['dashboard','alerts','hosts','config'].forEach(p => {
+ document.getElementById(`page-${p}`).classList.toggle('hidden', p !== page);
+ // Nav highlights
+ const nav = document.getElementById(`nav-${p}`);
+ const side = document.getElementById(`side-${p}`);
+ if (nav) { nav.className = p === page ? 'text-sm font-bold tracking-tight text-blue-400 px-3 py-1 rounded-lg font-headline' : 'text-sm font-medium tracking-tight text-slate-400 hover:bg-slate-800/50 px-3 py-1 rounded-lg transition-colors font-headline'; }
+ if (side) { side.className = p === page ? 'flex items-center gap-3 px-4 py-3 rounded-xl bg-blue-600/10 text-blue-400 border-r-2 border-blue-500 font-headline text-sm font-semibold' : 'flex items-center gap-3 px-4 py-3 rounded-xl text-slate-500 hover:text-slate-300 hover:bg-slate-900/80 transition-all font-headline text-sm font-semibold'; }
+ });
}
-// ── Toast ─────────────────────────────────────────────────────
-function toast(msg, type = 'success') {
- const c = document.getElementById('toastContainer');
+// ── Toast ─────────────────────────────────────────────────
+function toast(msg) {
const t = document.createElement('div');
- t.className = `toast ${type}`;
- t.innerHTML = `${type === 'success' ? '✓' : '✕'} ${msg}`;
- c.appendChild(t);
+ t.className = 'glass px-5 py-3 rounded-xl text-sm font-medium text-white shadow-2xl flex items-center gap-2 animate-[slideIn_0.3s_ease-out]';
+ t.innerHTML = `
check_circle ${msg}`;
+ document.getElementById('toasts').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;
-}
+// ── Helpers ───────────────────────────────────────────────
+function fmtBytes(b) { if (!b) return '0 B'; const u = ['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)+' '+u[i]; }
+function fmtDuration(s) { if (!s) return '–'; if (s<60) return s+'s'; if (s<3600) return Math.floor(s/60)+'m'; return Math.floor(s/3600)+'h '+Math.floor((s%3600)/60)+'m'; }
+function timeAgo(iso) { const d=(Date.now()-new Date(iso).getTime())/1000; if(d<60) return 'just now'; if(d<3600) return Math.floor(d/60)+'m ago'; if(d<86400) return Math.floor(d/3600)+'h ago'; return Math.floor(d/86400)+'d ago'; }
+function statusChipClass(s) { return { ok:'bg-secondary/10 text-secondary border border-secondary/20', error:'bg-error/10 text-error border border-error/20', stale:'bg-tertiary/10 text-tertiary border border-tertiary/20', disabled:'bg-outline/10 text-outline border border-outline/20' }[s] || ''; }
diff --git a/templates/index.html b/templates/index.html
index 91f2a71..6eb279d 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -1,102 +1,295 @@
-
+
-
-
-
Backup Monitor – Pfannkuchen
-
-
+
+
+
The Sentinel – Backup Monitor
+
+
+
+
+
+
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ cloud_done
+ System OK
+
+
refresh
+
add_circle
+
+
-
-
+
+
+
+
+
+ shield
+
+
+
The Sentinel
+
Backup Monitor
+
+
+
+
+
+
+ add New Host
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
–
+
Latest Backup
+
–
+
+
+
+
+
Issues
+
+ View alerts arrow_forward
+
+
+
+
+
+
+
+
+
+
+
Backup Volume Trends
+
Storage growth across all hosts (Last 30 Days)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Live Backup Stream
+ Auto-refresh: 30s
+
+
+
+
+
+
+
+
+
+
No active alerts – all systems operational ✓
+
+
+
+
+
+
+
+
+
+
+
+
Push Endpoint
+ POST /api/push
+
+
+
Prometheus Metrics
+ GET /metrics
+
+
+
API Key
+
{{ 'Enabled – set via API_KEY env var' if api_key_required else 'Disabled – all endpoints open' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Add Host
+ close
+
+
+
+
+
+
+
+