// public/views/cards/backups.js — offsite DR backup status (Core-4 -> Farm/Won). // Fed by /usr/local/bin/offsite-backup.sh which POSTs each run to /api/backups. import { el, mount } from '../../dom.js'; import { api } from '../../api.js'; let body, timer; const gb = b => (b == null ? '–' : b >= 1e12 ? (b / 1e12).toFixed(1) + 'T' : Math.round(b / 1e9) + 'G'); function ago(ts) { const s = Math.max(0, (Date.now() - Date.parse(ts)) / 1000); if (s < 3600) return Math.floor(s / 60) + 'm'; if (s < 86400) return Math.floor(s / 3600) + 'h'; return Math.floor(s / 86400) + 'd'; } async function load() { if (!body) return; try { const d = await api.get('/api/backups'); const r = d.latest; if (!r) { mount(body, el('span', { class: 'muted' }, 'No offsite backups yet.')); return; } const stale = (Date.now() - Date.parse(r.ran_at)) > 8 * 86400000; // >8d overdue const status = (!r.ok || stale) ? 'bad' : 'ok'; const kids = []; kids.push(el('div', { class: 'sv-row' }, el('span', { class: 'k' }, 'Last run'), el('span', { class: 'cl-badge ' + status }, r.ok ? ago(r.ran_at) + ' ago' : 'FAILED'))); kids.push(el('div', { class: 'sv-row' }, el('span', { class: 'k' }, 'Pushed to Farm'), el('span', {}, gb(r.total_bytes)))); for (const g of (r.guests || [])) kids.push(el('div', { class: 'sv-row' }, el('span', { class: 'k' }, 'CT ' + g.vmid + ' ' + g.name), el('span', { class: 'muted' }, gb(g.bytes)))); kids.push(el('div', { class: 'sv-row' }, el('span', { class: 'k' }, 'Farm free'), el('span', {}, gb(r.won_free_bytes)))); kids.push(el('div', { class: 'sv-row' }, el('span', { class: 'k' }, 'Schedule'), el('span', { class: 'muted' }, d.schedule || 'weekly'))); mount(body, el('div', { class: 'sv-cluster' }, ...kids)); } catch { mount(body, el('span', { class: 'muted' }, 'Backups unavailable')); } } export default { id: 'backups', title: 'Backups · offsite', size: 's', mount(e) { body = e; load(); }, start() { timer = setInterval(load, 60000); }, stop() { clearInterval(timer); body = null; } };