feat(sv): Backups card — offsite DR status (Core-4 -> Farm) + /api/backups (2.6.0)
migration 026 backup_runs; POST ingest (owner) from offsite-backup.sh, GET for the Sacred Valley card showing last run, per-guest sizes, Farm free, schedule. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
47
public/views/cards/backups.js
Normal file
47
public/views/cards/backups.js
Normal file
@@ -0,0 +1,47 @@
|
||||
// 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; }
|
||||
};
|
||||
@@ -15,8 +15,9 @@ import speedtest from './cards/speedtest.js';
|
||||
import aiUsage from './cards/ai_usage.js';
|
||||
import cluster from './cards/cluster.js';
|
||||
import storage from './cards/storage.js';
|
||||
import backups from './cards/backups.js';
|
||||
|
||||
const CARD_MODULES = [clock, weather, hostPerf, cluster, storage, jobs, inbox, search, speedtest, aiUsage];
|
||||
const CARD_MODULES = [clock, weather, hostPerf, cluster, storage, backups, jobs, inbox, search, speedtest, aiUsage];
|
||||
const BY_ID = new Map(CARD_MODULES.map(d => [d.id, d]));
|
||||
|
||||
let active = []; // mounted cards needing stop()
|
||||
|
||||
Reference in New Issue
Block a user