Dashboard tab: CPU/mem/load, hwmon temps+fans, net, disks, filesystems, UPS with 10-min sparklines

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-17 13:37:39 -07:00
co-authored by Claude Fable 5.1
parent 24d64245ec
commit 3c6f4d4639
10 changed files with 982 additions and 34 deletions
+27 -20
View File
@@ -1,43 +1,50 @@
import { useState } from 'react'
import { Term } from './api'
export interface Group { id: string; label: string; icon: string; soon?: boolean }
// A group is either a page (clicking the head opens it — Dashboard) or a folder of
// leaves (Terminal). `soon` groups are inert placeholders for the roadmap.
export interface Group { id: string; label: string; icon: string; soon?: boolean; page?: boolean }
interface Props {
groups: Group[]; terms: Term[]; active: string | null
onSelect: (id: string) => void; onNew: () => void
groups: Group[]; terms: Term[]; active: string | null; view: string
onSelect: (id: string) => void; onView: (id: string) => void; onNew: () => void
onRename: (t: Term) => void; onClose: (t: Term) => void
}
export default function Rail({ groups, terms, active, onSelect, onNew, onRename, onClose }: Props) {
export default function Rail({ groups, terms, active, view, onSelect, onView, onNew, onRename, onClose }: Props) {
const [open, setOpen] = useState<string>('terminal')
return (
<nav className="rail">
{groups.map((g) => {
const expanded = open === g.id
const expanded = !g.page && open === g.id
const isPage = !!g.page && view === g.id
return (
<div key={g.id} className={'group' + (expanded ? ' open' : '') + (g.soon ? ' soon' : '')}>
<button className="group-head" onClick={() => !g.soon && setOpen(expanded ? '' : g.id)} disabled={g.soon}>
<span className="chev">{expanded ? '▾' : '▸'}</span>
<button className={'group-head' + (isPage ? ' active' : '')} disabled={g.soon}
onClick={() => { if (g.soon) return; if (g.page) onView(g.id); else setOpen(expanded ? '' : g.id) }}>
<span className="chev">{g.page ? '' : expanded ? '▾' : '▸'}</span>
<span className="gicon">{g.icon}</span>
<span>{g.label}</span>
{g.soon && <span className="badge">soon</span>}
</button>
{expanded && g.id === 'terminal' && (
<div className="leaves">
{terms.map((t) => (
<div key={t.id} className={'leaf' + (t.id === active ? ' active' : '') + (t.running ? '' : ' exited')}
onClick={() => onSelect(t.id)}>
<span className={'sdot' + (t.running ? ' run' : '')} />
<span className="lname" title={t.name}>{t.name}</span>
{t.id === active && (
<span className="leaf-actions">
<button className="icon-btn" title="rename" onClick={(e) => { e.stopPropagation(); onRename(t) }}>✎</button>
<button className="icon-btn" title="close" onClick={(e) => { e.stopPropagation(); onClose(t) }}>✕</button>
</span>
)}
</div>
))}
{terms.map((t) => {
const sel = view === 'terminal' && t.id === active
return (
<div key={t.id} className={'leaf' + (sel ? ' active' : '') + (t.running ? '' : ' exited')}
onClick={() => onSelect(t.id)}>
<span className={'sdot' + (t.running ? ' run' : '')} />
<span className="lname" title={t.name}>{t.name}</span>
{sel && (
<span className="leaf-actions">
<button className="icon-btn" title="rename" onClick={(e) => { e.stopPropagation(); onRename(t) }}>✎</button>
<button className="icon-btn" title="close" onClick={(e) => { e.stopPropagation(); onClose(t) }}>✕</button>
</span>
)}
</div>
)
})}
<button className="leaf new" onClick={onNew}>+ New Terminal</button>
</div>
)}