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
+44
View File
@@ -0,0 +1,44 @@
import { useEffect, useRef, useState } from 'react'
import { api, TaskInfo, wsURLPath } from './api'
// Follows one background task: backlog, then live chunks over a WebSocket, until it exits.
export default function TaskOutput({ id, onDone }: { id: string; onDone?: (t: TaskInfo) => void }) {
const [task, setTask] = useState<TaskInfo | null>(null)
const [text, setText] = useState('')
const box = useRef<HTMLPreElement>(null)
const doneRef = useRef(onDone)
doneRef.current = onDone
useEffect(() => {
setText(''); setTask(null)
let closed = false
api.task(id).then(setTask).catch(() => { /* shown as unknown */ })
const s = new WebSocket(wsURLPath(`/api/tasks/${id}/stream`))
s.binaryType = 'arraybuffer'
const dec = new TextDecoder()
s.onmessage = (ev) => {
if (ev.data instanceof ArrayBuffer) { setText((t) => t + dec.decode(ev.data, { stream: true })); return }
try {
const m = JSON.parse(ev.data)
if (m.type === 'exit') api.task(id).then((t) => { setTask(t); doneRef.current?.(t) }).catch(() => { /* ignore */ })
} catch { /* ignore */ }
}
return () => { closed = true; s.close(); void closed }
}, [id])
useEffect(() => { if (box.current) box.current.scrollTop = box.current.scrollHeight }, [text])
const status = !task ? '' : task.running ? 'running…' : task.error ? `failed: ${task.error}` : task.exit_code === 0 ? 'done' : `exited ${task.exit_code}`
return (
<div className="task">
<div className="logs-bar">
<span className={'dot ' + (task?.running ? 'warn' : task && (task.exit_code !== 0 || task.error) ? 'bad' : 'ok')} />
<span>{task?.title ?? '…'}</span>
<span className="muted">· {status}</span>
<span className="spacer" />
{task?.running && <button className="btn" onClick={() => api.cancelTask(id)}>Cancel</button>}
</div>
<pre className="run-out task-out" ref={box}>{text || (task?.running ? '' : '(no output)')}</pre>
</div>
)
}