docker: drop the fleet-wide updates list
A view shows one machine: the one in the switcher. Seeing another machine's state means switching to it, not a hub-side page that reaches across — and this was one. Recorded as a decision so it does not get rebuilt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
608f0b7005
commit
b8c654b61b
@@ -18,6 +18,7 @@ stub tab or a roadmap item.
|
||||
| Data | `/home/core/apps/hamlab/` — `config.yaml`, `state.db` (SQLite) | `apps/` is the nightly rsync target to `grid`, so it's backed up for free. |
|
||||
| Ingress | NPM proxy host `hamlab.bigham.dev` → `:8100`, LE cert, WebSockets on, caching off, force-SSL + HSTS + HTTP/2 set in a **second** API call (NPM drops them on create-with-new-cert) | `*.bigham.dev` already resolves; the proxy host alone makes it live. |
|
||||
| Repo | Gitea `mitchell/hamlab`, private, `main` | Created 2026-09-17. |
|
||||
| Machines | **A view shows one machine: the one in the switcher.** No hub-side page lists or aggregates what its nodes are doing. To see or change anything on another machine, switch to it. | Mitchell, 2026-09-25, cutting a fleet-wide updates list out of the Docker tab. The switcher is the only place a machine is chosen, so there is exactly one answer to "which machine am I looking at" — a page that quietly reaches across breaks that, and it is never obvious which machine an action would land on. The hub still *proxies* for a node, and hub-only admin (Machines, Users, Settings, Alerts) is unaffected. |
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -580,8 +581,8 @@ something long in one, switch away and back, refresh — it's all still there.
|
||||
keeps its own 4000-entry ring, since `docker events` forgets after about a minute),
|
||||
**Shell** (`docker exec`, handed to the Terminal tab) and **Limits** (`docker update` for
|
||||
memory, CPU and restart policy); an **updates** tab giving the whole machine at once with
|
||||
a per-container indicator in the list; the same across every machine, asked of the hub;
|
||||
and **schedules**, which write `/etc/cron.d/hamlab-docker` and call
|
||||
a per-container indicator in the list; and **schedules**, which write
|
||||
`/etc/cron.d/hamlab-docker` and call
|
||||
`hamlab docker-update <id>` — the shape the ZFS schedules already use.
|
||||
Also done: Ham Lab keeps working when `dockhand` is removed. It remembers where a
|
||||
container's bind mounts pointed (`mounts` in `state.json`) and resolves a stack through
|
||||
|
||||
+1
-49
@@ -2,7 +2,7 @@
|
||||
// logs that follow, and inspect — then images, volumes and networks. Live CPU and memory
|
||||
// come from the same container sensors everything else uses.
|
||||
import { FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { api, apiPath, DockerContainer, DockerEvent, DockerHealth, DockerInfo, ImageCheck, MachineStatus, MachineUpdates, Stack, StackDiff, UpdateJob, UpdateJobRow } from './api'
|
||||
import { api, DockerContainer, DockerEvent, DockerHealth, DockerInfo, ImageCheck, MachineUpdates, Stack, StackDiff, UpdateJob, UpdateJobRow } from './api'
|
||||
import TaskOutput from './TaskOutput'
|
||||
import StackNew, { DupFrom } from './StackNew'
|
||||
import { Sensor, fetchHistories, fmtValue, useSensorStream } from './sensors'
|
||||
@@ -456,7 +456,6 @@ function UpdateDot({ state }: { state?: string }) {
|
||||
|
||||
// What is out of date here, and the schedules that keep it from happening again.
|
||||
function Updates({ upd, onChecked }: { upd: MachineUpdates | null; onChecked: (u: MachineUpdates) => void }) {
|
||||
const onHub = apiPath('/api/x') === '/api/x'
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [jobs, setJobs] = useState<UpdateJobRow[] | null>(null)
|
||||
const [file, setFile] = useState('')
|
||||
@@ -502,8 +501,6 @@ function Updates({ upd, onChecked }: { upd: MachineUpdates | null; onChecked: (u
|
||||
</section>
|
||||
)}
|
||||
|
||||
{onHub && <Fleet />}
|
||||
|
||||
<div className="dash-bar" style={{ marginTop: 14 }}><h2 className="section">Schedules</h2><span className="spacer" />
|
||||
<Btn className="btn on" onClick={() => setEditing({ cron: '0 3 * * *', apply: true, enabled: true })}>+ New schedule</Btn>
|
||||
</div>
|
||||
@@ -541,51 +538,6 @@ function Updates({ upd, onChecked }: { upd: MachineUpdates | null; onChecked: (u
|
||||
)
|
||||
}
|
||||
|
||||
// Every other machine at once. Only shown on the hub, because only the hub can reach them,
|
||||
// and each answer is that machine's own six-hour cache — nothing is checked here without
|
||||
// being asked for.
|
||||
function Fleet() {
|
||||
const [rows, setRows] = useState<{ name: string; online: boolean; upd?: MachineUpdates; err?: string }[] | null>(null)
|
||||
const [busy, setBusy] = useState('')
|
||||
const load = useCallback(async (refresh?: string) => {
|
||||
let nodes: MachineStatus[] = []
|
||||
try { nodes = (await api.machines()).nodes } catch { setRows([]); return }
|
||||
const out = await Promise.all(nodes.map(async (n) => {
|
||||
if (!n.online) return { name: n.name, online: false }
|
||||
try { return { name: n.name, online: true, upd: await api.nodeUpdates(n.name, refresh === n.name) } }
|
||||
catch (e) { return { name: n.name, online: true, err: e instanceof Error ? e.message : 'failed' } }
|
||||
}))
|
||||
setRows(out)
|
||||
}, [])
|
||||
useEffect(() => { load() }, [load])
|
||||
if (rows === null) return <div style={{ marginTop: 14 }}><Loading label="Asking the other machines…" /></div>
|
||||
if (rows.length === 0) return null
|
||||
return (
|
||||
<>
|
||||
<h2 className="section" style={{ marginTop: 14 }}>The other machines</h2>
|
||||
<section className="card">
|
||||
{rows.map((r) => {
|
||||
const behind = (r.upd?.images ?? []).filter((i) => i.state === 'update')
|
||||
return (
|
||||
<div className="row" key={r.name}>
|
||||
<span className="k">{r.name}</span>
|
||||
<span>
|
||||
{!r.online ? <span className="muted">offline</span>
|
||||
: r.err ? <span className="muted small">{r.err}</span>
|
||||
: !r.upd?.checked ? <span className="muted">not checked</span>
|
||||
: behind.length === 0 ? <span className="muted">all current</span>
|
||||
: <span className="warn-text" title={behind.map((i) => i.image).join('\n')}>{behind.length} behind</span>}
|
||||
</span>
|
||||
<span className="row-end">{r.online && !r.err && <Btn className="link" busy={busy === r.name} onClick={async () => { setBusy(r.name); try { await load(r.name) } finally { setBusy('') } }}>check</Btn>}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</section>
|
||||
<div className="muted small">Open a machine from the switcher to schedule anything on it — a schedule lives on the machine that runs it.</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function JobDialog({ j, stacks, onClose, onSaved }: { j: Partial<UpdateJob>; stacks: string[]; onClose: () => void; onSaved: () => void }) {
|
||||
const [f, setF] = useState<Partial<UpdateJob>>(j)
|
||||
const [svc, setSvc] = useState((j.services ?? []).join(' '))
|
||||
|
||||
@@ -257,8 +257,6 @@ export const api = {
|
||||
dockerLogs: (id: string, since = '') => req<{ lines: { t: string; m: string }[] }>('GET', `/api/docker/containers/${encodeURIComponent(id)}/logs${since ? `?since=${encodeURIComponent(since)}` : '?n=300'}`),
|
||||
dockerInspect: (id: string) => req<Record<string, unknown>>('GET', `/api/docker/containers/${encodeURIComponent(id)}/inspect`),
|
||||
machineUpdates: (refresh = false) => req<MachineUpdates>('GET', `/api/docker/updates${refresh ? '?refresh=1' : ''}`),
|
||||
// One named machine, asked of the hub — the fleet view, which is on the hub by definition.
|
||||
nodeUpdates: (node: string, refresh = false) => req<MachineUpdates>('GET', `/api/nodes/${encodeURIComponent(node)}/docker/updates${refresh ? '?refresh=1' : ''}`, undefined, true),
|
||||
updateJobs: () => req<{ jobs: UpdateJobRow[]; file: string }>('GET', '/api/docker/schedules'),
|
||||
saveUpdateJob: (j: Partial<UpdateJob>) => req<UpdateJob>('PUT', '/api/docker/schedules', j),
|
||||
deleteUpdateJob: (id: string) => req<void>('DELETE', `/api/docker/schedules/${encodeURIComponent(id)}`),
|
||||
|
||||
@@ -767,4 +767,3 @@ pre.diff .add { color: var(--ok); background: color-mix(in srgb, var(--ok) 12%,
|
||||
.kvm-shrink:hover, .kvm-shrink:active { opacity: 1; }
|
||||
.updot { margin-left: 6px; font-size: 11px; font-weight: 700; }
|
||||
.updot.behind { color: var(--s2); } .updot.unknown { color: var(--muted); }
|
||||
.card > .row > .row-end { text-align: right; white-space: nowrap; }
|
||||
|
||||
Reference in New Issue
Block a user