commit 190604247f826a1be1fc48bd1851c79452f49561 Author: Mitchell-Johnson Date: Thu Sep 17 09:52:19 2026 -0700 Initial plan: MVP scope, decisions, roadmap Co-Authored-By: Claude Fable 5.1 diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..2694c65 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,151 @@ +# Hamlet — MVP plan + +Proxmox-style web console for a bare-metal Debian 13 host. First target: `core` +(10.0.0.234). MVP is **service + shell + persistent terminals**; everything else is a +stub tab or a roadmap item. + +## Decisions + +| Area | Decision | Why | +|---|---|---| +| Backend | **Go 1.24**, single static binary, frontend embedded via `embed` | `core` has ~370 MiB free RAM; a Go daemon idles at ~15 MiB vs ~80+ for Node. One file to deploy, no nvm path in the unit. Same binary later installs on `grid` unchanged. | +| PTY | `creack/pty` — server-owned PTYs running `core`'s login shell | Sessions live in the daemon, not the browser, so refresh/tab-switch reattaches. No tmux dependency (not installed on `core`). | +| Transport | One WebSocket per attached terminal; REST for everything else | Standard; NPM passes WebSockets with one toggle. | +| Frontend | **React + TypeScript + Vite**, `xterm.js` (+ fit, webgl, web-links addons) | Matches `calorie-counter` / `wedding-website`, so no new toolchain to learn. | +| Auth | Single-user **password login**, argon2id hash, HttpOnly/Secure/SameSite=Strict session cookie, login rate-limit, Origin check on WS upgrade | This is a root-capable shell on the public internet. Auth is MVP, not roadmap. | +| Service | `hamlet.service`, `User=core`, `Restart=always`, binary at `/usr/local/bin/hamlet` | Same pattern as `calorie-counter.service`. Runs as `core` so `sudo` works in the terminal without a password, as on the SSH path. | +| Listen | `10.0.0.234:8100` (free) | NPM is a container, so loopback-only won't do. Firewalling to NPM is a later hardening step. | +| Data | `/home/core/apps/hamlet/` — `config.yaml`, `state.db` (SQLite) | `apps/` is the nightly rsync target to `grid`, so it's backed up for free. | +| Ingress | NPM proxy host `hamlet.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/hamlet`, private, `main` | Created 2026-09-17. | + +## Layout + +``` +┌──────────────────────────────────────────────────────────────┐ +│ header: Hamlet · core · load 0.42 · ● connected · [logout] │ +├──────────────┬───────────────────────────────────────────────┤ +│ ▾ Terminal │ │ +│ shell 1 ● │ │ +│ backups │ main content (xterm) │ +│ + New │ │ +│ ▸ Services │ │ +│ ▸ Cron │ │ +│ ▸ ZFS │ │ +│ ▸ Dashboard │ │ +├──────────────┴───────────────────────────────────────────────┤ +│ footer: v0.1.0 · up 12d · hamlet.bigham.dev │ +└──────────────────────────────────────────────────────────────┘ +``` + +- Left rail: collapsible groups, one expanded at a time. Active leaf highlighted. +- Terminal group: one leaf per session, `● ` when a process is running, "+ New + Terminal" pinned at the bottom. Right-click / long-press → rename, close. +- Non-MVP groups render as disabled leaves with "coming soon" — the rail shape is + the product; stubs keep it honest about scope. +- **Mobile**: rail becomes a slide-in drawer; xterm fits the viewport; a sticky key + bar above the keyboard (`Esc Tab Ctrl Alt ↑ ↓ ← → |`) because phone keyboards + have none of those. This is what makes it usable as a PWA rather than a novelty. + +## Terminal semantics + +- A session is a PTY + shell owned by the daemon, with an ID, name, cwd hint, and a + **scrollback ring buffer** (256 KiB). Attaching replays the buffer, then streams. +- Any number of browser tabs can attach to the same session (all get output; all can + type). Resize follows the most recent attacher. +- Switching tabs in the UI just detaches/attaches; the shell keeps running. +- Closing a session sends `SIGHUP` to the process group and drops it. Shell exit + closes the session automatically (leaf shows "exited" until dismissed). +- Session names persist in `state.db` so the sidebar order survives a service + restart. **The shells themselves do not survive a daemon restart** in the MVP — + see roadmap (tmux backing). +- Idle sessions cost one shell process each; no automatic reaping in MVP. + +## API (MVP) + +``` +POST /api/login {password} → 204 + cookie +POST /api/logout → 204 +GET /api/me → {host, user, version, uptime, load} +GET /api/terminals → [{id, name, running, created}] +POST /api/terminals {name?} → {id, ...} +PATCH /api/terminals/:id {name} → 204 +DELETE /api/terminals/:id → 204 +WS /api/terminals/:id/attach binary frames = raw bytes; JSON text frames = {resize} +GET /healthz → 200 (unauthenticated, for NPM) +``` + +Everything under `/api` except `login` and `/healthz` requires the session cookie. + +## Project layout + +``` +hamlet/ + cmd/hamlet/main.go serve | passwd | version + internal/ + auth/ argon2id, sessions, rate limit + term/ PTY manager, ring buffer, attach/detach + api/ chi router, handlers, WS + state/ SQLite (modernc, no cgo) + web/ go:embed of web/dist + web/ Vite + React + TS + xterm.js, PWA manifest + SW + deploy/ + hamlet.service + install.sh build → /usr/local/bin/hamlet → systemctl restart + Makefile build / dev / deploy / lint / test +``` + +## Milestones + +**M0 — skeleton** (service boots, login works) +- Go module, chi, config loading, `hamlet passwd`, argon2id login, session cookie. +- `hamlet.service` on `core`, `:8100`, serves a placeholder page. +- `make deploy` from `core` itself. + +**M1 — terminals** (the MVP feature) +- PTY manager with ring buffer, create/list/rename/close, WS attach with resize. +- React shell: header, rail, footer, Terminal group, xterm main pane. +- Persistence of session list across refresh and rail switching. + +**M2 — polish + PWA + ingress** +- Mobile drawer, key bar, fit-on-rotate. +- `manifest.webmanifest`, icons (Ham logo on black, same recipe as HamTV), service + worker caching the app shell only (never `/api`). +- NPM proxy host for `hamlet.bigham.dev`; verify WS through NPM; force-SSL/HSTS/HTTP2. +- Stub groups for Services / Cron / ZFS / Dashboard. + +**Done when:** from the phone, open the PWA, log in, open two terminals, run +something long in one, switch away and back, refresh — it's all still there. + +## Roadmap (not MVP — notes only) + +- **tmux-backed sessions** so shells survive a daemon restart / redeploy. Would spawn + `tmux new -A -s hamlet-`; needs `mouse on` and status-bar handling in xterm. +- **Services tab** — `systemctl list-units`, per-unit page: status, last 200 journal + lines (live-tail via WS), start/stop/restart/enable buttons, unit file viewer. +- **Cron tab** — parse `/etc/crontab`, `/etc/cron.d/*`, user crontabs, and systemd + timers into one table with next-run times; edit via `crontab -` round-trip. +- **ZFS tab** — pools, datasets (`used/avail/refer/usedbysnapshots`), snapshot list, + scrub status, and the `freeing` counter. `grid` is the target, which implies + multi-host. +- **Dashboard** — CPU/mem/load/net, disks and SMART (`smartctl -j`), temps and fans + (`sensors -j`), UPS from `pwrstat`. Hamlet collects on a timer into a SQLite + ring (e.g. 10 s resolution, 7 days) and draws sparklines. This is where the + "collects its own data" requirement lands; no Prometheus dependency. +- **Multi-host** — same binary as a headless *agent* on `grid` / Mac mini / + `wyse-5070-1`, `core`'s instance is the UI and fans out over mTLS. The rail grows a + host level at the top, exactly like Proxmox's datacenter → node tree. Design the + MVP URL space so `host` can be inserted later without breaking clients. +- **Auth hardening** — WebAuthn passkeys (phone Face ID), TOTP fallback, session + list + revoke, optional NPM access list restricting to WireGuard/LAN. +- **Files** — browse/upload/download/edit under `/`, with a sudo toggle. +- **Docker read-only view** — `docker ps`/logs; dockhand stays the source of truth. +- **Notifications** — push on service failure / SMART warning / UPS event. +- **Terminal extras** — search, copy-mode, font/theme, split panes, upload via + drag-drop into cwd. + +## Open questions + +- Idle-session reaping policy (never / after N days) — leaning never; it's a + single-user box. +- Whether `sudo -i`-style root sessions get their own leaf colour. Cosmetic. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b7faadb --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# Hamlet + +Web-based Linux management console for bare-metal Debian, in the style of the Proxmox +web UI. Named after Ham the cat (see also HamTV), and the play. + +Status: **planning**. See [`PLAN.md`](PLAN.md) for the MVP scope and roadmap. No code yet. + +- Runs as a systemd service on the host it manages, as user `core`. +- Public at `https://hamlet.bigham.dev` (behind Nginx Proxy Manager on `core`). +- Installable as a PWA.