Quality of life: in-app dialogs, selections in the URL, editable widgets, token dialog, gallery search, status history, stream errors that say why, tab reordering and shortcuts, log out everywhere

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-19 10:34:59 -07:00
co-authored by Claude Fable 5.1
parent 8ee2a2d293
commit d7ed4d8549
24 changed files with 361 additions and 101 deletions
+7 -7
View File
@@ -5,13 +5,13 @@ import { WIDGET_TYPES, WidgetSpec, WidgetType } from './widgets'
import { Btn, Loading, useEscape } from './ui'
// Pick a sensor from this machine's catalog (or type a pattern) and how to draw it.
export default function WidgetPicker({ onAdd, onClose }: { onAdd: (w: WidgetSpec) => void; onClose: () => void }) {
export default function WidgetPicker({ onAdd, onClose, initial }: { onAdd: (w: WidgetSpec) => void; onClose: () => void; initial?: WidgetSpec }) {
const [cat, setCat] = useState<Sensor[] | null>(null)
const [err, setErr] = useState('')
const [filter, setFilter] = useState('')
const [sensor, setSensor] = useState('')
const [type, setType] = useState<WidgetType>('stat')
const [title, setTitle] = useState('')
const [sensor, setSensor] = useState(initial?.sensor ?? '')
const [type, setType] = useState<WidgetType>(initial?.type ?? 'stat')
const [title, setTitle] = useState(initial?.title ?? '')
const [bad, setBad] = useState('')
// One bad pattern would 400 the stream every widget on the page shares, so ask the API first.
const add = async () => {
@@ -19,7 +19,7 @@ export default function WidgetPicker({ onAdd, onClose }: { onAdd: (w: WidgetSpec
const r = await fetch(apiPath('/api/sensors/query'), { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ w: sensor }) })
if (!r.ok) { setBad((await r.json().catch(() => ({}))).error ?? `the API refused it (${r.status})`); return }
} catch { setBad('could not reach the API'); return }
onAdd({ id: 'w' + Date.now().toString(36), type: pattern ? 'list' : type === 'list' ? 'stat' : type, sensor, title: title.trim() || undefined })
onAdd({ id: initial?.id ?? 'w' + Date.now().toString(36), type: pattern ? 'list' : type === 'list' ? 'stat' : type, sensor, title: title.trim() || undefined })
}
useEffect(() => { fetchCatalog().then(setCat).catch((e) => setErr(e instanceof Error ? e.message : 'failed')) }, [])
const shown = useMemo(() => {
@@ -32,7 +32,7 @@ export default function WidgetPicker({ onAdd, onClose }: { onAdd: (w: WidgetSpec
return (
<div className="modal-back" onClick={onClose}>
<div className="modal picker" onClick={(e) => e.stopPropagation()}>
<h3>Add a widget</h3>
<h3>{initial ? 'Edit widget' : 'Add a widget'}</h3>
<input autoFocus value={filter} onChange={(e) => setFilter(e.target.value)} placeholder="filter sensors — cpu, temp, fan, enp3s0…" />
{err && <div className="error">{err}</div>}
{!cat && !err && <Loading label="Reading the catalog…" />}
@@ -57,7 +57,7 @@ export default function WidgetPicker({ onAdd, onClose }: { onAdd: (w: WidgetSpec
</label>
{bad && <div className="error">{bad}</div>}
<div className="svc-actions">
<Btn className="btn on" disabled={!sensor} onClick={add}>Add</Btn>
<Btn className="btn on" disabled={!sensor} onClick={add}>{initial ? 'Save' : 'Add'}</Btn>
<span className="spacer" />
<button className="btn" onClick={onClose}>Cancel</button>
</div>