Updates tab (apt list/refresh/upgrade/changelog, reboot flag) on a new background tasks package with live WS streaming; header power button with hostname confirmation

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-17 17:40:29 -07:00
co-authored by Claude Fable 5.1
parent 41c2f05535
commit acb11193b9
11 changed files with 975 additions and 6 deletions
+47
View File
@@ -0,0 +1,47 @@
import { useState } from 'react'
import { api } from './api'
// Header power control: a small button, a dialog that needs the hostname typed back.
export default function Power({ host, rebootNeeded }: { host: string; rebootNeeded?: boolean }) {
const [open, setOpen] = useState(false)
const [typed, setTyped] = useState('')
const [busy, setBusy] = useState('')
const [err, setErr] = useState('')
const ok = typed.trim() === host
async function go(action: 'reboot' | 'poweroff') {
setBusy(action); setErr('')
try { await api.power(action, typed.trim()); setBusy('sent') }
catch (e) { setErr(e instanceof Error ? e.message : 'failed'); setBusy('') }
}
return (
<>
<button className={'icon-btn power' + (rebootNeeded ? ' attn' : '')} title={rebootNeeded ? 'reboot required' : 'power'} onClick={() => { setOpen(true); setTyped(''); setBusy(''); setErr('') }}>⏻</button>
{open && (
<div className="modal-back" onClick={() => busy !== 'sent' && setOpen(false)}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<h3>Power · {host}</h3>
{rebootNeeded && <div className="notice">A reboot is pending from package updates.</div>}
{busy === 'sent' ? (
<div className="muted">Command sent. The console will reconnect when the host is back.</div>
) : (
<>
<label className="muted">Type the hostname to confirm
<input autoFocus value={typed} onChange={(e) => setTyped(e.target.value)} placeholder={host} />
</label>
{err && <div className="error">{err}</div>}
<div className="svc-actions">
<button className="btn on" disabled={!ok || !!busy} onClick={() => go('reboot')}>{busy === 'reboot' ? '…' : 'Reboot'}</button>
<button className="btn danger" disabled={!ok || !!busy} onClick={() => go('poweroff')}>{busy === 'poweroff' ? '…' : 'Shut down'}</button>
<span className="spacer" />
<button className="btn" onClick={() => setOpen(false)}>Cancel</button>
</div>
</>
)}
</div>
</div>
)}
</>
)
}