diff --git a/PLAN.md b/PLAN.md index f80bd03..c57308c 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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 ` — 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 diff --git a/web/src/Docker.tsx b/web/src/Docker.tsx index b2f249a..0acd191 100644 --- a/web/src/Docker.tsx +++ b/web/src/Docker.tsx @@ -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(null) const [file, setFile] = useState('') @@ -502,8 +501,6 @@ function Updates({ upd, onChecked }: { upd: MachineUpdates | null; onChecked: (u )} - {onHub && } -

Schedules

setEditing({ cron: '0 3 * * *', apply: true, enabled: true })}>+ New schedule
@@ -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
- if (rows.length === 0) return null - return ( - <> -

The other machines

-
- {rows.map((r) => { - const behind = (r.upd?.images ?? []).filter((i) => i.state === 'update') - return ( -
- {r.name} - - {!r.online ? offline - : r.err ? {r.err} - : !r.upd?.checked ? not checked - : behind.length === 0 ? all current - : i.image).join('\n')}>{behind.length} behind} - - {r.online && !r.err && { setBusy(r.name); try { await load(r.name) } finally { setBusy('') } }}>check} -
- ) - })} -
-
Open a machine from the switcher to schedule anything on it — a schedule lives on the machine that runs it.
- - ) -} - function JobDialog({ j, stacks, onClose, onSaved }: { j: Partial; stacks: string[]; onClose: () => void; onSaved: () => void }) { const [f, setF] = useState>(j) const [svc, setSvc] = useState((j.services ?? []).join(' ')) diff --git a/web/src/api.ts b/web/src/api.ts index 1b750af..1855078 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -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>('GET', `/api/docker/containers/${encodeURIComponent(id)}/inspect`), machineUpdates: (refresh = false) => req('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('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) => req('PUT', '/api/docker/schedules', j), deleteUpdateJob: (id: string) => req('DELETE', `/api/docker/schedules/${encodeURIComponent(id)}`), diff --git a/web/src/styles.css b/web/src/styles.css index 5e1450a..868be28 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -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; }