Files
fuj-management/docs/plans/2026-05-08-0034-go-rewrite-m6-1-template-skeleton.md
Jan Novak 78e5059759
All checks were successful
Deploy to K8s / deploy (push) Successful in 7s
feat(go): M6.1 — template skeleton, embed.FS, HTML routes
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>
2026-05-08 00:45:22 +02:00

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.goRun() registers GET /{$} (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/, 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 blocks 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.
  4. 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.
  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).
  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

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.css200, 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-testweb/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 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.