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>
7.7 KiB
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.
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: "frontends are allowed to diverge").
Current Go web state (what we're building on)
- go/internal/web/server.go —
Run()registersGET /{$}(text-plain hello) + four/api/*routes.BuildInfo{Version,Commit,BuildDate}already flows in. - go/internal/web/api/handler.go — central handler struct holding
Sources,Config,Logger, build fields. - No
templates/, nostatic/, noembeddirectives, nohtml/templateimport anywhere ingo/. We're starting from a blank slate.
Approach
- Create
templates/andstatic/undergo/internal/web/, embedded via//go:embed. - Single base template (
base.tmpl) usinghtml/templateblocks fortitleandcontent. Each page template defines those blocks; the renderer executes viaExecuteTemplate(w, "base", data). - Nav partial parameterized by a
pageData.Activestring so each page highlights its own link withclass="active". Three tiers preserved (Primary / Archived / Tools) — matches Python UX in templates/adults.html:491-505. - CSS lifted verbatim from templates/adults.html:8-487 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. - HTML handlers in a new
go/internal/web/html/package (sibling ofapi/). OneHandlerstruct holding*Renderer+BuildInfo+Config. Routes for/,/adults,/juniors,/payments,/sync-bank,/flush-cacheregistered inserver.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. - Replace the existing
helloHandler:GET /{$}→http.Redirectto/adults(mirrors Python's meta-refresh in app.py:112-115). - Static file route:
GET /static/served fromstaticFSviahttp.StripPrefix("/static/", http.FileServerFS(staticFS)). - Footer keeps it simple:
{{.Build.Version}}@{{.Build.Commit}} | built {{.Build.BuildDate}}. The Python per-request render-time breakdown is deferred —middleware/timer.goonly 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)callstemplate.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:Rendererstoresmap[string]*template.Templatekeyed by page name; each isbase+partialsclone with that page'scontent/titleoverlaid. - View model — small
pageDatastruct:Active(nav key),Build(BuildInfo),Body(per-page payload,anyuntil 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/*.jscome 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. /qris not in the nav — it's an image endpoint, M6.6 territory.
Critical files to read
- go/internal/web/server.go — mux registration;
Run()signature stays unchanged. - 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 — CSS source, copy verbatim.
- templates/adults.html:491-505 — nav markup source (three tiers).
- docs/plans/2026-05-03-2349-go-backend-rewrite.md:88-101 — original layout intent for
web/handlers/,web/templates/,web/static/.
Verification
End-to-end smoke from a fresh make go-build:
make web-go &— server boots; no template-parse errors in slog output.curl -i localhost:8080/→302to/adults.curl -s localhost:8080/adults | grep -F 'class="active"'matches the Adults anchor only.curl -sI localhost:8080/static/css/app.css→200,Content-Type: text/css.- 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}. - Click each nav link in turn (
/juniors,/payments,/sync-bank,/flush-cache) — same chrome, the clicked link is highlighted, per-page placeholder body shows. make go-test—web/html/handler_test.gopasses: each route returns 200,text/html,class="active"only on the matching anchor.make go-lintclean.make paritystill green (regression check — M6.1 doesn't touch/api/*).- CHANGELOG entry per CLAUDE.md; M6.1 ticked in progress tracker §M6.1 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.