---
phase: 01-foundation
plan: 03
type: execute
wave: 3
depends_on: ["01-01", "01-02"]
files_modified:
  - backend/internal/db/pool.go
  - backend/internal/web/router.go
  - backend/internal/web/handlers.go
  - backend/internal/web/middleware.go
  - backend/internal/web/slog.go
  - backend/templates/layout.templ
  - backend/templates/index.templ
  - backend/templates/fragments.templ
  - backend/cmd/web/main.go
  - backend/cmd/worker/main.go
autonomous: false
requirements:
  - FOUND-01
  - FOUND-02
  - FOUND-03
  - FOUND-04
tags:
  - go
  - chi
  - templ
  - htmx
  - pgx
  - slog
  - graceful-shutdown

must_haves:
  truths:
    - "After this plan lands, every test in `backend/internal/web/handlers_test.go` and `backend/internal/db/pool_test.go` is GREEN under default `go test ./...` — the `//go:build red_gate` tags placed in Plan 01-02 are REMOVED in this plan's Task 1 (Codex concern #3). After removal, `go test ./...` (no tag) runs the full suite green."
    - "`go mod tidy` runs cleanly at the start of Task 1 (Codex concern #1 — deferred from Plan 01-01) and does NOT strip any of the five pinned runtime deps because real importers now exist in `internal/db/pool.go`, `internal/web/{router,handlers,middleware}.go`, and `cmd/{web,worker}/main.go`."
    - "`go build ./cmd/web ./cmd/worker` succeeds — both binaries compile"
    - "Running `./tmp/web` (or `go run ./cmd/web`) with `DATABASE_URL` pointing at the compose-managed Postgres serves `/` (200, HTML), `/healthz` (200, JSON with db:ok), `/demo/time` (200, HTML fragment), and `/static/*` (file server)"
    - "The root page is rendered via `internal/web/ui` components — handlers do NOT inline Tailwind classes — and contains the canonical `hx-get` button declared in UI-SPEC"
    - "Sending SIGINT to the web binary triggers graceful shutdown: in-flight requests are allowed to finish (up to 10s), then the pgxpool is closed, then the process exits 0"
    - "`./tmp/worker` connects to Postgres, logs `worker ready` via slog, blocks on signal, and exits 0 on SIGINT (D-03)"
    - "D-08: chi router middleware stack ordered RequestID then RealIP then Logger then Recoverer per the locked CONTEXT decision"
    - "D-10: HTMX runtime vendored at backend/static/htmx.min.js — never loaded from a CDN; base layout references the local path"
    - "D-17: Structured logging via log/slog standard library — JSON handler when ENV=production and text handler in development"
    - "D-18: RequestID middleware attaches a UUIDv4 per request into context.Context and onto the slog logger as request_id attribute"
    - "D-19: Graceful shutdown traps SIGINT and SIGTERM; http.Server.Shutdown runs with 10-second timeout then pgxpool Close runs before process exits"
  artifacts:
    - path: backend/internal/db/pool.go
      provides: NewPool(ctx, dsn) (*pgxpool.Pool, error) per RESEARCH Pattern 1
      contains: "func NewPool"
    - path: backend/internal/web/router.go
      provides: NewRouter(pinger Pinger, staticDir string) http.Handler + Pinger interface declaration
      contains: "type Pinger interface"
    - path: backend/internal/web/handlers.go
      provides: HealthzHandler, IndexHandler, DemoTimeHandler
      contains: "func HealthzHandler"
    - path: backend/internal/web/middleware.go
      provides: RequestIDMiddleware, SlogLoggerMiddleware, request_id context key + helper
      contains: "func RequestIDMiddleware"
    - path: backend/internal/web/slog.go
      provides: NewSlogHandler(env, w) slog.Handler — JSON when env=production, text otherwise
      contains: "func NewSlogHandler"
    - path: backend/templates/layout.templ
      provides: Base templ layout per UI-SPEC §Base Layout Contract
      contains: "templ Layout"
    - path: backend/templates/index.templ
      provides: Root page rendering via ui.Card + ui.Button + the canonical hx-get demo
      contains: "templ Index"
    - path: backend/templates/fragments.templ
      provides: TimeFragment(t time.Time) templ component returning `<span>{ISO-8601-UTC}</span>`
      contains: "templ TimeFragment"
    - path: backend/cmd/web/main.go
      provides: Web entrypoint — loads env, builds slog handler, opens pgxpool, builds router, runs http.Server with graceful shutdown
      contains: "package main"
    - path: backend/cmd/worker/main.go
      provides: Worker skeleton — opens pgxpool, logs ready, blocks on signal, closes pool, exits (D-03)
      contains: "package main"
  key_links:
    - from: backend/cmd/web/main.go
      to: backend/internal/web/router.go (NewRouter)
      via: import + direct call
      pattern: "web.NewRouter"
    - from: backend/cmd/web/main.go
      to: backend/internal/db/pool.go (NewPool)
      via: import + direct call with DATABASE_URL
      pattern: "db.NewPool"
    - from: backend/templates/index.templ
      to: backend/internal/web/ui (Button, Card)
      via: templ component imports
      pattern: "ui.Button|ui.Card"
    - from: backend/internal/web/handlers.go (IndexHandler)
      to: backend/templates/index.templ
      via: templates.Index().Render(ctx, w)
      pattern: "templates\\.Index"
    - from: backend/internal/web/handlers.go (DemoTimeHandler)
      to: backend/templates/fragments.templ (TimeFragment)
      via: templates.TimeFragment(now()).Render(ctx, w)
      pattern: "templates\\.TimeFragment"
    - from: backend/internal/web/middleware.go (RequestIDMiddleware)
      to: backend/internal/web/slog.go
      via: ctx-stored UUID is read by SlogLoggerMiddleware and attached as a "request_id" slog attribute
      pattern: "request_id"
---

<objective>
Implement the thinnest end-to-end slice that turns Plan 02's RED tests GREEN: pgxpool wiring, chi router with middleware stack, /healthz / / /demo/time handlers, base + index + fragments templ templates (consuming `internal/web/ui`), and the two main entrypoints (`cmd/web` with full graceful shutdown, `cmd/worker` as a minimal skeleton per D-03).

Purpose: This is the GREEN step of the Walking Skeleton. After this plan, a developer can run `just dev`, open `http://localhost:8080/`, click the demo button, and see the HTMX round-trip work end-to-end against a real Postgres connection. Every FOUND-01..04 requirement is functionally satisfied here.

Output: 10 files; all Plan 02 tests pass; `go build ./cmd/web ./cmd/worker` produces both binaries.
</objective>

<execution_context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
@.planning/phases/01-foundation/01-UI-SPEC.md
@.planning/phases/01-foundation/01-VALIDATION.md
@.planning/phases/01-foundation/SKELETON.md
@.planning/phases/01-foundation/01-01-SUMMARY.md
@.planning/phases/01-foundation/01-02-SUMMARY.md

# Tests this plan must turn GREEN — read carefully, do not modify
@backend/internal/web/handlers_test.go
@backend/internal/db/pool_test.go
@backend/internal/web/ui/variants.go
@backend/internal/web/ui/button.templ
@backend/internal/web/ui/card.templ

<interfaces>
# Interfaces from Plan 02's tests that this plan implements verbatim:

```go
// internal/web
type Pinger interface { Ping(ctx context.Context) error }

func NewRouter(pinger Pinger, staticDir string) http.Handler
func HealthzHandler(pinger Pinger) http.HandlerFunc
func IndexHandler() http.HandlerFunc
func DemoTimeHandler(now func() time.Time) http.HandlerFunc
func RequestIDMiddleware(next http.Handler) http.Handler
func SlogLoggerMiddleware(logger *slog.Logger) func(http.Handler) http.Handler
func NewSlogHandler(env string, w io.Writer) slog.Handler

// internal/db
func NewPool(ctx context.Context, dsn string) (*pgxpool.Pool, error)

// pgxpool.Pool already satisfies the Pinger interface — used directly from cmd/web.
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: pgxpool wrapper + middleware (RequestID, slog) + slog handler switch</name>
  <files>backend/internal/db/pool.go, backend/internal/web/middleware.go, backend/internal/web/slog.go</files>
  <read_first>
    - backend/internal/web/handlers_test.go (TestRequestID_HeaderSet, TestSlog_HandlerSwitch — exact expectations)
    - backend/internal/db/pool_test.go (TestPool_Connects expectations)
    - .planning/phases/01-foundation/01-RESEARCH.md (Pattern 1 pgxpool wiring; Pattern 3 slog handler switch; Pattern 4 RequestID → context → slog)
  </read_first>
  <behavior>
    - `db.NewPool(ctx, dsn)` returns a `*pgxpool.Pool` with `MaxConns=10`, `MinConns=1`, no eager Ping (lazy per RESEARCH Pitfall 2). Returns error for an unparseable DSN.
    - `web.NewSlogHandler("production", w)` returns a `*slog.JSONHandler` writing to `w`.
    - `web.NewSlogHandler("development", w)` (or any non-"production" string) returns a `*slog.TextHandler`.
    - `web.RequestIDMiddleware` generates a UUIDv4 via `github.com/google/uuid`, attaches it to the request context under the package-private `requestIDKey` ctx key, and sets the `X-Request-ID` response header.
    - `web.LoggerFromContext(ctx)` (helper) returns `slog.Default().With("request_id", id)` if a request ID is in context, else `slog.Default()`.
    - `web.SlogLoggerMiddleware(logger)` is a factory returning a `func(http.Handler) http.Handler` middleware that logs one structured line per request (method, path, status, duration, request_id) — uses `chi/v5/middleware.WrapResponseWriter` to capture status.
  </behavior>
  <action>
    **Pre-step (Codex concerns #1 and #3).** Before writing any new code, perform two cleanup steps:

    1. **Remove the `//go:build red_gate` line** (and the blank line immediately after it) from BOTH `backend/internal/web/handlers_test.go` and `backend/internal/db/pool_test.go`. Plan 01-02 placed these tags so the failing tests would not leak into default `go test ./...` runs; this plan turns them GREEN, so the gate is no longer needed.
    2. **Now that consumer code is about to exist**, the runtime deps pinned in Plan 01-01 will have real importers in this plan. After Task 1's files are written, run `cd backend && go mod tidy` — it should be a no-op on the require list (Codex concern #1: tidy was deferred to here precisely so this is safe).

    Write `internal/db/pool.go` per RESEARCH Pattern 1 exactly. Use `github.com/jackc/pgx/v5/pgxpool`. The function MUST NOT call `pool.Ping` (Pitfall 2 — lazy is correct).

    Write `internal/web/slog.go`: declare `NewSlogHandler(env string, w io.Writer) slog.Handler`. Use `slog.HandlerOptions{Level: slog.LevelInfo}`. Switch by `env == "production"` per RESEARCH Pattern 3. Do NOT call `slog.SetDefault` here — that's `cmd/web`'s job. The function returns the handler, callers decide what to do with it.

    Write `internal/web/middleware.go`:
    - Declare `type ctxKey string; const requestIDKey ctxKey = "request_id"`.
    - `RequestIDMiddleware(next http.Handler) http.Handler` — per RESEARCH Pattern 4.
    - `LoggerFromContext(ctx context.Context) *slog.Logger` — per RESEARCH Pattern 4.
    - `SlogLoggerMiddleware(logger *slog.Logger) func(http.Handler) http.Handler`: wraps the handler, records start time, uses `github.com/go-chi/chi/v5/middleware.NewWrapResponseWriter(w, r.ProtoMajor)` to capture status, then logs `logger.With("request_id", id).Info("request", "method", r.Method, "path", r.URL.Path, "status", ww.Status(), "duration_ms", ms)`. Per RESEARCH Pitfall 6: do NOT also register chi's `middleware.Logger`.

    All three files: package clause matches directory (`package db` for pool.go; `package web` for the other two).
  </action>
  <verify>
    <automated>cd backend && ! grep -q '^//go:build red_gate' internal/web/handlers_test.go && ! grep -q '^//go:build red_gate' internal/db/pool_test.go && go mod tidy && templ generate && go build ./internal/db/ ./internal/web/ && go test ./internal/web/ -run 'TestRequestID_HeaderSet|TestSlog_HandlerSwitch' -count=1</automated>
  </verify>
  <acceptance_criteria>
    - `//go:build red_gate` line is removed from both `internal/web/handlers_test.go` and `internal/db/pool_test.go` (Codex concern #3 — RED gate retired)
    - `go mod tidy` runs cleanly and produces no removal of the five pinned runtime deps (Codex concern #1 — deferred tidy from Plan 01-01 is safe here)
    - `go build ./internal/db/ ./internal/web/` succeeds (no missing symbols from Plan 02 tests)
    - `TestRequestID_HeaderSet` passes: response carries a UUIDv4 in `X-Request-ID`
    - `TestSlog_HandlerSwitch` passes: production handler emits JSON, dev handler emits text
    - `TestPool_Connects` either passes (with `DATABASE_URL` set) or skips cleanly
    - chi's built-in `middleware.Logger` is NOT imported anywhere (per Pitfall 6 — `grep -r 'chi/v5/middleware.Logger' internal/web/` returns empty)
  </acceptance_criteria>
  <done>The three infrastructure files are GREEN; two of the six handler tests now pass; the pgxpool wrapper satisfies the integration test.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Templates (layout, index, fragments) + handlers + router</name>
  <files>backend/templates/layout.templ, backend/templates/index.templ, backend/templates/fragments.templ, backend/internal/web/handlers.go, backend/internal/web/router.go</files>
  <read_first>
    - backend/internal/web/handlers_test.go (TestHealthz_OK, TestHealthz_Down, TestIndex_RendersHxGet, TestDemoTime_Fragment)
    - .planning/phases/01-foundation/01-UI-SPEC.md (Base Layout Contract §, HTMX Interaction Pattern §, Copywriting Contract §, Component Library Contract § — canonical Button usage block is the exact templ source for the demo CTA)
    - .planning/phases/01-foundation/01-RESEARCH.md (Code Examples §`/healthz` handler; Pattern 6 templ + chi; Pattern 7 hx-get demo)
    - backend/internal/web/ui/button.templ + card.templ (the components index.templ consumes)
  </read_first>
  <behavior>
    - `templates.Layout(title string)` renders the exact HTML shape from UI-SPEC §Base Layout Contract (system font stack via Tailwind defaults, `bg-white text-slate-900 antialiased`, header/main/footer regions, `/static/htmx.min.js` defer-loaded, `/static/tailwind.css` in `<head>`)
    - `templates.Index()` wraps `Layout("Xtablo — Foundation")` with the canonical UI-SPEC content: H1 "Xtablo", muted subtitle (exact copy from Copywriting Contract), then a `@ui.Card` containing the demo section (H2 "HTMX demo", helper paragraph, the `@ui.Button` with hx-get attrs, the loading indicator span, and `<div id="demo-out">No time fetched yet.</div>`)
    - `templates.TimeFragment(t time.Time)` renders `<span class="text-slate-900">{t.UTC().Format(time.RFC3339)}</span>` (exact format per UI-SPEC §HTMX Interaction Pattern: `2026-05-14T14:42:38Z`)
    - `HealthzHandler(pinger Pinger)` returns 200 JSON `{"status":"ok","db":"ok"}` when `pinger.Ping(ctx2sec)` is nil; 503 JSON `{"status":"degraded","db":"down"}` otherwise (RESEARCH Code Examples block — verbatim shape)
    - `IndexHandler()` returns `http.HandlerFunc` that sets `Content-Type: text/html; charset=utf-8` and calls `templates.Index().Render(r.Context(), w)`
    - `DemoTimeHandler(now func() time.Time)` returns `http.HandlerFunc` that sets `Content-Type: text/html; charset=utf-8` and calls `templates.TimeFragment(now()).Render(r.Context(), w)`
    - `NewRouter(pinger Pinger, staticDir string)` returns a chi router with middleware stack in this exact order per RESEARCH Pattern 2 + D-08:
      1. `chi/v5/middleware.RequestID` — NO, use OUR `RequestIDMiddleware` (Pitfall: chi's RequestID uses a base32 string not a UUID; UI-SPEC + RESEARCH say UUIDv4)
      2. `chi/v5/middleware.RealIP`
      3. `web.SlogLoggerMiddleware(slog.Default())`
      4. `chi/v5/middleware.Recoverer`
      Then routes: `GET /healthz` → HealthzHandler, `GET /` → IndexHandler, `GET /demo/time` → DemoTimeHandler with `time.Now` as the clock, `GET /static/*` → `http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))`.
  </behavior>
  <action>
    Write `templates/layout.templ` matching UI-SPEC §Base Layout Contract byte-for-byte for structural classes (`min-h-screen bg-white text-slate-900 antialiased`, `mx-auto max-w-5xl px-4 sm:px-6`, etc.). Footer text: `Phase 1 · Walking skeleton`. The layout takes a `title` parameter (`templ Layout(title string)`) and accepts children. The `<title>` element interpolates `{title}`. The `<head>` includes `<link rel="stylesheet" href="/static/tailwind.css">` and `<meta charset="utf-8">` + viewport. `<script src="/static/htmx.min.js" defer>` lives at the bottom of `<body>`.

    Write `templates/index.templ` consuming the layout. Inside, place an `<h1>` styled per UI-SPEC §Typography Display row, a muted subtitle paragraph with the exact copy from Copywriting Contract ("Go + HTMX foundation. Sign-in and the Tablos workflow ship in later phases."), then `@ui.Card(nil)` containing the demo section. Inside the Card: an `<h2 class="text-xl font-semibold leading-snug">HTMX demo</h2>`, a `<p class="mt-2 text-base text-slate-600">` with the helper copy, then a flex row containing the `@ui.Button(...)` (use the exact ButtonProps from UI-SPEC §Component Library Contract canonical block: `Label: "Fetch server time"`, `Variant: ui.ButtonVariantDefault`, `Tone: ui.ButtonToneSolid`, `Size: ui.SizeMD`, `Type: "button"`, `Attrs: templ.Attributes{"hx-get": "/demo/time", "hx-target": "#demo-out", "hx-swap": "innerHTML", "hx-indicator": "#demo-spinner"}`) and a `<span id="demo-spinner" class="htmx-indicator text-sm text-slate-600">Loading…</span>`. Below the flex row: `<div id="demo-out" class="mt-4 text-base text-slate-600">No time fetched yet.</div>`. Imports: `backend/internal/web/ui`.

    Write `templates/fragments.templ`: `templ TimeFragment(t time.Time) { <span class="text-slate-900">{ t.UTC().Format(time.RFC3339) }</span> }`. Import `time`.

    Write `internal/web/handlers.go` declaring `HealthzHandler`, `IndexHandler`, `DemoTimeHandler` per the behavior block. Per RESEARCH Code Examples for HealthzHandler use a 2-second `context.WithTimeout` around `pinger.Ping`. Imports: standard, plus `backend/templates`.

    Write `internal/web/router.go`: declare the `Pinger` interface and `NewRouter`. Middleware order per behavior block above. Routes registered with `chi.Router.Get`. Static file server: `r.Get("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir))).ServeHTTP)`. The `time.Now` passed to `DemoTimeHandler` is wrapped in a `func() time.Time { return time.Now() }` to honor the injected-clock signature from `<interfaces>`.

    Run `templ generate` and `go test ./internal/web/` to confirm all six handler tests pass.

    Do NOT introduce ANY additional routes, middlewares, or template helpers beyond what is specified. Phase 1 is a Walking Skeleton — resist expansion.
  </action>
  <verify>
    <automated>cd backend && templ generate && go build ./internal/web/ ./templates/ && go test ./internal/web/... -count=1</automated>
  </verify>
  <acceptance_criteria>
    - `templ generate` produces `layout_templ.go`, `index_templ.go`, `fragments_templ.go` without error
    - `go test ./internal/web/...` exits 0 — all six handler tests + the ui package tests pass
    - `TestIndex_RendersHxGet` confirms the index page renders the `ui-button-solid-default-md` class (proves it consumes the ui package, not raw Tailwind)
    - `TestDemoTime_Fragment` confirms the body matches the ISO-8601 UTC pattern and contains no `<html` tag
    - `index.templ` imports `backend/internal/web/ui` (verifiable: grep)
    - Router middleware order is RequestIDMiddleware → RealIP → SlogLoggerMiddleware → Recoverer (verifiable in `router.go` source order)
    - No raw `<button class="bg-blue-600 ...">` anywhere in templates/ (UI-SPEC: pages MUST NOT bypass the ui package)
  </acceptance_criteria>
  <done>Templates render via the ui package; all six failing tests from Plan 02 are now GREEN; the chi router serves the four endpoints in scope.</done>
</task>

<task type="auto">
  <name>Task 3: cmd/web/main.go — env loading, slog setup, pgxpool, server lifecycle, graceful shutdown</name>
  <files>backend/cmd/web/main.go</files>
  <read_first>
    - .planning/phases/01-foundation/01-RESEARCH.md (Pattern 5 graceful shutdown; Code Examples /healthz handler; Security Domain §Slow client → resource exhaustion — set ReadTimeout/WriteTimeout/IdleTimeout)
    - .planning/phases/01-foundation/01-CONTEXT.md (D-15, D-17, D-19, D-20)
    - backend/internal/web/router.go (just written — direct caller)
    - backend/internal/db/pool.go (just written — direct caller)
  </read_first>
  <action>
    Write `backend/cmd/web/main.go` as the production entrypoint:

    1. `package main` + `func main()`.
    2. Read env vars: `DATABASE_URL` (required — log fatal + exit 1 if empty), `PORT` (default "8080"), `ENV` (default "development"). Do NOT use any third-party env library — `os.Getenv` only (CONTEXT D-15 says `.env` is loaded by the dev wrapper, not by the binary; production injects real env). Note: per RESEARCH Open Question 1 and CONTEXT D-15 there's no in-binary `.env` parser — `just dev` exports `DATABASE_URL` for development.
    3. Build slog handler via `web.NewSlogHandler(env, os.Stdout)` and `slog.SetDefault(slog.New(handler))`.
    4. Create a background `context.Context` with `signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)` (Go 1.21+ idiom; cleaner than the manual `signal.Notify` channel pattern shown in RESEARCH Pattern 5 but functionally equivalent — use signal.NotifyContext).
    5. Open the pgxpool: `pool, err := db.NewPool(ctx, dsn)`. Log fatal on error.
    6. Build the router: `router := web.NewRouter(pool, "./static")`.
    7. Create `*http.Server` with: `Addr: ":" + port`, `Handler: router`, `ReadTimeout: 15 * time.Second`, `WriteTimeout: 15 * time.Second`, `IdleTimeout: 60 * time.Second` (per RESEARCH Security Domain entry on slow clients).
    8. Start the server in a goroutine: log on `ErrServerClosed` (info) vs other errors (fatal).
    9. Block on `<-ctx.Done()` (the signal-notify context).
    10. On wake: log "shutting down"; create a 10-second shutdown context `shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)`; call `srv.Shutdown(shutdownCtx)`; AFTER `Shutdown` returns, call `pool.Close()` explicitly per RESEARCH Pitfall 4 (NOT via `defer` — defer order during fatal-exit paths is unreliable). Log "shutdown complete" then return from main.

    Imports needed: standard library (`context`, `log/slog`, `net/http`, `os`, `os/signal`, `syscall`, `time`), `backend/internal/db`, `backend/internal/web`.

    Do NOT add any feature flags, dynamic config reload, or extra endpoints. Do NOT add a `/readyz` route (Phase 7 owns it per Deferred Ideas).
  </action>
  <verify>
    <automated>cd backend && go build ./cmd/web/ && grep -q 'signal.NotifyContext' cmd/web/main.go && grep -q 'srv.Shutdown' cmd/web/main.go && grep -q 'pool.Close' cmd/web/main.go && grep -q 'ReadTimeout' cmd/web/main.go && ! grep -q '/readyz' cmd/web/main.go</automated>
  </verify>
  <acceptance_criteria>
    - `go build ./cmd/web/` succeeds
    - `signal.NotifyContext` is used for signal handling
    - `srv.Shutdown` is called with a context bounded by 10s
    - `pool.Close()` is called explicitly AFTER `Shutdown` (not via defer) — Pitfall 4
    - HTTP server has explicit ReadTimeout, WriteTimeout, IdleTimeout
    - No `/readyz` route registered (Phase 7 scope)
    - On empty `DATABASE_URL`, the binary exits non-zero with a log line naming the missing env var (verifiable: `cd backend && DATABASE_URL='' go run ./cmd/web 2>&1 | grep -i DATABASE_URL` exits 0; manual or follow-up scripted test)
  </acceptance_criteria>
  <done>`cmd/web` binary builds, boots, accepts traffic, and shuts down gracefully.</done>
</task>

<task type="auto">
  <name>Task 4: cmd/worker/main.go — Phase 1 skeleton (boot, log ready, wait on signal, close pool, exit)</name>
  <files>backend/cmd/worker/main.go</files>
  <read_first>
    - .planning/phases/01-foundation/01-CONTEXT.md (D-03 — Phase 1 worker is a minimal skeleton; real job runtime is Phase 6)
    - backend/cmd/web/main.go (just written — mirror the slog + env + pool + signal-context pattern)
  </read_first>
  <action>
    Write `backend/cmd/worker/main.go` as the Phase 1 worker skeleton per D-03 ("a minimal binary that boots, connects to Postgres, logs 'worker ready', and exits cleanly on signal. Real job runtime is Phase 6").

    1. `package main` + `func main()`.
    2. Read `DATABASE_URL` (required) and `ENV` (default "development"). NO `PORT` — the worker doesn't serve HTTP.
    3. Build slog handler via `web.NewSlogHandler(env, os.Stdout)` and set as default. (Re-using `web.NewSlogHandler` is acceptable — it's a pure helper, not coupled to HTTP.)
    4. Open the pgxpool: `pool, err := db.NewPool(ctx, dsn)`. Log fatal on error.
    5. Log `slog.Info("worker ready")`. This is the load-bearing signal per D-03.
    6. Block on `signal.NotifyContext(ctx, SIGINT, SIGTERM)` until done.
    7. Log "shutting down", `pool.Close()`, log "shutdown complete", return.

    Do NOT add a job registry, queue, or any handler logic. Phase 6 will replace this file in full — keep it ≤ 60 lines including comments.
  </action>
  <verify>
    <automated>cd backend && go build ./cmd/worker/ && grep -q '"worker ready"' cmd/worker/main.go && grep -q 'signal.NotifyContext' cmd/worker/main.go && grep -q 'pool.Close' cmd/worker/main.go && wc -l cmd/worker/main.go | awk '$1<=80{exit 0} {exit 1}'</automated>
  </verify>
  <acceptance_criteria>
    - `go build ./cmd/worker/` succeeds
    - Binary logs `worker ready` after opening the pool (verifiable: `DATABASE_URL=... ./bin/worker &`, then `kill -INT $!`, expect log line)
    - Total file length ≤ 80 lines (signals "we did not implement Phase 6 by accident")
    - No job-queue libraries imported (`grep -E 'river|asynq|pg_notify' cmd/worker/main.go` returns empty)
  </acceptance_criteria>
  <done>Worker skeleton compiles, boots against the compose Postgres, logs its readiness, and shuts down cleanly. Phase 6 will replace this file in full.</done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 5 (checkpoint): Full Walking Skeleton run — browser + HTMX demo + graceful shutdown</name>
  <what-built>
    After Tasks 1–4, the full Walking Skeleton is functional: pgxpool wired, chi router with middleware stack, three endpoints (/, /healthz, /demo/time), static assets served, base+index+fragments templ pages consuming the ui design-system, both binaries building and running.
  </what-built>
  <files>backend/cmd/web (running binary), backend/cmd/worker (running binary), browser session, postgres container</files>
  <action>Human walks the live HTMX round-trip + graceful shutdown + worker boot. The agent does NOT execute these steps autonomously because they involve interactive browser interaction, live process supervision (Ctrl-C timing), and reversible filesystem edits to verify air's live-reload. The agent has already proven correctness via `go test ./...` in Tasks 1–4; this checkpoint is the end-to-end sanity that closes the Walking Skeleton.</action>
  <how-to-verify>
    From `backend/`:
    1. `just db-up` and confirm Postgres is healthy. Then `just migrate up`.
    2. In one terminal: `just styles-watch` (Tailwind builds `static/tailwind.css`). Confirm the file is non-empty after first compile.
    3. In a second terminal: `just dev` (air starts the web binary). Confirm logs show `pool open`, `listening on :8080`, no errors.
    4. `curl -s http://localhost:8080/healthz | jq` → returns `{"status":"ok","db":"ok"}` with HTTP 200.
    5. Stop the Postgres container (`just db-down`), wait ~10s, then `curl -is http://localhost:8080/healthz` → returns 503 with `{"status":"degraded","db":"down"}`. Bring DB back up.
    6. Open `http://localhost:8080/` in a browser. Verify:
       - Page title is "Xtablo — Foundation"
       - H1 "Xtablo" displays
       - Demo card shows "HTMX demo" heading and "Fetch server time" button
       - Click the button → HTML fragment with an ISO-8601 timestamp swaps into `#demo-out`
       - Browser DevTools console shows NO errors
       - Network panel shows the `/demo/time` request returned `Content-Type: text/html` and the response body is a `<span>` (NOT JSON, NOT a full page)
       - `htmx.min.js` is loaded from `/static/htmx.min.js` (NOT a CDN — verify via DevTools Network)
    7. Edit `internal/web/handlers.go` (add a no-op comment) → air rebuilds, browser refresh shows updated build. Edit `templates/index.templ` (change H1 to "Xtablo 2") → templ regenerates, air rebuilds, refresh shows the change. Revert both edits.
    8. In the `just dev` terminal: Ctrl-C. Confirm shutdown logs in order: `shutting down`, `shutdown complete`, and the process exits 0 within ~10s.
    9. Separately: `DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go run ./cmd/worker &`, expect log `worker ready` within 1s, then `kill -INT $!`, expect `shutdown complete` and exit 0.
    10. `go test ./...` — all tests pass.

    Expected outcome: every step succeeds without manual fixes. If any browser console error appears, or any test fails, or HTMX swap returns JSON instead of HTML, fix before approving.
  </how-to-verify>
  <verify>Human completes all 10 verification steps; observes HTMX swap, graceful shutdown logs, and `go test ./...` green.</verify>
  <done>User responds "approved" after all 10 steps pass without manual fixes.</done>
  <resume-signal>Type "approved" or describe issues encountered (specific failure step + observed behavior).</resume-signal>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Public HTTP → `/healthz` | Endpoint exposes db reachability boolean — accepted; no PII leaked |
| Public HTTP → `/static/*` | Static file server rooted at `./static`; path traversal must be prevented |
| Process → Postgres | DSN comes from env, never logged |
| OS signal → process | SIGINT/SIGTERM trigger graceful shutdown path |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-01-08 | T (Tampering) via path traversal | `/static/*` handler | mitigate | Use `http.FileServer(http.Dir(staticDir))` — Go's `http.Dir` rejects `..` traversal by default. Do NOT use raw `os.Open`. (RESEARCH Security Domain entry on file traversal.) |
| T-01-09 | I (Information disclosure) | slog middleware logging request bodies / headers | mitigate | `SlogLoggerMiddleware` logs only: method, path, status, duration_ms, request_id. NEVER logs Authorization header, Cookie header, or request body. Allowlist-only (RESEARCH §Security Domain V7). |
| T-01-10 | D (DoS via slow client) | `cmd/web` http.Server | mitigate | Explicit ReadTimeout=15s, WriteTimeout=15s, IdleTimeout=60s on `http.Server` (RESEARCH §Security Domain). |
| T-01-11 | D (panic crashes process) | Any handler | mitigate | `chi/v5/middleware.Recoverer` is registered after the logging middleware so panics are logged with request_id then converted to 500. |
| T-01-12 | I (DSN leak via slog fatal) | `cmd/web` startup | mitigate | On `db.NewPool` error, log the error type and message but DO NOT log the DSN. Sanitize: `slog.Error("db connect failed", "err", err)` without an `"dsn"` attribute. |
| T-01-13 | T (XSS via `/demo/time` echo) | TimeFragment | mitigate | templ auto-escapes interpolated values by default (`{ t.UTC().Format(...) }`). No `templ.Raw` used anywhere. Fragment content is server-generated (time.Now), not user-controlled — defense-in-depth. |
</threat_model>

<verification>
- `cd backend && templ generate && go test ./... -count=1` exits 0
- `cd backend && go build ./cmd/web ./cmd/worker` produces `bin/web` (if `just build`) or compiles cleanly via `go build`
- Checkpoint Task 5 walks the full Walking Skeleton: dev loop, browser interaction, graceful shutdown, worker boot
- No `chi/v5/middleware.Logger` import anywhere (Pitfall 6): `grep -r 'middleware.Logger' backend/` returns empty
- No runtime CDN reference anywhere in served code (Codex concerns #4 and #5): `grep -r 'unpkg.com\|cdn\.' backend/internal backend/templates backend/cmd backend/static/htmx.min.js` returns empty in the `internal/`, `templates/`, and `cmd/` paths. The justfile's bootstrap-time download URLs (Tailwind GitHub release URL, HTMX unpkg URL) are the explicit allowed exceptions and live ONLY in `backend/justfile`.
- No raw `<button class="bg-blue-`anywhere in templates/: `grep -r 'class="bg-blue-' backend/templates/` returns empty (pages consume `ui.Button`, never inline)
</verification>

<success_criteria>
1. All 6 handler tests from Plan 02 are GREEN — the RED gate is turned over
2. `TestPool_Connects` passes when `DATABASE_URL` is set against compose Postgres
3. Both `cmd/web` and `cmd/worker` build and run against the compose Postgres
4. Browser end-to-end: load `/`, click "Fetch server time", see HTMX swap a server-rendered timestamp into `#demo-out` with no console errors
5. Graceful shutdown closes pgxpool AFTER http.Server.Shutdown returns (Pitfall 4)
6. All UI rendered through `internal/web/ui` components — no raw Tailwind utility-class soup leaks into `templates/`
7. ALL FOUND-01..04 success criteria from ROADMAP are functionally exercised by the checkpoint
</success_criteria>

<output>
After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md` documenting:
- Final middleware order in `NewRouter` (verifies it matches CONTEXT D-08)
- Number of test functions now GREEN
- Final binary sizes for `cmd/web` and `cmd/worker`
- Any observations from the live-reload + HTMX browser interaction
- README work remaining for Plan 04
</output>
