A machine is titled by its own hostname, and follows a rename

The name a machine was added under stays its identity — the URL, its accent, what the hub's
API calls it — and is what the Machines page shows as 'Added as'. The hostname is read live
rather than at start-up, so renaming a machine reaches the console without a restart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-21 17:04:17 -07:00
co-authored by Claude Opus 5
parent ec94cb8bff
commit dbfe163a75
8 changed files with 66 additions and 31 deletions
+8 -6
View File
@@ -10,7 +10,9 @@ const patience = (s: number) => (s === 0 ? 'at once' : s < 120 ? `for ${s} s` :
const show = (v: unknown) => (typeof v === 'number' ? String(Math.round(v * 10) / 10) : String(v))
const parse = (raw: string): number | boolean | string => (raw === 'true' ? true : raw === 'false' ? false : raw.trim() !== '' && !isNaN(Number(raw)) ? Number(raw) : raw)
function RuleDialog({ rule, machines, onSave, onClose }: { rule: AlertRule; machines: string[]; onSave: (r: AlertRule) => void; onClose: () => void }) {
// A rule names the machines it watches by the name the hub knows them by; on screen each is
// titled by its own hostname, like everywhere else.
function RuleDialog({ rule, machines, title, onSave, onClose }: { rule: AlertRule; machines: string[]; title: (n: string) => string; onSave: (r: AlertRule) => void; onClose: () => void }) {
const [r, setR] = useState({ ...rule, value: String(rule.value) })
const [hits, setHits] = useState<string>('')
useEscape(onClose)
@@ -47,7 +49,7 @@ function RuleDialog({ rule, machines, onSave, onClose }: { rule: AlertRule; mach
<div className="chips">{(['info', 'warn', 'crit'] as const).map((s) => <button type="button" key={s} className={'chip' + (r.severity === s ? ' on' : '')} onClick={() => setR({ ...r, severity: s })}>{s === 'crit' ? 'critical' : s === 'warn' ? 'warning' : 'info'}</button>)}</div>
<div className="chips">
<button type="button" className={'chip' + (all ? ' on' : '')} onClick={() => setR({ ...r, machines: '*' })}>every machine</button>
{machines.map((m) => <button type="button" key={m} className={'chip' + (picked.includes(m) ? ' on' : '')} onClick={() => { const next = picked.includes(m) ? picked.filter((x) => x !== m) : [...picked, m]; setR({ ...r, machines: next.length ? next.join(',') : '*' }) }}>{m}</button>)}
{machines.map((m) => <button type="button" key={m} className={'chip' + (picked.includes(m) ? ' on' : '')} onClick={() => { const next = picked.includes(m) ? picked.filter((x) => x !== m) : [...picked, m]; setR({ ...r, machines: next.length ? next.join(',') : '*' }) }}>{title(m)}</button>)}
</div>
<div className="svc-actions"><button type="submit" className="btn on" disabled={!r.name.trim() || !r.sensor.trim()}>Save</button><span className="spacer" /><button type="button" className="btn" onClick={onClose}>Cancel</button></div>
</form>
@@ -55,7 +57,7 @@ function RuleDialog({ rule, machines, onSave, onClose }: { rule: AlertRule; mach
)
}
export default function Alerts({ machines }: { machines: string[] }) {
export default function Alerts({ machines, titleOf: title }: { machines: string[]; titleOf: (n: string) => string }) {
const [a, setA] = useState<AlertsInfo | null>(null)
const [err, setErr] = useState('')
const [editing, setEditing] = useState<AlertRule | null>(null)
@@ -77,7 +79,7 @@ export default function Alerts({ machines }: { machines: string[] }) {
<div className="cards">
{a.firing.map((f) => (
<section className={'card firing ' + sevClass(f.severity)} key={f.rule_id + f.machine + f.sensor}>
<div className="sub-head"><span className={'sdot ' + (f.severity === 'crit' ? 'bad' : 'warn')} /><span className="dname">{f.rule}</span><span className="spacer" /><span className="badge" style={{ color: accentFor(f.machine), borderColor: accentFor(f.machine) }}>{f.machine}</span></div>
<div className="sub-head"><span className={'sdot ' + (f.severity === 'crit' ? 'bad' : 'warn')} /><span className="dname">{f.rule}</span><span className="spacer" /><span className="badge" style={{ color: accentFor(f.machine), borderColor: accentFor(f.machine) }}>{title(f.machine)}</span></div>
<div className="mono small">{f.sensor} = {show(f.value)}</div>
<div className="muted small">for {ago(f.since)}</div>
</section>
@@ -119,12 +121,12 @@ export default function Alerts({ machines }: { machines: string[] }) {
{a.events.map((e: AlertEvent, i) => (
<div className="ev" key={i}>
<span className={e.kind === 'firing' ? (e.severity === 'crit' ? 'warn-text bad-text' : 'warn-text') : 'good'}>{e.kind === 'firing' ? '▲' : '✓'}</span>
<span className="ev-main">{e.rule} <span className="muted">· {e.machine} · </span><span className="mono small muted">{e.sensor} = {show(e.value)}</span></span>
<span className="ev-main">{e.rule} <span className="muted">· {title(e.machine)} · </span><span className="mono small muted">{e.sensor} = {show(e.value)}</span></span>
<span className="muted small num" title={new Date(e.t).toLocaleString()}>{ago(e.t)} ago</span>
</div>
))}
</section>
{editing && <RuleDialog rule={editing} machines={machines} onSave={saveRule} onClose={() => setEditing(null)} />}
{editing && <RuleDialog rule={editing} machines={machines} title={title} onSave={saveRule} onClose={() => setEditing(null)} />}
</div>
)
}