All checks were successful
Deploy to K8s / deploy (push) Successful in 7s
Stand up the Go-native HTML frontend foundation: - base.tmpl layout + nav/footer partials (three-tier nav, active-link highlighting) - terminal-green-on-black theme extracted to static/css/app.css (served via embed.FS) - HTMLHandler with stub pages for all five routes; / redirects to /adults - NewRenderer parses per-page template sets at startup so parse failures abort boot - Smoke test: each route returns 200 text/html with exactly one class="active" link Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
89 lines
7.7 KiB
Markdown
89 lines
7.7 KiB
Markdown
# Plan: Go rewrite — M6.1 Template skeleton
|
|
|
|
## Context
|
|
|
|
M5 finished — Go and Python now produce byte-equal JSON across `/api/{adults,juniors,payments,version}` and `cmd/parity` enforces it. See [progress tracker §M5](2026-05-03-2349-go-backend-rewrite-progress.md#L96-L106).
|
|
|
|
M6 begins the Go-native HTML frontend that lets us actually retire the Python web UI. **M6.1 is the foundation step**: stand up the base HTML layout, shared nav, terminal-green-on-black theme, and the `embed.FS` plumbing that every subsequent M6 page (adults, juniors, payments, sync, flush, qr) will compose into. After M6.1, opening :8080 shows chrome (header, nav, footer) on every route with placeholder bodies — no real data wiring; that lands in M6.2+.
|
|
|
|
This is **not a Jinja port**. The Python templates are reference for nav structure and CSS values; the Go layout is designed natively (per the [master plan](2026-05-03-2349-go-backend-rewrite.md#L24-L27): "frontends are allowed to diverge").
|
|
|
|
## Current Go web state (what we're building on)
|
|
|
|
- [go/internal/web/server.go](../../go/internal/web/server.go) — `Run()` registers `GET /{$}` (text-plain hello) + four `/api/*` routes. `BuildInfo{Version,Commit,BuildDate}` already flows in.
|
|
- [go/internal/web/api/handler.go](../../go/internal/web/api/handler.go) — central handler struct holding `Sources`, `Config`, `Logger`, build fields.
|
|
- No `templates/`, no `static/`, no `embed` directives, no `html/template` import anywhere in `go/`. We're starting from a blank slate.
|
|
|
|
## Approach
|
|
|
|
1. **Create `templates/` and `static/` under `go/internal/web/`**, embedded via `//go:embed`.
|
|
2. **Single base template** (`base.tmpl`) using `html/template` `block`s for `title` and `content`. Each page template defines those blocks; the renderer executes via `ExecuteTemplate(w, "base", data)`.
|
|
3. **Nav partial** parameterized by a `pageData.Active` string so each page highlights its own link with `class="active"`. Three tiers preserved (Primary / Archived / Tools) — matches Python UX in [templates/adults.html:491-505](../../templates/adults.html#L491-L505).
|
|
4. **CSS** lifted verbatim from [templates/adults.html:8-487](../../templates/adults.html#L8-L487) into `static/css/app.css`. Page templates `<link>` it from the base layout. M6.2+ reuses the same class names (`.cell-ok`, `.modal-content`, …) without re-extracting.
|
|
5. **HTML handlers** in a new `go/internal/web/html/` package (sibling of `api/`). One `Handler` struct holding `*Renderer` + `BuildInfo` + `Config`. Routes for `/`, `/adults`, `/juniors`, `/payments`, `/sync-bank`, `/flush-cache` registered in `server.go`. Each route renders a placeholder body ("Coming in M6.X") inside the base shell — the goal is to prove the chrome works, not to populate data.
|
|
6. **Replace the existing `helloHandler`**: `GET /{$}` → `http.Redirect` to `/adults` (mirrors Python's meta-refresh in [app.py:112-115](../../app.py#L112-L115)).
|
|
7. **Static file route**: `GET /static/` served from `staticFS` via `http.StripPrefix("/static/", http.FileServerFS(staticFS))`.
|
|
8. **Footer keeps it simple**: `{{.Build.Version}}@{{.Build.Commit}} | built {{.Build.BuildDate}}`. The Python per-request render-time breakdown is deferred — `middleware/timer.go` only logs to slog today; threading a timer into request context can come back in a later milestone if we miss it.
|
|
|
|
## Files to create / modify
|
|
|
|
```
|
|
go/internal/web/
|
|
├── assets.go NEW — //go:embed templates/* static/*
|
|
├── render.go NEW — Renderer wraps parsed *template.Template
|
|
├── server.go MODIFY — drop helloHandler; mount HTML routes + /static/
|
|
├── html/
|
|
│ ├── handler.go NEW — html.Handler + page methods
|
|
│ └── handler_test.go NEW — httptest smoke per route
|
|
├── templates/
|
|
│ ├── base.tmpl NEW — <html>/<head>/<body>; nav + content + footer blocks
|
|
│ ├── partials/
|
|
│ │ ├── nav.tmpl NEW — three-tier nav, active highlighting
|
|
│ │ └── footer.tmpl NEW — build meta line
|
|
│ ├── adults.tmpl NEW — placeholder "Coming in M6.2"
|
|
│ ├── juniors.tmpl NEW — placeholder "Coming in M6.3"
|
|
│ ├── payments.tmpl NEW — placeholder "Coming in M6.4"
|
|
│ ├── sync.tmpl NEW — placeholder "Coming in M6.6"
|
|
│ └── flush_cache.tmpl NEW — placeholder "Coming in M6.6"
|
|
└── static/
|
|
└── css/
|
|
└── app.css NEW — verbatim from templates/adults.html:8-487
|
|
```
|
|
|
|
## Key design notes
|
|
|
|
- **Parse once at startup**: `NewRenderer(fs embed.FS)` calls `template.ParseFS(fs, "templates/*.tmpl", "templates/partials/*.tmpl")`. Parse failure aborts boot — template syntax errors surface immediately, not at first request.
|
|
- **Per-page cloned templates** to avoid `define "content"` collisions across pages: `Renderer` stores `map[string]*template.Template` keyed by page name; each is `base+partials` clone with that page's `content`/`title` overlaid.
|
|
- **View model** — small `pageData` struct: `Active` (nav key), `Build` (BuildInfo), `Body` (per-page payload, `any` until M6.2 widens it).
|
|
- **Active-link logic** in nav partial: `{{ if eq .Active "adults" }}class="active"{{ end }}` — markup-side, no template funcs needed.
|
|
- **No JS in M6.1.** Member-detail and QR modals + `static/js/*.js` come in M6.5. M6.1 only sets up the embedded-asset machinery.
|
|
- **Nav anchor labels are literal text** (`[Adults]`, `[Juniors]`, etc.) — keep the brackets in the template strings, they're part of the look.
|
|
- **`/qr` is not in the nav** — it's an image endpoint, M6.6 territory.
|
|
|
|
## Critical files to read
|
|
|
|
- [go/internal/web/server.go](../../go/internal/web/server.go) — mux registration; `Run()` signature stays unchanged.
|
|
- [go/internal/web/api/handler.go](../../go/internal/web/api/handler.go) — pattern to mirror for `html.Handler` (same field layout, most fields unused in M6.1).
|
|
- [templates/adults.html:8-487](../../templates/adults.html#L8-L487) — CSS source, copy verbatim.
|
|
- [templates/adults.html:491-505](../../templates/adults.html#L491-L505) — nav markup source (three tiers).
|
|
- [docs/plans/2026-05-03-2349-go-backend-rewrite.md:88-101](2026-05-03-2349-go-backend-rewrite.md#L88-L101) — original layout intent for `web/handlers/`, `web/templates/`, `web/static/`.
|
|
|
|
## Verification
|
|
|
|
End-to-end smoke from a fresh `make go-build`:
|
|
|
|
1. `make web-go &` — server boots; no template-parse errors in slog output.
|
|
2. `curl -i localhost:8080/` → `302` to `/adults`.
|
|
3. `curl -s localhost:8080/adults | grep -F 'class="active"'` matches the Adults anchor only.
|
|
4. `curl -sI localhost:8080/static/css/app.css` → `200`, `Content-Type: text/css`.
|
|
5. Browser at `http://localhost:8080/adults`: terminal-green-on-black theme, three-tier nav at top (Adults active), "Coming in M6.2" placeholder, footer `{tag}@{commit} | built {date}`.
|
|
6. Click each nav link in turn (`/juniors`, `/payments`, `/sync-bank`, `/flush-cache`) — same chrome, the clicked link is highlighted, per-page placeholder body shows.
|
|
7. `make go-test` — `web/html/handler_test.go` passes: each route returns 200, `text/html`, `class="active"` only on the matching anchor.
|
|
8. `make go-lint` clean.
|
|
9. `make parity` still green (regression check — M6.1 doesn't touch `/api/*`).
|
|
10. CHANGELOG entry per CLAUDE.md; M6.1 ticked in [progress tracker §M6.1](2026-05-03-2349-go-backend-rewrite-progress.md#L113) with commit SHA.
|
|
|
|
## Branch & MR
|
|
|
|
`feat/go-m6-1-template-skeleton` per CLAUDE.md branch-per-feature workflow. Open MR via `tea pr create --base main --head feat/go-m6-1-template-skeleton`. User merges in the Gitea browser UI.
|