// Admin → Health: is the server in good shape? Disk use, snapshots (make, download, restore) and recent failures. import { useCallback, useEffect, useState } from 'react'; import { adminBackupNow, adminGetHealth, adminRestoreSnapshot, adminSnapshotUrl } from '../syncService.js'; const btn = 'rounded-lg border border-[var(--r-line)] px-3 py-1.5 text-xs hover:bg-black/5 disabled:opacity-50'; const card = 'rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-4'; export const formatBytes = (n) => { if (n == null) return 'unknown'; if (n < 1024) return `${n} B`; const units = ['KB', 'MB', 'GB', 'TB']; let v = n / 1024; let i = 0; while (v >= 1024 && i < units.length - 1) { v /= 1024; i += 1; } return `${v < 10 ? v.toFixed(1) : Math.round(v)} ${units[i]}`; }; export const formatUptime = (s) => { if (s < 90) return `${s} seconds`; if (s < 5400) return `${Math.round(s / 60)} minutes`; if (s < 129600) return `${Math.round(s / 3600)} hours`; return `${Math.round(s / 86400)} days`; }; const ago = (at) => { const m = Math.round((Date.now() - at) / 60000); if (m < 2) return 'just now'; if (m < 120) return `${m} minutes ago`; if (m < 2880) return `${Math.round(m / 60)} hours ago`; return `${Math.round(m / 1440)} days ago`; }; function Log({ title, empty, rows }) { return (

{title}

{rows.length === 0 ?

{empty}

: ( )}
); } export default function AdminHealth() { const [state, setState] = useState({ loading: true, error: '', data: null }); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(''); const [restoring, setRestoring] = useState(null); // snapshot name awaiting typed confirmation const [typed, setTyped] = useState(''); const load = useCallback(async () => { const r = await adminGetHealth(); setState(r.ok ? { loading: false, error: '', data: r.data } : { loading: false, error: r.error ?? 'Could not read server health.', data: null }); }, []); useEffect(() => { load(); }, [load]); const backupNow = async () => { setBusy(true); setMessage(''); const r = await adminBackupNow(); setBusy(false); setMessage(r.ok ? 'Snapshot saved.' : r.error ?? 'The snapshot could not be made.'); load(); }; const restore = async () => { setBusy(true); setMessage(''); const r = await adminRestoreSnapshot(restoring); setBusy(false); if (r.ok) { setRestoring(null); setTyped(''); setMessage(`Restored ${r.data.restored}. What was there before is saved as ${r.data.safetyCopy}. You may need to sign in again.`); load(); } else setMessage(r.error ?? 'The snapshot could not be restored.'); }; if (state.loading) return

Loading…

; if (state.error) return

{state.error}

; const { data } = state; const { backups, disk } = data; const stale = backups.enabled && (!backups.latest || Date.now() - backups.latest.at > 36 * 3600 * 1000); const usedPct = disk.total ? Math.round(((disk.total - disk.free) / disk.total) * 100) : null; return (
Running for
{formatUptime(data.uptimeSeconds)}
Database
{formatBytes(disk.db)}
Snapshots
{formatBytes(disk.backups)}
Disk free
{formatBytes(disk.free)}{usedPct != null ? ` (${usedPct}% used)` : ''}

Node {data.node} · IQ Bible {data.iqConfigured ? 'connected' : 'not set up'} · data in {data.dataDir}

Snapshots

{!backups.enabled &&

Automatic snapshots are turned off (BACKUP_DISABLED).

} {stale &&

{backups.latest ? `The last snapshot is ${ago(backups.latest.at)}.` : 'There are no snapshots yet.'} One should be made every day.

} {data.backupFailures.length > 0 &&

Last failure: {data.backupFailures[0].detail} ({ago(data.backupFailures[0].at)})

} {backups.snapshots.length > 0 && ( )}

Keeps the newest {backups.keep}. A snapshot holds every account’s data.

{restoring && (

This replaces everyone’s data with what was saved in {restoring}. Anything changed since then is lost, though what’s there now is saved as a new snapshot first. Type the snapshot’s name to go ahead.

setTyped(e.target.value)} aria-label="Snapshot name" placeholder={restoring} className="w-full rounded-lg border border-[var(--r-line)] bg-[var(--r-bg)] px-3 py-2 text-xs" />
)} {message &&

{message}

}

The failure lists are kept in memory, so they start over when the server restarts.

); }