From d76da4b37a836b7fadb8c9bd6fd7f6d7c5ca2fbd Mon Sep 17 00:00:00 2001 From: Mitchell-Johnson Date: Thu, 17 Sep 2026 10:40:40 -0700 Subject: [PATCH] Rename: Hamlet -> Ham Lab (binary/module/service/config hamlab) Co-Authored-By: Claude Fable 5.1 --- Makefile | 4 +-- PLAN.md | 32 +++++++++++------------ README.md | 6 ++--- cmd/{hamlet => hamlab}/main.go | 26 +++++++++--------- deploy/{hamlet.service => hamlab.service} | 6 ++--- deploy/install.sh | 10 +++---- go.mod | 2 +- internal/api/server.go | 8 +++--- internal/auth/auth.go | 2 +- internal/config/config.go | 2 +- internal/term/manager.go | 2 +- web/index.html | 4 +-- web/package-lock.json | 4 +-- web/package.json | 2 +- web/public/manifest.webmanifest | 4 +-- web/public/sw.js | 4 +-- web/src/Login.tsx | 2 +- web/src/Shell.tsx | 2 +- 18 files changed, 61 insertions(+), 61 deletions(-) rename cmd/{hamlet => hamlab}/main.go (85%) rename deploy/{hamlet.service => hamlab.service} (61%) diff --git a/Makefile b/Makefile index 1f19e1b..6dd8843 100644 --- a/Makefile +++ b/Makefile @@ -10,11 +10,11 @@ web: cd web && npm ci --no-audit --no-fund && npm run build build: web - $(GO) build -trimpath -ldflags "-s -w -X main.version=$(VERSION)" -o bin/hamlet ./cmd/hamlet + $(GO) build -trimpath -ldflags "-s -w -X main.version=$(VERSION)" -o bin/hamlab ./cmd/hamlab # backend only (assumes web/dist already built) go: - $(GO) build -ldflags "-X main.version=$(VERSION)" -o bin/hamlet ./cmd/hamlet + $(GO) build -ldflags "-X main.version=$(VERSION)" -o bin/hamlab ./cmd/hamlab dev: cd web && npm run dev diff --git a/PLAN.md b/PLAN.md index 2694c65..dd4d4bd 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,4 +1,4 @@ -# Hamlet — MVP plan +# Ham Lab — 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 @@ -13,17 +13,17 @@ stub tab or a roadmap item. | 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. | +| Service | `hamlab.service`, `User=core`, `Restart=always`, binary at `/usr/local/bin/hamlab` | 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. | +| 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. | ## Layout ``` ┌──────────────────────────────────────────────────────────────┐ -│ header: Hamlet · core · load 0.42 · ● connected · [logout] │ +│ header: Ham Lab · core · load 0.42 · ● connected · [logout] │ ├──────────────┬───────────────────────────────────────────────┤ │ ▾ Terminal │ │ │ shell 1 ● │ │ @@ -34,7 +34,7 @@ stub tab or a roadmap item. │ ▸ ZFS │ │ │ ▸ Dashboard │ │ ├──────────────┴───────────────────────────────────────────────┤ -│ footer: v0.1.0 · up 12d · hamlet.bigham.dev │ +│ footer: v0.1.0 · up 12d · hamlab.bigham.dev │ └──────────────────────────────────────────────────────────────┘ ``` @@ -80,8 +80,8 @@ Everything under `/api` except `login` and `/healthz` requires the session cooki ## Project layout ``` -hamlet/ - cmd/hamlet/main.go serve | passwd | version +hamlab/ + cmd/hamlab/main.go serve | passwd | version internal/ auth/ argon2id, sessions, rate limit term/ PTY manager, ring buffer, attach/detach @@ -90,16 +90,16 @@ hamlet/ 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 + hamlab.service + install.sh build → /usr/local/bin/hamlab → 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. +- Go module, chi, config loading, `hamlab passwd`, argon2id login, session cookie. +- `hamlab.service` on `core`, `:8100`, serves a placeholder page. - `make deploy` from `core` itself. **M1 — terminals** (the MVP feature) @@ -111,7 +111,7 @@ hamlet/ - 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. +- NPM proxy host for `hamlab.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 @@ -120,7 +120,7 @@ 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. + `tmux new -A -s hamlab-`; 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 @@ -129,7 +129,7 @@ something long in one, switch away and back, refresh — it's all still there. 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 + (`sensors -j`), UPS from `pwrstat`. Ham Lab 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 / diff --git a/README.md b/README.md index b7faadb..403518b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# Hamlet +# Ham Lab 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. +web UI. Named after Ham the cat (see also HamTV). 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`). +- Public at `https://hamlab.bigham.dev` (behind Nginx Proxy Manager on `core`). - Installable as a PWA. diff --git a/cmd/hamlet/main.go b/cmd/hamlab/main.go similarity index 85% rename from cmd/hamlet/main.go rename to cmd/hamlab/main.go index e6359d1..278593d 100644 --- a/cmd/hamlet/main.go +++ b/cmd/hamlab/main.go @@ -1,4 +1,4 @@ -// Hamlet — Proxmox-style web console for a bare-metal Debian host. +// Ham Lab — Proxmox-style web console for a bare-metal Debian host. package main import ( @@ -17,25 +17,25 @@ import ( "golang.org/x/term" - "hamlet/internal/api" - "hamlet/internal/auth" - "hamlet/internal/config" - "hamlet/internal/store" - hterm "hamlet/internal/term" - "hamlet/internal/web" + "hamlab/internal/api" + "hamlab/internal/auth" + "hamlab/internal/config" + "hamlab/internal/store" + hterm "hamlab/internal/term" + "hamlab/internal/web" ) var version = "dev" func main() { - defaultCfg := os.Getenv("HAMLET_CONFIG") + defaultCfg := os.Getenv("HAMLAB_CONFIG") if defaultCfg == "" { home, _ := os.UserHomeDir() - defaultCfg = filepath.Join(home, "apps", "hamlet", "config.json") + defaultCfg = filepath.Join(home, "apps", "hamlab", "config.json") } cfgPath := flag.String("config", defaultCfg, "path to config.json") flag.Usage = func() { - fmt.Fprintf(os.Stderr, "usage: hamlet [-config path] [serve|passwd|version]\n") + fmt.Fprintf(os.Stderr, "usage: hamlab [-config path] [serve|passwd|version]\n") flag.PrintDefaults() } flag.Parse() @@ -50,7 +50,7 @@ func main() { } switch cmd { case "version": - fmt.Println("hamlet", version) + fmt.Println("hamlab", version) case "passwd": if err := passwd(cfg); err != nil { log.Fatal(err) @@ -98,7 +98,7 @@ func passwd(cfg *config.Config) error { func serve(cfg *config.Config) error { if cfg.PasswordHash == "" { - return fmt.Errorf("no password set — run: hamlet -config %s passwd", cfg.Path()) + return fmt.Errorf("no password set — run: hamlab -config %s passwd", cfg.Path()) } if err := os.MkdirAll(cfg.DataDir(), 0o700); err != nil { return err @@ -152,7 +152,7 @@ func serve(cfg *config.Config) error { _ = hs.Shutdown(ctx) mgr.Shutdown() }() - log.Printf("hamlet %s listening on %s as %s (shell %s)", version, cfg.Listen, u.Username, shell) + log.Printf("hamlab %s listening on %s as %s (shell %s)", version, cfg.Listen, u.Username, shell) if err := hs.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { return err } diff --git a/deploy/hamlet.service b/deploy/hamlab.service similarity index 61% rename from deploy/hamlet.service rename to deploy/hamlab.service index 069cfff..55fd8fa 100644 --- a/deploy/hamlet.service +++ b/deploy/hamlab.service @@ -1,5 +1,5 @@ [Unit] -Description=Hamlet web console +Description=Ham Lab web console After=network.target [Service] @@ -7,8 +7,8 @@ Type=simple User=core Group=core WorkingDirectory=/home/core -Environment=HAMLET_CONFIG=/home/core/apps/hamlet/config.json -ExecStart=/usr/local/bin/hamlet serve +Environment=HAMLAB_CONFIG=/home/core/apps/hamlab/config.json +ExecStart=/usr/local/bin/hamlab serve Restart=always RestartSec=3 KillMode=control-group diff --git a/deploy/install.sh b/deploy/install.sh index 575edb8..12e2578 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -2,10 +2,10 @@ # Install the built binary and unit on this host, then (re)start the service. set -eu cd "$(dirname "$0")/.." -sudo install -m 0755 bin/hamlet /usr/local/bin/hamlet -sudo install -m 0644 deploy/hamlet.service /etc/systemd/system/hamlet.service +sudo install -m 0755 bin/hamlab /usr/local/bin/hamlab +sudo install -m 0644 deploy/hamlab.service /etc/systemd/system/hamlab.service sudo systemctl daemon-reload -sudo systemctl enable --now hamlet.service >/dev/null -sudo systemctl restart hamlet.service +sudo systemctl enable --now hamlab.service >/dev/null +sudo systemctl restart hamlab.service sleep 1 -systemctl is-active hamlet.service && /usr/local/bin/hamlet version +systemctl is-active hamlab.service && /usr/local/bin/hamlab version diff --git a/go.mod b/go.mod index 26fb4e9..7c03e2c 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module hamlet +module hamlab go 1.26.0 diff --git a/internal/api/server.go b/internal/api/server.go index cd21fa6..ca03955 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -15,12 +15,12 @@ import ( "github.com/coder/websocket" - "hamlet/internal/auth" - "hamlet/internal/config" - "hamlet/internal/term" + "hamlab/internal/auth" + "hamlab/internal/config" + "hamlab/internal/term" ) -const cookieName = "hamlet_session" +const cookieName = "hamlab_session" type Server struct { cfg *config.Config diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 316b907..af6cdf9 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -14,7 +14,7 @@ import ( "golang.org/x/crypto/argon2" - "hamlet/internal/store" + "hamlab/internal/store" ) const ( diff --git a/internal/config/config.go b/internal/config/config.go index 3a72f21..ec9e131 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,4 +1,4 @@ -// Package config loads and saves hamlet's JSON config file. +// Package config loads and saves hamlab's JSON config file. package config import ( diff --git a/internal/term/manager.go b/internal/term/manager.go index 5d284a5..bb48a01 100644 --- a/internal/term/manager.go +++ b/internal/term/manager.go @@ -87,7 +87,7 @@ func (m *Manager) Create(id, name string) (*Session, error) { } cmd := exec.Command(m.shell, "-l") cmd.Dir = m.home - cmd.Env = append(os.Environ(), "TERM=xterm-256color", "COLORTERM=truecolor", "HAMLET=1") + cmd.Env = append(os.Environ(), "TERM=xterm-256color", "COLORTERM=truecolor", "HAMLAB=1") ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{Cols: defaultCol, Rows: defaultRow}) if err != nil { return nil, err diff --git a/web/index.html b/web/index.html index 8660e4f..aa9c2f0 100644 --- a/web/index.html +++ b/web/index.html @@ -7,11 +7,11 @@ - + - Hamlet + Ham Lab
diff --git a/web/package-lock.json b/web/package-lock.json index bbab6fd..a16a578 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,11 +1,11 @@ { - "name": "hamlet-web", + "name": "hamlab-web", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "hamlet-web", + "name": "hamlab-web", "version": "0.1.0", "dependencies": { "@xterm/addon-fit": "^0.10.0", diff --git a/web/package.json b/web/package.json index e7e4428..3d2046e 100644 --- a/web/package.json +++ b/web/package.json @@ -1,5 +1,5 @@ { - "name": "hamlet-web", + "name": "hamlab-web", "private": true, "version": "0.1.0", "type": "module", diff --git a/web/public/manifest.webmanifest b/web/public/manifest.webmanifest index c24d929..f40de82 100644 --- a/web/public/manifest.webmanifest +++ b/web/public/manifest.webmanifest @@ -1,6 +1,6 @@ { - "name": "Hamlet", - "short_name": "Hamlet", + "name": "Ham Lab", + "short_name": "Ham Lab", "description": "Linux management console", "start_url": "/", "scope": "/", diff --git a/web/public/sw.js b/web/public/sw.js index af261a6..0d19f5d 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -1,5 +1,5 @@ -// Hamlet service worker: cache hashed assets, never the API, network-first for the shell. -const CACHE = 'hamlet-v1'; +// Ham Lab service worker: cache hashed assets, never the API, network-first for the shell. +const CACHE = 'hamlab-v1'; self.addEventListener('install', () => self.skipWaiting()); self.addEventListener('activate', (e) => { e.waitUntil(caches.keys().then((ks) => Promise.all(ks.filter((k) => k !== CACHE).map((k) => caches.delete(k)))).then(() => self.clients.claim())); diff --git a/web/src/Login.tsx b/web/src/Login.tsx index a05e8b9..5c185d7 100644 --- a/web/src/Login.tsx +++ b/web/src/Login.tsx @@ -17,7 +17,7 @@ export default function Login({ onLogin }: { onLogin: () => void }) { return (
-

Hamlet

+

Ham Lab

setPw(e.target.value)} /> diff --git a/web/src/Shell.tsx b/web/src/Shell.tsx index fb2b358..639b62e 100644 --- a/web/src/Shell.tsx +++ b/web/src/Shell.tsx @@ -56,7 +56,7 @@ export default function Shell({ me, onLoggedOut }: { me: Me; onLoggedOut: () =>
- Hamlet + Ham Lab {me.user}@{me.host} {me.load}