xtablo-source/backend/internal/web/router.go
Arthur Belleville 3a12f8f47d
feat(01-03): templ layout/index/fragments + handlers + chi router
- templates/layout.templ: base HTML shell per UI-SPEC §Base Layout Contract
  (max-w-5xl container, slate-50 header, slate-200 borders, footer copy,
  /static/tailwind.css in <head>, /static/htmx.min.js deferred at body end —
  D-10: HTMX never loaded from a CDN)
- templates/index.templ: root page consuming @ui.Card and @ui.Button for the
  canonical HTMX demo (UI-SPEC §Component Library Contract canonical block)
- templates/fragments.templ: TimeFragment renders <span> with RFC3339 UTC
  timestamp; templ auto-escapes interpolation (T-01-13)
- internal/web/handlers.go: HealthzHandler (200 ok / 503 degraded per D-20,
  2s Ping timeout), IndexHandler, DemoTimeHandler with injected clock
- internal/web/router.go: Pinger interface; NewRouter wires
  RequestIDMiddleware → RealIP → SlogLoggerMiddleware → Recoverer (D-08
  + Pitfall 6 — chi middleware.Logger deliberately NOT registered) and
  routes /, /healthz, /demo/time, /static/* via http.FileServer (T-01-08
  path traversal blocked by http.Dir)

All six handler tests + ui package tests are GREEN under default go test.
2026-05-14 19:25:43 +02:00

46 lines
1.4 KiB
Go

package web
import (
"context"
"log/slog"
"net/http"
"time"
"github.com/go-chi/chi/v5"
chimw "github.com/go-chi/chi/v5/middleware"
)
// Pinger is the contract /healthz uses to probe the data plane. *pgxpool.Pool
// satisfies this interface out of the box, which is why cmd/web passes the
// pool directly to NewRouter (no adapter required).
type Pinger interface {
Ping(ctx context.Context) error
}
// NewRouter constructs the chi router with the middleware stack locked by
// CONTEXT D-08 + RESEARCH Pattern 2:
//
// 1. RequestIDMiddleware (UUIDv4 — NOT chi's base32 RequestID)
// 2. chi RealIP
// 3. SlogLoggerMiddleware (REPLACES chi's middleware.Logger — Pitfall 6)
// 4. chi Recoverer (after Logger so panics carry request_id)
//
// Routes (Phase 1 only): GET / · GET /healthz · GET /demo/time · GET /static/*.
// staticDir is the on-disk path served at /static/*; path traversal is
// blocked by http.Dir's default behavior (T-01-08).
func NewRouter(pinger Pinger, staticDir string) http.Handler {
r := chi.NewRouter()
r.Use(RequestIDMiddleware)
r.Use(chimw.RealIP)
r.Use(SlogLoggerMiddleware(slog.Default()))
r.Use(chimw.Recoverer)
r.Get("/", IndexHandler())
r.Get("/healthz", HealthzHandler(pinger))
r.Get("/demo/time", DemoTimeHandler(func() time.Time { return time.Now() }))
fs := http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))
r.Get("/static/*", fs.ServeHTTP)
return r
}