Rename: Hamlet -> Ham Lab (binary/module/service/config hamlab)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-17 10:40:40 -07:00
co-authored by Claude Fable 5.1
parent 436732dac0
commit d76da4b37a
18 changed files with 61 additions and 61 deletions
+2 -2
View File
@@ -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
+16 -16
View File
@@ -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-<id>`; needs `mouse on` and status-bar handling in xterm.
`tmux new -A -s hamlab-<id>`; 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 /
+3 -3
View File
@@ -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.
+13 -13
View File
@@ -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
}
@@ -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
+5 -5
View File
@@ -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
+1 -1
View File
@@ -1,4 +1,4 @@
module hamlet
module hamlab
go 1.26.0
+4 -4
View File
@@ -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
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"golang.org/x/crypto/argon2"
"hamlet/internal/store"
"hamlab/internal/store"
)
const (
+1 -1
View File
@@ -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 (
+1 -1
View File
@@ -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
+2 -2
View File
@@ -7,11 +7,11 @@
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Hamlet" />
<meta name="apple-mobile-web-app-title" content="Ham Lab" />
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" href="/icons/icon-192.png" />
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
<title>Hamlet</title>
<title>Ham Lab</title>
</head>
<body>
<div id="root"></div>
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "hamlet-web",
"name": "hamlab-web",
"private": true,
"version": "0.1.0",
"type": "module",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "Hamlet",
"short_name": "Hamlet",
"name": "Ham Lab",
"short_name": "Ham Lab",
"description": "Linux management console",
"start_url": "/",
"scope": "/",
+2 -2
View File
@@ -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()));
+1 -1
View File
@@ -17,7 +17,7 @@ export default function Login({ onLogin }: { onLogin: () => void }) {
return (
<div className="center">
<form className="login" onSubmit={submit}>
<h1>Hamlet</h1>
<h1>Ham Lab</h1>
<input type="password" autoFocus autoComplete="current-password" placeholder="password"
value={pw} onChange={(e) => setPw(e.target.value)} />
<button disabled={busy || !pw}>{busy ? '…' : 'Sign in'}</button>
+1 -1
View File
@@ -56,7 +56,7 @@ export default function Shell({ me, onLoggedOut }: { me: Me; onLoggedOut: () =>
<div className={'app' + (drawer ? ' drawer-open' : '')}>
<header className="header">
<button className="icon-btn menu" onClick={() => setDrawer(!drawer)} aria-label="menu">☰</button>
<span className="brand">Hamlet</span>
<span className="brand">Ham Lab</span>
<span className="host">{me.user}@{me.host}</span>
<span className="spacer" />
<span className="stat" title="load average">{me.load}</span>