PLAN: the KVM tab shipped

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-24 16:43:44 -07:00
co-authored by Claude Opus 5
parent 823c72122c
commit 70fcf86283
+28 -33
View File
@@ -320,6 +320,34 @@ something long in one, switch away and back, refresh — it's all still there.
snapshot, the package is purged and the old cron line is gone, with the 515 snapshots it
had taken left untouched and fully readable here — which is what adopting its naming
bought. See `hamlet/docs/hosts.md`.
- **KVM tab** — **shipped 2026-09-24** (`internal/api/kvm.go`, `KVM.tsx`, `config.KVM`).
Paste a device's address; its own web page appears in the tab. **Nothing in it knows
anything about any particular KVM** — the first device here is a Sipeed NanoKVM on `grid`
at `10.0.0.217`, but none of that is in the code.
**Why it proxies rather than framing the device directly.** Ham Lab is served over TLS and
a KVM usually is not, so a bare `http://…` iframe is blocked as mixed content before it
loads. Serving the device from Ham Lab's own origin (`/kvm/<id>/…`) fixes that, puts it
behind **Ham Lab's** login rather than a second one, and makes the iframe same-origin.
**The general problem, and the answer.** A device UI asks for its assets by absolute path
— `/assets/app.js`, `/api/whatever` — which no path prefix can rewrite away, and rewriting
bodies per device would not be general. So the prefix is only the entry point: any request
Ham Lab does not serve itself is routed to whichever KVM the asking page belongs to, read
from the **Referer**, falling back to a **cookie** when there is none (a WebSocket upgrade).
Ham Lab's own routes always match first, so a device can never shadow the console, and the
fallback is gated on a live session. The device never receives Ham Lab's session cookie,
and its `X-Frame-Options`/CSP/HSTS are dropped on the way out since Ham Lab is the origin
now. Redirects to the device's root are rewritten back through the prefix.
**One catch found in testing:** Ham Lab's own `frame-ancestors 'none'` forbids framing even
from the same origin, so `/kvm/` paths send `'self'` instead. Without that the iframe was
refused before it loaded.
**The keyboard** needs less than it sounds: a page cannot forward keys into an iframe and
does not need to, since one with focus receives them directly. Fullscreen plus
`navigator.keyboard.lock()` is what takes Escape, Tab and Ctrl-W back from the browser;
that is Chromium-only, and the tab says which version you are getting rather than
pretending. The list is **hub-side** on purpose — a KVM earns its keep when the machine is
down, which is when that machine's own Ham Lab cannot answer — and a KVM need not belong to
a machine Ham Lab manages at all. Not done: holding the device's own login on the hub, so
its page does not ask for a second password.
- **Skins** — 13 since 2026-09-19 (`themes.ts`: a palette is one `mk(…)` line): Dark,
Light, Cyberpunk, Retro, **HUD** (hairlines of cyan on near-black, corner ticks, thin
type), Nord, Gruvbox, Catppuccin + Latte, Dracula, Tokyo Night, Solarized Dark + Light;
@@ -585,39 +613,6 @@ something long in one, switch away and back, refresh — it's all still there.
`internal/term`); snapshots and clones; and what it does on a host with no virtualisation
at all, which is every host here today. Worth scoping as its own plan before starting.
- **A KVM tab** — asked for 2026-09-24, not started. **Dead simple is the brief**: paste one
HTTP link, the tab shows whatever UI that KVM serves, and the keyboard goes to it. **This
is KVM the keyboard-video-mouse switch, not KVM the hypervisor** — the Virtualization item
above means the other one.
- **The device**: a **Sipeed NanoKVM** attached to `grid` at **`10.0.0.217`** (MAC
`48:da:35:6f:d0:31`), web UI on port 80. The only one so far, so store a list, not one URL.
- **Framing works.** Probed 2026-09-24: it sends **no `X-Frame-Options` and no
`frame-ancestors`**, so it does not refuse to be embedded. No fallback needed.
- **Proxy it through the daemon — that is what makes the simple version possible.** The
KVM is **HTTP only**, and Ham Lab is served over TLS, so a bare `http://…` iframe is
blocked as mixed content and the "just paste a link" idea dies there. Serving it from Ham
Lab's own origin (`/api/kvm/<id>/…`, on the `proxyNode` machinery, which already carries
WebSockets — exactly what the video and input streams are) fixes three things at once:
no mixed content, the KVM sits behind **Ham Lab's** login rather than a second one
(`/api/vm/info` answers 401, so it has its own, which the hub can hold once), and the
iframe becomes **same-origin**, which matters for the keyboard below.
- **The keyboard needs less than it sounds.** A parent page *cannot* capture keys and
forward them into a **cross-origin** iframe — the browser forbids synthetic events across
origins — but it does not need to: an iframe with focus receives keystrokes directly, and
the NanoKVM's own page already handles them. So "capture and give it to the iframe" is
really: click-to-focus, keep focus there, and stop the console's own shortcuts from
eating keys while it is focused. What is genuinely worth adding is **fullscreen** plus
`navigator.keyboard.lock()`, which is what stops the *browser* swallowing Escape, Tab and
Ctrl-W so they reach the guest — note it is Chromium-only, so Firefox and iOS Safari get
the plain version. Proxying makes the iframe same-origin anyway, so real forwarding stays
available if some KVM ever needs it.
- **Where the list lives is the one real design decision.** A KVM earns its keep when the
machine is *down* — no OS, a bad kernel, a BIOS screen — which is when that host's Ham Lab
cannot answer. So it is **hub-side** and must work for machines running no Ham Lab at all.
A `kvm_url` in each node's own `config.json` would fail in exactly the case it is for.
- **Do not give it an NPM host** without deciding to: `*.bigham.dev` is a wildcard to the
public IP, so that would put BIOS-level control of `grid` on the internet behind only the
KVM's own login. Behind Ham Lab's proxy it inherits Ham Lab's auth instead.
## Open questions