Alerts tab, Docker tab, nine new skins including HUD, a colour for every machine
Alerts: rules over sensors, watched fleet-wide from the hub; ntfy and webhook channels; firing count on the rail. Docker: containers by stack with live use, start/stop/restart, following logs, inspect, images/volumes/networks. Skins: HUD, Nord, Gruvbox, Catppuccin (+Latte), Dracula, Tokyo Night, Solarized (+Light). Accents: per-machine colour. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
63fcba6077
commit
cb1617c2b8
+31
-5
@@ -6,6 +6,8 @@ import TermTabs from './TermTabs'
|
||||
import KeyBar from './KeyBar'
|
||||
import Dashboard from './Dashboard'
|
||||
import Gallery from './Gallery'
|
||||
import Docker from './Docker'
|
||||
import Alerts from './Alerts'
|
||||
import Services from './Services'
|
||||
import Cron from './Cron'
|
||||
import Updates from './Updates'
|
||||
@@ -18,6 +20,7 @@ import MachineSwitcher from './MachineSwitcher'
|
||||
import { MachineStatus, setMachine } from './api'
|
||||
import { LIST_FLOOR, useRefreshMs } from './prefs'
|
||||
import Power from './Power'
|
||||
import { applyAccent } from './accents'
|
||||
import { Btn, Toasts, askText, askYes, hasUnsaved, okToLeave, toast } from './ui'
|
||||
|
||||
// Inline outline icon so it matches the monochrome glyphs in the rail (emoji would be full colour).
|
||||
@@ -34,23 +37,36 @@ const ClockIcon = (
|
||||
</svg>
|
||||
)
|
||||
|
||||
const DockerIcon = (
|
||||
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M2 5.2 8 2l6 3.2v5.6L8 14l-6-3.2z" /><path d="M2 5.2 8 8.4l6-3.2M8 8.4V14" />
|
||||
</svg>
|
||||
)
|
||||
const BellIcon = (
|
||||
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M4 11V7.2a4 4 0 0 1 8 0V11l1.2 1.6H2.8z" /><path d="M6.6 14.2h2.8" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const GROUPS: Group[] = [
|
||||
{ id: 'dashboard', label: 'Monitors', icon: '◔' },
|
||||
{ id: 'procs', label: 'Processes', icon: '≡' },
|
||||
{ id: 'terminal', label: 'Terminal', icon: '>_' },
|
||||
{ id: 'services', label: 'Services', icon: '◈' },
|
||||
{ id: 'docker', label: 'Docker', icon: DockerIcon },
|
||||
{ id: 'cron', label: 'Cron', icon: ClockIcon },
|
||||
{ id: 'updates', label: 'Updates', icon: '⇪' },
|
||||
{ id: 'disks', label: 'Disks', icon: '▤' },
|
||||
{ id: 'files', label: 'Files', icon: FolderIcon },
|
||||
{ id: 'alerts', label: 'Alerts', icon: BellIcon },
|
||||
{ id: 'machines', label: 'Machines', icon: '⌂' },
|
||||
{ id: 'widgets', label: 'Widgets', icon: '▦' },
|
||||
{ id: 'settings', label: 'Settings', icon: '⚙' },
|
||||
]
|
||||
|
||||
type View = 'terminal' | 'dashboard' | 'widgets' | 'services' | 'cron' | 'updates' | 'disks' | 'files' | 'procs' | 'machines' | 'settings'
|
||||
const VIEWS: View[] = ['terminal', 'dashboard', 'widgets', 'services', 'cron', 'updates', 'disks', 'files', 'procs', 'machines', 'settings']
|
||||
const HUB_ONLY: View[] = ['machines', 'settings']
|
||||
type View = 'terminal' | 'dashboard' | 'widgets' | 'docker' | 'alerts' | 'services' | 'cron' | 'updates' | 'disks' | 'files' | 'procs' | 'machines' | 'settings'
|
||||
const VIEWS: View[] = ['terminal', 'dashboard', 'widgets', 'docker', 'alerts', 'services', 'cron', 'updates', 'disks', 'files', 'procs', 'machines', 'settings']
|
||||
const HUB_ONLY: View[] = ['machines', 'settings', 'alerts']
|
||||
|
||||
// URL hash is the source of truth for where you are: #/<machine|hub>/<view>. Back and
|
||||
// forward work, reload restores the tab, and links can point at a page on a machine.
|
||||
@@ -106,6 +122,14 @@ export default function Shell({ me: hubMe, onLoggedOut }: { me: Me; onLoggedOut:
|
||||
}
|
||||
|
||||
// keep the hash in step, and follow it when the browser navigates
|
||||
const [firing, setFiring] = useState(0)
|
||||
useEffect(() => {
|
||||
const tick = () => { if (document.visibilityState === 'visible') api.alerts().then((a) => setFiring(a.firing.length)).catch(() => {}) }
|
||||
tick(); const t = setInterval(tick, 15000); window.addEventListener('hamlab:alerts', tick)
|
||||
return () => { clearInterval(t); window.removeEventListener('hamlab:alerts', tick) }
|
||||
}, [])
|
||||
// The machine's colour: set on <html>, so the header stripe and the switcher pick it up.
|
||||
useEffect(() => { const paint = () => applyAccent(machine ?? hubMe.host); paint(); window.addEventListener('hamlab:accents', paint); return () => window.removeEventListener('hamlab:accents', paint) }, [machine, hubMe.host])
|
||||
// The tab's title says where you are, so history entries and open tabs can be told apart.
|
||||
useEffect(() => { document.title = `${GROUPS.find((g) => g.id === view)?.label ?? view} · ${machine ?? hubMe.host} — Ham Lab` }, [view, machine, hubMe.host])
|
||||
useEffect(() => {
|
||||
@@ -216,7 +240,7 @@ export default function Shell({ me: hubMe, onLoggedOut }: { me: Me; onLoggedOut:
|
||||
</header>
|
||||
|
||||
<div className="body">
|
||||
<Rail groups={GROUPS} view={view}
|
||||
<Rail groups={GROUPS} view={view} badges={{ alerts: firing }}
|
||||
header={<MachineSwitcher hub={hubMe.host} hubAddr={hubMe.addr} current={machine} machines={machines} onChange={switchMachine} onManage={() => { setView('machines'); setDrawer(false) }} />}
|
||||
onView={async (id) => { if (id !== view && !(await okToLeave())) return; setView(id as View); setDrawer(false) }} />
|
||||
<div className="backdrop" onClick={() => setDrawer(false)} />
|
||||
@@ -226,6 +250,8 @@ export default function Shell({ me: hubMe, onLoggedOut }: { me: Me; onLoggedOut:
|
||||
{view === 'machines' && <Machines hub={hubMe} onChanged={loadMachines} onOpen={(n) => { switchMachine(n); setView('dashboard') }} />}
|
||||
{view === 'dashboard' && <Dashboard />}
|
||||
{view === 'widgets' && <Gallery demoUrl={hubMe.demo_url} />}
|
||||
{view === 'docker' && <Docker />}
|
||||
{view === 'alerts' && <Alerts machines={machines.map((m) => m.name)} />}
|
||||
{view === 'procs' && <Procs />}
|
||||
{view === 'services' && <Services />}
|
||||
{view === 'cron' && <Cron />}
|
||||
@@ -257,7 +283,7 @@ export default function Shell({ me: hubMe, onLoggedOut }: { me: Me; onLoggedOut:
|
||||
<span>{machine ? `${machine} · ` : ''}{me.version.startsWith('v') ? me.version : 'v' + me.version}</span>
|
||||
<span>up {fmtUptime(me.uptime)}</span>
|
||||
<span className="spacer" />
|
||||
<span className="muted">{view === 'dashboard' ? 'Monitors' : view === 'widgets' ? 'Widgets' : view === 'services' ? 'Services' : view === 'cron' ? 'Cron' : view === 'updates' ? 'Updates' : view === 'disks' ? 'Disks' : view === 'files' ? 'Files' : view === 'procs' ? 'Processes' : view === 'machines' ? 'Machines' : view === 'settings' ? 'Settings' : activeTerm ? activeTerm.name : ''}</span>
|
||||
<span className="muted">{view === 'dashboard' ? 'Monitors' : view === 'widgets' ? 'Widgets' : view === 'docker' ? 'Docker' : view === 'alerts' ? 'Alerts' : view === 'services' ? 'Services' : view === 'cron' ? 'Cron' : view === 'updates' ? 'Updates' : view === 'disks' ? 'Disks' : view === 'files' ? 'Files' : view === 'procs' ? 'Processes' : view === 'machines' ? 'Machines' : view === 'settings' ? 'Settings' : activeTerm ? activeTerm.name : ''}</span>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user