Files
hamlab/web/src/Power.tsx
T
Mitchell-JohnsonandClaude Fable 5.1 668e716601 Top wears the skin; the refresh rate reaches every machine; bug bash 4
The process table's columns fit the box they are in, the rate control saves, curves are
timed on the viewer's clock and keep to the stride, the power icon is drawn, and a young
history draws in from the right instead of being stretched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 10:12:45 -07:00

54 lines
2.7 KiB
TypeScript

import { useState } from 'react'
import { api } from './api'
import { Btn, Spinner, useEscape } from './ui'
// Header power control: a small button, a dialog that needs the hostname typed back.
// `label` is what the user types (the machine name on a remote); `host` is what the node checks.
export default function Power({ host, label, rebootNeeded }: { host: string; label: string; rebootNeeded?: boolean }) {
const [open, setOpen] = useState(false)
const [typed, setTyped] = useState('')
const [sent, setSent] = useState<'' | 'reboot' | 'poweroff'>('')
useEscape(() => { if (!sent) setOpen(false) }, open)
const [err, setErr] = useState('')
const ok = typed.trim() === label
async function go(action: 'reboot' | 'poweroff') {
setErr('')
try { await api.power(action, host); setSent(action) }
catch (e) { setErr(e instanceof Error ? e.message : 'failed') }
}
return (
<>
<button className={'icon-btn power' + (rebootNeeded ? ' attn' : '')} title={rebootNeeded ? 'reboot required' : 'power'} onClick={() => { setOpen(true); setTyped(''); setSent(''); setErr('') }}>
{/* drawn, not typed: ⏻ (U+23FB) is missing from Windows' fonts and shows as a box of hex digits */}
<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" aria-hidden="true"><path d="M8 1.8v6" /><path d="M4.6 4.2a5.2 5.2 0 1 0 6.8 0" /></svg>
</button>
{open && (
<div className="modal-back" onClick={() => !sent && setOpen(false)}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<h3>Power · {label}</h3>
{rebootNeeded && <div className="notice">A reboot is pending from package updates.</div>}
{sent ? (
<div className="muted"><Spinner /> {sent === 'reboot' ? 'Rebooting. The console will reconnect when the host is back.' : 'Shutting down. Power it back on by hand.'}</div>
) : (
<>
<label className="muted">Type <b>{label}</b> to confirm
<input autoFocus value={typed} onChange={(e) => setTyped(e.target.value)} placeholder={label} />
</label>
{err && <div className="error">{err}</div>}
<div className="svc-actions">
<Btn className="btn on" disabled={!ok} onClick={() => go('reboot')}>Reboot</Btn>
<Btn className="btn danger" disabled={!ok} onClick={() => go('poweroff')}>Shut down</Btn>
<span className="spacer" />
<button className="btn" onClick={() => setOpen(false)}>Cancel</button>
</div>
</>
)}
</div>
</div>
)}
</>
)
}