- 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.
53 lines
1.6 KiB
Go
53 lines
1.6 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"backend/templates"
|
|
)
|
|
|
|
// HealthzHandler returns an HTTP handler that probes the supplied Pinger
|
|
// with a 2-second timeout and responds per CONTEXT D-20: 200 + JSON
|
|
// `{"status":"ok","db":"ok"}` when reachable, 503 + JSON
|
|
// `{"status":"degraded","db":"down"}` otherwise.
|
|
func HealthzHandler(pinger Pinger) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
|
defer cancel()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := pinger.Ping(ctx); err != nil {
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{
|
|
"status": "degraded",
|
|
"db": "down",
|
|
})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{
|
|
"status": "ok",
|
|
"db": "ok",
|
|
})
|
|
}
|
|
}
|
|
|
|
// IndexHandler renders the root page (templates.Index) as text/html.
|
|
func IndexHandler() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
_ = templates.Index().Render(r.Context(), w)
|
|
}
|
|
}
|
|
|
|
// DemoTimeHandler renders the HTMX fragment for /demo/time. The `now` clock
|
|
// is injected so tests can substitute a deterministic time source; production
|
|
// passes time.Now.
|
|
func DemoTimeHandler(now func() time.Time) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
_ = templates.TimeFragment(now()).Render(r.Context(), w)
|
|
}
|
|
}
|