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
+18
-2
@@ -2,6 +2,7 @@ import { FormEvent, useCallback, useEffect, useState } from 'react'
|
||||
import { api, MachineStatus, Me } from './api'
|
||||
import { Btn, Loading, askYes, toast } from './ui'
|
||||
import { LIST_FLOOR, useRefreshMs } from './prefs'
|
||||
import { ACCENTS, accentFor, getAccents, setAccents } from './accents'
|
||||
|
||||
const fmtUptime = (s: number) => { const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60); return d > 0 ? `${d}d ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m` }
|
||||
const ago = (iso?: string) => { if (!iso) return 'never'; const s = (Date.now() - Date.parse(iso)) / 1000; return s < 90 ? 'just now' : s < 3600 ? `${Math.round(s / 60)}m ago` : s < 86400 ? `${Math.round(s / 3600)}h ago` : `${Math.round(s / 86400)}d ago` }
|
||||
@@ -67,8 +68,9 @@ export default function Machines({ hub, onChanged, onOpen }: { hub: Me; onChange
|
||||
<div className="dash machines">
|
||||
<div className="dash-bar"><h2 className="section">This host</h2></div>
|
||||
<section className="card">
|
||||
<div className="sub-head"><span className="dot ok" /><span className="dname">{hub.host}</span><span className="badge">hub</span><span className="spacer" /><span className="num muted">v{hub.version.replace(/^v/, '')} · up {fmtUptime(hub.uptime)}</span></div>
|
||||
<div className="sub-head"><span className="dot ok" /><span className="dname" style={{ color: accentFor(hub.host) }}>{hub.host}</span><span className="badge">hub</span><span className="spacer" /><span className="num muted">v{hub.version.replace(/^v/, '')} · up {fmtUptime(hub.uptime)}</span></div>
|
||||
<div className="muted small">{fp ? <>Certificate <span className="mono">{fp}</span></> : 'Serving plain HTTP (behind a reverse proxy). Set "tls": "auto" in config.json for a self-signed certificate if another hub should manage this host.'}</div>
|
||||
<Swatches name={hub.host} />
|
||||
</section>
|
||||
|
||||
<div className="dash-bar"><h2 className="section">Managed machines · {list.length}</h2><span className="spacer" />{list.some(outdated) && <Btn className="btn on" onClick={updateAll}>Update {list.filter(outdated).length} outdated</Btn>}</div>
|
||||
@@ -79,7 +81,7 @@ export default function Machines({ hub, onChanged, onOpen }: { hub: Me; onChange
|
||||
<section className="card" key={m.name}>
|
||||
<div className="sub-head">
|
||||
<span className={'dot ' + (m.online ? 'ok' : 'bad')} />
|
||||
<span className="dname">{m.name}</span>
|
||||
<span className="dname" style={{ color: accentFor(m.name) }}>{m.name}</span>
|
||||
<span className="muted small mono">{m.url}</span>
|
||||
<span className="spacer" />
|
||||
{m.reboot_needed && <span className="badge" title="reboot required">⟳</span>}
|
||||
@@ -93,6 +95,7 @@ export default function Machines({ hub, onChanged, onOpen }: { hub: Me; onChange
|
||||
{m.fingerprint && <div><span className="k">Cert</span><span className="mono" title={m.fingerprint}>{shortFp(m.fingerprint)}</span></div>}
|
||||
<div><span className="k">Added</span><span>{new Date(m.added).toLocaleDateString()}</span></div>
|
||||
</div>
|
||||
<Swatches name={m.name} />
|
||||
<div className="svc-actions">
|
||||
<button className="btn on" disabled={!m.online} onClick={() => onOpen(m.name)}>Open</button>
|
||||
{outdated(m) && <Btn className="btn" onClick={() => update(m)}>Update</Btn>}
|
||||
@@ -116,3 +119,16 @@ export default function Machines({ hub, onChanged, onOpen }: { hub: Me; onChange
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// The colour a machine wears in the header stripe and the switcher: ten to tap, or any other.
|
||||
export function Swatches({ name }: { name: string }) {
|
||||
const [, bump] = useState(0)
|
||||
const cur = accentFor(name)
|
||||
const pick = (c: string) => { setAccents({ ...getAccents(), [name]: c }); bump((n) => n + 1) }
|
||||
return (
|
||||
<div className="swatches" title={`${name}'s colour — it marks the header whenever you are on this machine`}>
|
||||
{ACCENTS.map((c) => <button key={c} className={'swatch' + (c.toLowerCase() === cur.toLowerCase() ? ' on' : '')} style={{ background: c }} aria-label={c} onClick={() => pick(c)} />)}
|
||||
<input type="color" value={cur} onChange={(e) => pick(e.target.value)} aria-label="another colour" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user