- 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.
46 lines
1.4 KiB
Go
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
|
|
}
|