import { useCallback, useEffect, useRef, useState } from 'react' import { api, TaskInfo, UpdatePackage, UpdatesStatus } from './api' import TaskOutput from './TaskOutput' import { Btn, Loading, Spinner, toast } from './ui' function ago(ms?: number): string { if (!ms) return 'never' const s = Math.max(0, (Date.now() - ms) / 1000) const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60) return (d > 0 ? `${d}d ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m`) + ' ago' } export default function Updates() { const [st, setSt] = useState(null) const [err, setErr] = useState('') const [reloading, setReloading] = useState(false) const [sel, setSel] = useState>(new Set()) const [task, setTask] = useState(null) const [taskRunning, setTaskRunning] = useState(false) const [open, setOpen] = useState(null) const [changelog, setChangelog] = useState>({}) const taskBox = useRef(null) const load = useCallback(async () => { setReloading(true) try { const s = await api.updates() setSt(s); setErr('') if (s.running_task) { setTask(s.running_task); setTaskRunning(true) } setSel((prev) => new Set([...prev].filter((n) => s.packages.some((p) => p.name === n)))) } catch (e) { setErr(e instanceof Error ? e.message : 'failed') } finally { setReloading(false) } }, []) useEffect(() => { load() }, [load]) async function start(fn: () => Promise) { setErr('') try { const t = await fn() setTask(t.id); setTaskRunning(true) window.setTimeout(() => taskBox.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }), 50) } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') } } function done(t: TaskInfo) { setTaskRunning(false) const ok = t.exit_code === 0 && !t.error toast(`${t.title}: ${ok ? 'finished' : t.error || `exited ${t.exit_code}`}`, ok ? 'ok' : 'err') load() } async function showChangelog(p: UpdatePackage) { if (open === p.name) { setOpen(null); return } setOpen(p.name) if (!changelog[p.name]) { try { const r = await api.changelog(p.name); setChangelog((c) => ({ ...c, [p.name]: r.text || '(empty)' })) } catch (e) { setChangelog((c) => ({ ...c, [p.name]: e instanceof Error ? e.message : 'unavailable' })) } } } const toggle = (n: string) => setSel((s) => { const x = new Set(s); if (x.has(n)) x.delete(n); else x.add(n); return x }) if (!st) return
{err ?
{err}
: }
const canAct = st.can_upgrade && !taskRunning return (
{!st.manager &&
No supported package manager found (looked for apt, dnf, zypper, pacman, apk).
} {st.manager && !st.sudo &&
Read-only: refreshing and upgrading need root. Run Ham Lab as root, or give its user passwordless sudo or doas.
} {st.reboot_needed && (
Reboot required{st.reboot_pkgs.length > 0 && <> · {st.reboot_pkgs.join(', ')}} — use the power button in the header.
)} {err &&
{err}
}
Upgradable
{st.packages.length}
{st.security_count ? `${st.security_count} security` : 'no security updates'}
Index refreshed
{ago(st.last_refresh)}
{st.manager}
Last upgrade
{ago(st.last_upgrade)}
from {st.manager} history
Unattended
{st.auto_upgrades ? 'on' : 'off'}
automatic upgrades
start(api.refreshUpdates)}>Refresh index start(() => api.upgrade([]))}>Upgrade all start(() => api.upgrade([...sel]))}>Upgrade selected ({sel.size}) {st.packages.length > 0 && }
{task && }
{st.packages.length === 0 &&
Everything is up to date.
} {st.packages.map((p) => (
toggle(p.name)} /> showChangelog(p)}> {p.name}{p.security && security} {p.description} {p.current} → {p.candidate} · {p.suite}
{open === p.name &&
{changelog[p.name] ??  fetching changelog…}
}
))}
) }