diff --git a/PLAN.md b/PLAN.md index 85b8e30..be9bc13 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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//…`) 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//…`, 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