diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 63c7aae..0e197d6 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -37,6 +37,13 @@
**User-in-loop:** Approve directory layout (`backend/cmd/web`, `backend/cmd/worker`, `backend/internal/...`) and pick the migration tool (`goose` vs `golang-migrate`).
+**Plans:** 4 plans
+Plans:
+- [ ] 01-01-PLAN.md — Project scaffold: Go module, compose, justfile, env, sqlc/air/tailwind config, bootstrap migration
+- [ ] 01-02-PLAN.md — RED gate: failing handler tests + `internal/web/ui` design-system package (Button/Card/Badge)
+- [ ] 01-03-PLAN.md — GREEN slice: pgxpool, chi router, middleware, templates, cmd/web + cmd/worker, end-to-end HTMX demo
+- [ ] 01-04-PLAN.md — README quickstart + clean-clone onboarding walkthrough (closes FOUND-05)
+
### Phase 2: Authentication
**Goal:** A new user can sign up, log in with email + password, and stay logged in across requests using server-managed sessions.
**Mode:** mvp
diff --git a/.planning/phases/01-foundation/01-01-PLAN.md b/.planning/phases/01-foundation/01-01-PLAN.md
new file mode 100644
index 0000000..4a78673
--- /dev/null
+++ b/.planning/phases/01-foundation/01-01-PLAN.md
@@ -0,0 +1,339 @@
+---
+phase: 01-foundation
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - backend/go.mod
+ - backend/go.sum
+ - backend/.env.example
+ - backend/.gitignore
+ - backend/.air.toml
+ - backend/compose.yaml
+ - backend/justfile
+ - backend/sqlc.yaml
+ - backend/tailwind.input.css
+ - backend/migrations/0001_init.sql
+ - backend/internal/db/doc.go
+ - backend/internal/session/doc.go
+ - backend/internal/tablos/doc.go
+ - backend/internal/tasks/doc.go
+ - backend/internal/files/doc.go
+autonomous: false
+requirements:
+ - FOUND-01
+ - FOUND-02
+ - FOUND-05
+tags:
+ - go
+ - scaffold
+ - tooling
+ - postgres
+ - tailwind
+ - htmx
+
+must_haves:
+ truths:
+ - "Running `just bootstrap` from a fresh clone installs goose, templ, sqlc, air CLIs and downloads the Tailwind standalone binary + htmx.min.js into backend/bin and backend/static"
+ - "Running `just db-up` (or `podman compose up -d postgres`) starts a healthy Postgres 16 container reachable at localhost:5432"
+ - "Running `just migrate up` against the running Postgres applies migration 0001_init.sql cleanly and `just migrate status` shows it applied"
+ - "Running `just --list` shows recipes for at least: bootstrap, dev, db-up, db-down, migrate, generate, styles-watch, test, lint, build"
+ - "Tailwind input CSS declares @source globs that include backend/templates/**/*.templ and backend/internal/web/**/*.{templ,go} so JIT does not purge classes used only in templ files"
+ - ".env.example documents DATABASE_URL, PORT, ENV and .env is gitignored"
+ artifacts:
+ - path: backend/go.mod
+ provides: Go module declaration with required deps (chi, templ, pgx, goose, uuid)
+ contains: "module backend"
+ - path: backend/compose.yaml
+ provides: podman/docker compose service for postgres:16-alpine with pg_isready healthcheck
+ contains: "postgres:16-alpine"
+ - path: backend/justfile
+ provides: task runner recipes (bootstrap, dev, db-up, db-down, migrate, generate, styles-watch, test, lint, build)
+ contains: "bootstrap"
+ - path: backend/migrations/0001_init.sql
+ provides: goose-formatted no-op bootstrap migration exercising the migration pipeline
+ contains: "+goose Up"
+ - path: backend/sqlc.yaml
+ provides: sqlc configuration pointing at migrations/ as schema source, internal/db/queries as queries source, emitting pgx/v5 code
+ contains: "sql_package"
+ - path: backend/tailwind.input.css
+ provides: Tailwind v4 entry CSS with @source globs for templ files and @import lines for ui/*.css
+ contains: "@source"
+ - path: backend/.air.toml
+ provides: air live-reload config for Go + templ
+ contains: "templ generate"
+ - path: backend/.env.example
+ provides: Required env keys (DATABASE_URL, PORT, ENV)
+ contains: "DATABASE_URL"
+ - path: backend/.gitignore
+ provides: Excludes bin/, tmp/, .env, static/tailwind.css, static/htmx.min.js, *_templ.go
+ contains: ".env"
+ - path: backend/internal/db/doc.go
+ provides: db package marker (Phase 1 placeholder; pgxpool wiring lands in Plan 03)
+ contains: "package db"
+ - path: backend/internal/session/doc.go
+ provides: session package placeholder (populated Phase 2)
+ contains: "package session"
+ - path: backend/internal/tablos/doc.go
+ provides: tablos package placeholder (populated Phase 3)
+ contains: "package tablos"
+ - path: backend/internal/tasks/doc.go
+ provides: tasks package placeholder (populated Phase 4)
+ contains: "package tasks"
+ - path: backend/internal/files/doc.go
+ provides: files package placeholder (populated Phase 5)
+ contains: "package files"
+ key_links:
+ - from: backend/justfile (bootstrap recipe)
+ to: backend/bin/tailwindcss + backend/static/htmx.min.js
+ via: curl download to pinned versions
+ pattern: "curl.*tailwindcss"
+ - from: backend/justfile (migrate recipe)
+ to: backend/migrations/0001_init.sql
+ via: goose CLI with GOOSE_MIGRATION_DIR=migrations
+ pattern: "GOOSE_MIGRATION_DIR=migrations"
+ - from: backend/sqlc.yaml
+ to: backend/migrations/
+ via: schema path so sqlc reads the same SQL files goose runs
+ pattern: "schema:.*migrations"
+ - from: backend/tailwind.input.css
+ to: backend/templates/**/*.templ and backend/internal/web/**/*.{templ,go}
+ via: "@source" directives
+ pattern: "@source.*templ"
+---
+
+
+Lay down the entire `backend/` project skeleton: Go module, directory layout, local Postgres compose file, justfile, env example, gitignore, air config, sqlc config, Tailwind input CSS, the no-op goose bootstrap migration, and `doc.go` placeholders for every `internal/` package locked in CONTEXT D-01/D-02.
+
+Purpose: Every subsequent plan in this phase (and the rest of v1) depends on a working `cd backend && just bootstrap && just db-up && just migrate up` loop. This plan delivers that loop without yet writing any Go application code or handlers — pure infra.
+
+Output: `backend/` exists with a valid Go module, all directory placeholders, working `just` recipes, a healthy compose-managed Postgres, and one applied bootstrap migration.
+
+
+
+@/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
+
+
+
+@.planning/STATE.md
+@.planning/ROADMAP.md
+@.planning/REQUIREMENTS.md
+@.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
+
+# Reference scaffolds — read for shape, do NOT copy wholesale (they use pnpm Tailwind which we are dropping)
+@go-backend/justfile
+@go-backend/compose.yaml
+@go-backend/sqlc.yaml
+
+
+# Phase 1 has no upstream Go interfaces to honor. The shape this plan establishes:
+# - Module path: `backend`
+# - Tailwind input file path (relative to backend/): `tailwind.input.css`
+# - Tailwind output path: `static/tailwind.css`
+# - Goose migration dir: `migrations/`
+# - sqlc schema source: `migrations/`
+# - sqlc queries source: `internal/db/queries/` (empty in Phase 1)
+# - sqlc output: `internal/db/sqlc/` (empty in Phase 1)
+# - Air tmp dir: `tmp/`
+# - Local DB DSN (dev only, in justfile): `postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable`
+
+
+
+
+
+
+ Task 1: Initialize Go module and pin runtime dependencies
+ backend/go.mod, backend/go.sum
+
+ - .planning/phases/01-foundation/01-RESEARCH.md (Standard Stack section — pinned versions are authoritative)
+ - .planning/phases/01-foundation/SKELETON.md (locked versions table)
+
+ From repo root, create `backend/`, then `cd backend && go mod init backend`. Pin the following runtime deps with `go get` at the exact versions from RESEARCH.md Standard Stack: `github.com/go-chi/chi/v5@v5.2.5`, `github.com/a-h/templ@v0.3.1020`, `github.com/jackc/pgx/v5@v5.9.2`, `github.com/pressly/goose/v3@v3.27.1`, `github.com/google/uuid@v1.6.0`. Do NOT use any version other than those listed. Run `go mod tidy` so `go.sum` is populated. The Go directive should match the developer's installed Go (1.22 minimum; existing `go-backend/go.mod` uses 1.26 — use the same `go` line if available, otherwise `1.22`). Do not import or fetch `air`, `sqlc`, `templ` CLI, `goose` CLI as Go module deps in `go.mod` — those are installed via `go install` from the `just bootstrap` recipe (Task 6).
+
+ cd backend && go mod verify && grep -q 'github.com/go-chi/chi/v5 v5.2.5' go.mod && grep -q 'github.com/a-h/templ v0.3.1020' go.mod && grep -q 'github.com/jackc/pgx/v5 v5.9.2' go.mod && grep -q 'github.com/pressly/goose/v3 v3.27.1' go.mod && grep -q 'github.com/google/uuid v1.6.0' go.mod
+
+
+ - `backend/go.mod` declares `module backend`
+ - All five runtime deps pinned at the versions above (verified by grep, not just presence)
+ - `go mod verify` exits 0
+ - `go.sum` is populated and committed
+
+ `backend/go.mod` and `backend/go.sum` exist, module is `backend`, all five deps pinned at locked versions, `go mod verify` passes.
+
+
+
+ Task 2: Create directory skeleton and per-package doc.go placeholders
+ backend/internal/db/doc.go, backend/internal/session/doc.go, backend/internal/tablos/doc.go, backend/internal/tasks/doc.go, backend/internal/files/doc.go, backend/internal/db/queries/.gitkeep, backend/internal/db/sqlc/.gitkeep, backend/templates/.gitkeep, backend/migrations/.gitkeep, backend/bin/.gitkeep, backend/static/.gitkeep
+
+ - .planning/phases/01-foundation/01-CONTEXT.md (D-01, D-02 — directory layout is non-negotiable)
+ - .planning/phases/01-foundation/SKELETON.md (Directory layout section)
+
+ Create the directory tree exactly as locked in D-01/D-02 and mirrored in SKELETON.md. For each placeholder Go package directory, write a minimal `doc.go` containing only a package clause and a one-line comment naming the phase that will populate it. Example shape (do NOT add anything else): a `// Package files is a placeholder; the upload/storage implementation lands in Phase 5.` line above `package files`. The five required doc.go files cover packages: `db`, `session`, `tablos`, `tasks`, `files` — per D-02 ("creates the directory skeleton for all internal/<domain> packages (empty doc.go is fine) so later phases drop files in without restructuring"). Use `.gitkeep` empty files for directories that have no Go files yet (`migrations/`, `templates/`, `bin/`, `static/`, `internal/db/queries/`, `internal/db/sqlc/`). Do NOT create `internal/web/` here — Plan 02 owns that package (handler tests + ui design-system). Do NOT create `cmd/web/` or `cmd/worker/` here — Plan 03 owns those entrypoints. This task strictly delivers the placeholder skeleton.
+
+ cd backend && for p in db session tablos tasks files; do test -f "internal/$p/doc.go" && grep -q "^package $p$" "internal/$p/doc.go" || { echo "missing or wrong package: $p"; exit 1; }; done && test -d migrations && test -d templates && test -d bin && test -d static && go build ./internal/...
+
+
+ - Each of `internal/{db,session,tablos,tasks,files}/doc.go` exists and declares the correct package
+ - `internal/db/queries/`, `internal/db/sqlc/`, `migrations/`, `templates/`, `bin/`, `static/` directories exist (via `.gitkeep`)
+ - `go build ./internal/...` succeeds (empty packages compile)
+ - `internal/web/`, `cmd/web/`, `cmd/worker/` are NOT created by this task
+
+ Directory skeleton matches D-01/D-02 verbatim; placeholder Go packages compile.
+
+
+
+ Task 3: Compose file, .env.example, .gitignore, bootstrap goose migration
+ backend/compose.yaml, backend/.env.example, backend/.gitignore, backend/migrations/0001_init.sql
+
+ - .planning/phases/01-foundation/01-RESEARCH.md (Code Examples section — compose.yaml and .env.example are verbatim references; D-15, D-20)
+ - go-backend/compose.yaml (existing healthy reference; strip dev seed mounts as the research note instructs)
+
+ Write `backend/compose.yaml` based on the verbatim block in RESEARCH.md Code Examples. Required: service name `postgres`, image `postgres:16-alpine`, container name `xtablo-backend-postgres`, env `POSTGRES_DB=xtablo`, `POSTGRES_USER=xtablo`, `POSTGRES_PASSWORD=xtablo`, port mapping `5432:5432`, named volume `postgres_data`, `pg_isready -U xtablo -d xtablo` healthcheck (interval 5s, timeout 5s, retries 10), `restart: unless-stopped`. Do NOT include the seed-mount volume from `go-backend/compose.yaml` — Phase 1 has nothing to seed. Write `backend/.env.example` with exactly the three keys locked in D-15: `DATABASE_URL=postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable`, `PORT=8080`, `ENV=development`. Add a brief comment line above each key. Write `backend/.gitignore` covering: `bin/` (Tailwind binary + go-installed CLIs may live here), `tmp/` (air rebuild output), `.env`, `.env.local`, `static/tailwind.css` (generated), `static/htmx.min.js` (vendored on bootstrap, not committed), `*_templ.go` (templ-generated; per RESEARCH Pitfall 1 these are never committed), `internal/db/sqlc/*.go` (generated, except keep `.gitkeep`). Write `backend/migrations/0001_init.sql` exactly per RESEARCH.md Code Examples — goose annotations `-- +goose Up` and `-- +goose Down`, each section containing `SELECT 1;`. The file MUST start with `-- +goose Up` on the first non-comment line (goose parser requirement).
+
+ cd backend && grep -q 'postgres:16-alpine' compose.yaml && grep -q 'pg_isready' compose.yaml && grep -q '^DATABASE_URL=' .env.example && grep -q '^PORT=' .env.example && grep -q '^ENV=' .env.example && grep -q '^\.env$' .gitignore && grep -q 'tailwind.css' .gitignore && grep -q 'htmx.min.js' .gitignore && grep -q '_templ\.go' .gitignore && grep -q '\-\- +goose Up' migrations/0001_init.sql && grep -q '\-\- +goose Down' migrations/0001_init.sql
+
+
+ - `compose.yaml` declares postgres:16-alpine with a pg_isready healthcheck
+ - `.env.example` contains exactly DATABASE_URL, PORT, ENV (per D-15)
+ - `.gitignore` excludes bin/, tmp/, .env, generated CSS, vendored htmx, *_templ.go
+ - `0001_init.sql` is a valid goose file with `-- +goose Up` / `-- +goose Down` markers and a no-op body
+ - No seed-mount carryover from go-backend/compose.yaml
+
+ Local Postgres can be brought up via `podman compose up -d postgres` (verified in Task 7); the bootstrap migration file is goose-parseable.
+
+
+
+ Task 4: sqlc config, Tailwind input CSS, air config
+ backend/sqlc.yaml, backend/tailwind.input.css, backend/.air.toml
+
+ - .planning/phases/01-foundation/01-RESEARCH.md (Pitfall 3, Pitfall 5, Pitfall 7, Code Examples for .air.toml)
+ - .planning/phases/01-foundation/01-UI-SPEC.md (Tailwind Configuration Contract — the @source globs and @import lines are exact)
+ - go-backend/sqlc.yaml (reference shape only)
+
+ Write `backend/sqlc.yaml` with `version: "2"`, one `sql` entry: `engine: postgresql`, `schema: "migrations"` (Pitfall 7 — point sqlc at the goose migrations dir so the schema sources match), `queries: "internal/db/queries"`, output `gen.go` with `package: "sqlc"`, `out: "internal/db/sqlc"`, `sql_package: "pgx/v5"`, `emit_json_tags: false`, `emit_interface: false`. Write `backend/tailwind.input.css` verbatim per UI-SPEC §Tailwind Configuration Contract: line 1 `@import "tailwindcss";`, then `@source "../templates/**/*.templ";`, `@source "../internal/web/**/*.templ";`, `@source "../internal/web/**/*.go";`, then `@import "../internal/web/ui/base.css";`, `@import "../internal/web/ui/button.css";`, `@import "../internal/web/ui/card.css";`, `@import "../internal/web/ui/badge.css";`. Note: the four `ui/*.css` files are created in Plan 02. They do not exist yet when this plan runs — that's fine, Tailwind v4 will error only when invoked. The CSS file pins the contract; the styles compile step is exercised by Plan 03's verify (after Plan 02 lands the ui CSS files). Write `backend/.air.toml` per RESEARCH.md Code Examples: `root = "."`, `tmp_dir = "tmp"`, `[build]` with `cmd = "templ generate && go build -o ./tmp/web ./cmd/web"`, `bin = "./tmp/web"`, `include_ext = ["go", "templ"]`, `exclude_dir = ["tmp", "bin", "static", ".git", "internal/db/sqlc"]`, `exclude_regex = [".*_templ\\.go$"]`, `delay = 200`, `stop_on_error = true`. Per RESEARCH Open Question 2 the recommendation is two-terminal workflow (Tailwind watch separate from air); do NOT add Tailwind to air's pre_cmd.
+
+ cd backend && grep -q '^version: "2"' sqlc.yaml && grep -q 'engine: postgresql' sqlc.yaml && grep -q 'schema: "migrations"' sqlc.yaml && grep -q 'sql_package: "pgx/v5"' sqlc.yaml && grep -q '^@import "tailwindcss";' tailwind.input.css && grep -c '@source' tailwind.input.css | awk '$1>=3{exit 0} {exit 1}' && grep -q 'internal/web/ui/button.css' tailwind.input.css && grep -q 'include_ext = \["go", "templ"\]' .air.toml && grep -q 'exclude_regex = \[".*_templ' .air.toml && grep -q 'templ generate' .air.toml
+
+
+ - sqlc.yaml schema source points at `migrations/` (Pitfall 7)
+ - tailwind.input.css declares ≥3 @source globs covering templ files and internal/web Go files (Pitfall 3)
+ - tailwind.input.css @imports the four `internal/web/ui/*.css` files exactly as UI-SPEC requires
+ - .air.toml watches `.go` + `.templ`, excludes generated `*_templ.go` (Pitfall 5), runs `templ generate` before build
+ - Tailwind is NOT added to air's pre_cmd (RESEARCH Open Question 2 recommendation)
+
+ sqlc, Tailwind, and air configs match UI-SPEC + RESEARCH-mandated shapes; later plans can rely on these contracts.
+
+
+
+ Task 5: justfile with bootstrap, db, migrate, generate, dev, test, lint, build recipes
+ backend/justfile
+
+ - .planning/phases/01-foundation/01-RESEARCH.md (Code Examples section — justfile recipes are the canonical reference; Environment Availability table for tool install commands and pinned versions)
+ - .planning/phases/01-foundation/01-CONTEXT.md (D-05, D-11, D-12, D-13)
+ - go-backend/justfile (shape only — strip pnpm Tailwind, retain podman + templ + sqlc + air pattern)
+
+ Write `backend/justfile` modeled on the RESEARCH Code Examples block. Required at minimum, these recipes (names exact):
+
+ - `default` → `@just --list`
+ - `bootstrap` → mkdir bin, then `go install` the four CLI tools at the exact RESEARCH-pinned versions: `github.com/pressly/goose/v3/cmd/goose@v3.27.1`, `github.com/a-h/templ/cmd/templ@v0.3.1020`, `github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1`, `github.com/air-verse/air@v1.65.1`. Then `curl -sSL` the Tailwind v4 standalone binary into `./bin/tailwindcss` (use the platform-detection pattern `tailwindcss-$(uname -s | tr A-Z a-z)-$(uname -m)`) and `chmod +x`. Then `curl -sSL` the HTMX v2.x `htmx.min.js` from `unpkg.com/htmx.org@2/dist/htmx.min.js` into `./static/htmx.min.js` (vendored once at bootstrap; subsequently committed-via-gitignore semantics — per CONTEXT D-10 we vendor, never CDN). Pin the Tailwind version in a recipe-local variable at the top of the justfile (e.g., `tailwind_version := "v4.0.0"` — planner: use the latest 4.x stable available at bootstrap time; document the version in a comment).
+ - `db-up` → `podman compose up -d postgres`
+ - `db-down` → `podman compose down`
+ - `migrate cmd="status"` → invokes `goose` with `GOOSE_DRIVER=postgres`, `GOOSE_DBSTRING` set to the dev DSN, `GOOSE_MIGRATION_DIR=migrations`, passing `{{cmd}}` (so `just migrate up`, `just migrate down`, `just migrate status` all work)
+ - `generate` → runs `templ generate`, then `sqlc generate`, then `./bin/tailwindcss -i tailwind.input.css -o static/tailwind.css`
+ - `styles-watch` → `./bin/tailwindcss -i tailwind.input.css -o static/tailwind.css --watch`
+ - `dev` → depends on `db-up`, runs `just generate` once, then exports `DATABASE_URL` for the dev DSN and runs `air -c .air.toml`. Document in a top-of-file comment that the developer is expected to run `just styles-watch` in a second terminal (RESEARCH Open Question 2 — two-terminal workflow).
+ - `test` → runs `just generate` then `go test ./...`
+ - `lint` → runs `go vet ./...` and `gofmt -l .` (failing if any file would be reformatted)
+ - `build` → runs `just generate`, then `go build -o bin/web ./cmd/web`, then `go build -o bin/worker ./cmd/worker`
+
+ Document at top of justfile in a comment block: project name, podman/docker portability note (CONTEXT D-11 — `compose.yaml` works under either; README will spell out the alternative `docker compose` invocation), and the two-terminal `dev` + `styles-watch` workflow. Do NOT add a recipe to download `goose`/`templ`/`sqlc`/`air` outside of `bootstrap` — keep one install path.
+
+ Note: `build` will fail until Plan 03 ships `cmd/web/main.go` and `cmd/worker/main.go`. That is expected and acceptable for Plan 01's verification — we only assert the justfile parses and `just --list` enumerates the required recipes.
+
+ cd backend && just --list 2>/dev/null | grep -E '^\s+(bootstrap|db-up|db-down|migrate|generate|styles-watch|dev|test|lint|build)\b' | wc -l | awk '$1>=9{exit 0} {exit 1}'
+
+
+ - `just --list` enumerates at least: bootstrap, db-up, db-down, migrate, generate, styles-watch, dev, test, lint, build
+ - `bootstrap` recipe installs goose v3.27.1, templ v0.3.1020, sqlc v1.31.1, air v1.65.1 at exact pinned versions (verifiable by grep on the justfile)
+ - `bootstrap` recipe downloads htmx.min.js into static/ from unpkg.com (NOT a CDN reference at runtime — D-10)
+ - `migrate` recipe sets GOOSE_DRIVER, GOOSE_DBSTRING, GOOSE_MIGRATION_DIR
+ - `dev` recipe runs `db-up`, `generate`, then `air`
+ - No pnpm / npm / node references anywhere in the justfile (D-12)
+
+ justfile parses, exposes required recipes at the right names, and only references tools approved by CONTEXT/RESEARCH.
+
+
+
+ Task 6 (checkpoint): Bootstrap + bring up Postgres + apply migration end-to-end
+ The full scaffold from Tasks 1–5: Go module, directory skeleton, compose.yaml, .env.example, .gitignore, sqlc.yaml, tailwind.input.css, .air.toml, justfile, bootstrap migration. After this checkpoint a developer can run the four-command bootstrap loop and see a healthy DB with one migration applied.
+ backend/bin/tailwindcss, backend/static/htmx.min.js, postgres container, goose_db_version table
+ Human verifies that the scaffold from Tasks 1–5 actually bootstraps end-to-end. The CLI work is automated in `just bootstrap`; the human gate confirms the tool downloads + compose health + first migration are real, not aspirational. Steps in how-to-verify are executed by the user; the agent does not run them autonomously because `just bootstrap` performs network downloads and `podman compose up -d postgres` mutates host state — both require explicit user authorization.
+
+ Run, from the repo root, in order:
+ 1. `cd backend && just bootstrap` — installs goose/templ/sqlc/air, downloads `bin/tailwindcss`, downloads `static/htmx.min.js`. Confirm all four `go install` commands succeed and both files exist (`test -x bin/tailwindcss && test -s static/htmx.min.js`).
+ 2. `just db-up` — starts the postgres container. Wait for `podman compose ps` to show the service as healthy (5–15s).
+ 3. `just migrate up` — exit 0; output mentions `0001_init`.
+ 4. `just migrate status` — shows `0001_init.sql` as Applied with a timestamp.
+ 5. `psql "postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable" -c "SELECT version FROM goose_db_version ORDER BY id DESC LIMIT 1;"` — returns `1`.
+
+ Expected outcome: postgres container healthy, goose_db_version table contains version 1, no errors in any step.
+
+ If any step fails: investigate before approving. Common failure modes per RESEARCH Pitfalls 1, 2, 7.
+
+ Human walks the five-step bootstrap loop and confirms each step exits successfully; `just migrate status` shows `0001_init.sql` Applied.
+ User responds "approved" after observing all five steps succeed on their machine.
+ Type "approved" or describe issues (e.g., "Tailwind binary URL 404", "goose migration parse error").
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Internet → `just bootstrap` downloads | Tailwind binary + htmx.min.js are pulled from public URLs during bootstrap; integrity not yet pinned by hash in Phase 1 |
+| Local filesystem → goose / sqlc CLIs | Installed via `go install` from public proxy.golang.org |
+| Local Postgres container ↔ host | Bound to `localhost:5432` only via compose port mapping |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-01-01 | T (Tampering) | `static/htmx.min.js` vendored via curl from unpkg | accept | Phase 1 scope: vendoring from a public CDN is an explicit decision (D-10 forbids runtime CDN, not bootstrap-time fetch). Subresource integrity hashes deferred to Phase 7 along with deploy hardening. Rationale: low-value local-dev attack surface; CSS/HTMX assets do not handle secrets in Phase 1. |
+| T-01-02 | T (Tampering) | `bin/tailwindcss` binary downloaded via curl | accept | Same rationale as T-01-01. Tailwind is local-dev tooling only; not shipped in any production image in Phase 1. Hash pinning revisited at Phase 7 (deploy) where reproducible CI bootstrap matters. |
+| T-01-03 | I (Information disclosure) | `.env` containing dev DSN | mitigate | `.gitignore` excludes `.env`; only `.env.example` (with placeholder/dev-only credentials) is committed. Dev DSN uses the literal `xtablo:xtablo` credential set, which is harmless for local-only bound containers. |
+| T-01-04 | I (Information disclosure) | `goose_db_version` table | accept | Standard goose metadata table; contains only migration version numbers and timestamps. No user data. |
+| T-01-05 | D (Denial of service) | postgres container port 5432 exposed to host | accept | Container binds to all interfaces by default in compose; mitigation deferred — developer machine is the trust zone. If the developer is on a hostile network, they can bind `127.0.0.1:5432:5432` in a follow-up; not in Phase 1 scope. |
+
+
+
+- `cd backend && go mod verify` exits 0
+- `cd backend && go build ./internal/...` succeeds (placeholder packages compile)
+- `cd backend && just --list` enumerates all required recipes
+- `cd backend && just bootstrap && just db-up && just migrate up && just migrate status` runs cleanly end-to-end (checkpoint Task 6)
+- Manual: open `backend/compose.yaml`, `backend/.env.example`, `backend/.gitignore`, `backend/sqlc.yaml`, `backend/.air.toml`, `backend/tailwind.input.css`, `backend/justfile`, `backend/migrations/0001_init.sql` and verify each matches its acceptance criteria above
+
+
+
+1. `backend/` directory tree exactly matches the layout locked in CONTEXT D-01/D-02 and reproduced in SKELETON.md
+2. Go module declares `module backend` with all five runtime deps pinned at RESEARCH-Standard-Stack versions
+3. `just bootstrap` installs all four CLI tools at pinned versions and vendors `bin/tailwindcss` + `static/htmx.min.js`
+4. `just db-up` starts a healthy postgres:16-alpine container reachable at localhost:5432
+5. `just migrate up` applies the no-op bootstrap migration; `just migrate status` shows it as Applied
+6. No Node/npm/pnpm artifacts anywhere in `backend/` (D-12)
+7. No CDN references in any committed file (D-10)
+
+
+
diff --git a/.planning/phases/01-foundation/01-02-PLAN.md b/.planning/phases/01-foundation/01-02-PLAN.md
new file mode 100644
index 0000000..9be6255
--- /dev/null
+++ b/.planning/phases/01-foundation/01-02-PLAN.md
@@ -0,0 +1,424 @@
+---
+phase: 01-foundation
+plan: 02
+type: execute
+wave: 2
+depends_on: ["01-01"]
+files_modified:
+ - backend/internal/web/ui/tokens.go
+ - backend/internal/web/ui/variants.go
+ - backend/internal/web/ui/helpers.go
+ - backend/internal/web/ui/base.css
+ - backend/internal/web/ui/button.templ
+ - backend/internal/web/ui/button.css
+ - backend/internal/web/ui/card.templ
+ - backend/internal/web/ui/card.css
+ - backend/internal/web/ui/badge.templ
+ - backend/internal/web/ui/badge.css
+ - backend/internal/web/ui/ui_test.go
+ - backend/internal/web/handlers_test.go
+ - backend/internal/db/pool_test.go
+autonomous: true
+requirements:
+ - FOUND-01
+ - FOUND-02
+ - FOUND-03
+ - FOUND-04
+tags:
+ - go
+ - testing
+ - templ
+ - design-system
+ - htmx
+
+must_haves:
+ truths:
+ - "After this plan lands, `go test ./internal/...` runs and the listed handler tests FAIL with 'undefined: ' or equivalent — proving the test scaffold targets the right functions/signatures before Plan 03 implements them (RED step of MVP)"
+ - "The `internal/web/ui` design-system package compiles standalone and its `ui_test.go` smoke tests PASS — each shipped variant of Button, Card, Badge renders without panicking and emits the expected root CSS class"
+ - "The UI component package mirrors the enum surface of `go-backend/internal/web/ui/` for Size, ButtonVariant, ButtonTone, BadgeVariant — future phases extend these enums' CSS without redefining them"
+ - "Test files declare the exact handler function signatures and templ component names that Plan 03 will implement, so Plan 03 is a fill-in not an exploration"
+ artifacts:
+ - path: backend/internal/web/ui/variants.go
+ provides: Size, ButtonVariant, ButtonTone, BadgeVariant enums + class-builder functions + normalizers
+ contains: "ButtonVariantDefault"
+ - path: backend/internal/web/ui/tokens.go
+ provides: Semantic token constants (e.g. TokenPrimary, TokenDanger) declared but minimally used in Phase 1
+ contains: "package ui"
+ - path: backend/internal/web/ui/button.templ
+ provides: Button templ component accepting ButtonProps{Label, Variant, Tone, Size, Type, Attrs}
+ contains: "ButtonProps"
+ - path: backend/internal/web/ui/button.css
+ provides: CSS rules for `.ui-button-solid-default-md` and the base `.ui-button` class
+ contains: ".ui-button"
+ - path: backend/internal/web/ui/card.templ
+ provides: Card templ component accepting children (slate-50 panel + slate-200 border)
+ contains: "templ Card"
+ - path: backend/internal/web/ui/card.css
+ provides: CSS rules for `.ui-card`
+ contains: ".ui-card"
+ - path: backend/internal/web/ui/badge.templ
+ provides: Badge templ component accepting Label + Variant (info / success / warning / danger)
+ contains: "BadgeProps"
+ - path: backend/internal/web/ui/badge.css
+ provides: CSS rules for `.ui-badge-{info,success,danger}` (warning declared in enum but no CSS in Phase 1)
+ contains: ".ui-badge"
+ - path: backend/internal/web/ui/base.css
+ provides: Resets, focus-ring base, html/body defaults imported into tailwind.input.css
+ contains: "html"
+ - path: backend/internal/web/ui/ui_test.go
+ provides: Smoke tests rendering each Phase-1 variant; asserts root CSS class appears in output
+ contains: "TestButton"
+ - path: backend/internal/web/handlers_test.go
+ provides: Failing tests for healthz OK + down, index hx-get presence, demo/time fragment, RequestID header, slog handler switch
+ contains: "TestHealthz_OK"
+ - path: backend/internal/db/pool_test.go
+ provides: pgxpool integration test that SKIPS when DATABASE_URL is unset
+ contains: "t.Skip"
+ key_links:
+ - from: backend/internal/web/handlers_test.go
+ to: backend/internal/web/{router.go,handlers.go,middleware.go} (Plan 03)
+ via: tests reference exact function names — NewRouter, healthzHandler, indexHandler, demoTimeHandler, RequestIDMiddleware
+ pattern: "NewRouter|healthzHandler|indexHandler|demoTimeHandler"
+ - from: backend/internal/web/ui/button.templ
+ to: backend/internal/web/ui/button.css
+ via: emitted root class `ui-button-solid-default-md`
+ pattern: "ui-button-solid-default-md"
+ - from: backend/internal/web/ui/ui_test.go
+ to: ButtonProps / CardProps / BadgeProps types declared in this plan
+ via: direct constructor calls
+ pattern: "ui.ButtonProps"
+---
+
+
+Land the RED step of the Walking Skeleton (failing handler + db tests targeting exact function signatures Plan 03 will implement) AND the `internal/web/ui` design-system package (Button / Card / Badge with their CSS files, enum surface, and passing smoke tests).
+
+Purpose: Plan 03's job becomes "make the failing tests pass and render the index page via these components" rather than "design the package, then build the page, then write tests." Test-targeted signatures eliminate exploration; the UI package eliminates inline Tailwind in handlers.
+
+Output: 13 files split between `internal/web/ui/` (10 files, all GREEN — package compiles, its own tests pass) and the failing-test scaffold (`internal/web/handlers_test.go`, `internal/db/pool_test.go`). The handler tests reference yet-unwritten code from Plan 03 — they are intentionally RED right now.
+
+
+
+@/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
+
+
+
+@.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
+
+# Reference implementation for the ui package shape (READ carefully — DO NOT copy without re-keying class names)
+@go-backend/internal/web/ui/variants.go
+@go-backend/internal/web/ui/button.templ
+@go-backend/internal/web/ui/card.templ
+@go-backend/internal/web/ui/badge.templ
+
+
+# Contracts Plan 03 will implement against. Defining them here, in tests, locks Plan 03's API surface.
+
+## Handler signatures (backend/internal/web)
+
+```go
+// NewRouter returns a fully configured chi.Router. The Pinger interface allows
+// test injection of a stub for /healthz coverage without spinning up Postgres.
+type Pinger interface {
+ Ping(ctx context.Context) error
+}
+
+func NewRouter(pinger Pinger, staticDir string) http.Handler
+
+// Individual handlers exported for direct httptest invocation.
+func HealthzHandler(pinger Pinger) http.HandlerFunc
+func IndexHandler() http.HandlerFunc // renders templates.Index via templ
+func DemoTimeHandler(now func() time.Time) http.HandlerFunc // injected clock for deterministic tests
+
+// Middleware
+func RequestIDMiddleware(next http.Handler) http.Handler
+func SlogLoggerMiddleware(logger *slog.Logger) func(http.Handler) http.Handler
+
+// Slog handler selection (in package web — pure function, no globals)
+func NewSlogHandler(env string, w io.Writer) slog.Handler
+```
+
+## DB package signatures (backend/internal/db)
+
+```go
+func NewPool(ctx context.Context, dsn string) (*pgxpool.Pool, error)
+```
+
+## UI package signatures (backend/internal/web/ui)
+
+```go
+type Size string
+const (
+ SizeSM Size = "sm"
+ SizeMD Size = "md"
+ SizeLG Size = "lg"
+)
+
+type ButtonVariant string
+const (
+ ButtonVariantDefault ButtonVariant = "default"
+ ButtonVariantNeutral ButtonVariant = "neutral"
+ ButtonVariantWarning ButtonVariant = "warning"
+ ButtonVariantSuccess ButtonVariant = "success"
+ ButtonVariantDanger ButtonVariant = "danger"
+)
+
+type ButtonTone string
+const (
+ ButtonToneSolid ButtonTone = "solid"
+ ButtonToneSoft ButtonTone = "soft"
+)
+
+type BadgeVariant string
+const (
+ BadgeVariantInfo BadgeVariant = "info"
+ BadgeVariantWarning BadgeVariant = "warning"
+ BadgeVariantSuccess BadgeVariant = "success"
+ BadgeVariantDanger BadgeVariant = "danger"
+)
+
+type ButtonProps struct {
+ Label string
+ Variant ButtonVariant
+ Tone ButtonTone
+ Size Size
+ Type string // "button" / "submit"; defaults to "button" if empty
+ Attrs templ.Attributes // pass-through for hx-* etc.
+}
+
+type BadgeProps struct {
+ Label string
+ Variant BadgeVariant
+}
+
+// Templ components — match these names exactly
+templ Button(props ButtonProps)
+templ Card(attrs templ.Attributes) // children via templ children
+templ Badge(props BadgeProps)
+```
+
+
+
+
+
+
+ Task 1: UI package enums, helpers, base CSS
+ backend/internal/web/ui/tokens.go, backend/internal/web/ui/variants.go, backend/internal/web/ui/helpers.go, backend/internal/web/ui/base.css
+
+ - .planning/phases/01-foundation/01-UI-SPEC.md (Component Library Contract — enum names and normalizer pattern are non-negotiable)
+ - go-backend/internal/web/ui/variants.go (reference shape; mirror the enum/normalizer surface)
+
+
+ - Importing `backend/internal/web/ui` from a Go test compiles successfully
+ - `ui.SizeMD`, `ui.ButtonVariantDefault`, `ui.ButtonToneSolid`, `ui.BadgeVariantInfo` are exported constants of the right types
+ - `ui.NormalizedSize("")` returns `ui.SizeMD` (zero-value safe default)
+ - `ui.NormalizedSize("md")` returns `ui.SizeMD`
+ - `ui.NormalizedButtonVariant("")` returns `ui.ButtonVariantDefault`
+ - `ui.NormalizedButtonTone("")` returns `ui.ButtonToneSolid`
+ - `ui.NormalizedBadgeVariant("")` returns `ui.BadgeVariantInfo`
+ - `ui.ButtonClass(ButtonVariantDefault, ButtonToneSolid, SizeMD)` returns `"ui-button ui-button-solid-default-md"`
+ - `ui.BadgeClass(BadgeVariantInfo)` returns `"ui-badge ui-badge-info"`
+
+
+ Write `tokens.go`: declare a minimal set of semantic-token string constants (`TokenPrimary`, `TokenNeutral`, `TokenWarning`, `TokenSuccess`, `TokenDanger`) — these mirror the reference package's surface but are not heavily consumed in Phase 1; they exist so later phases can extend without restructuring. Package clause: `package ui`.
+
+ Write `variants.go`: declare exactly the types and constants from the `` block above (Size, ButtonVariant, ButtonTone, BadgeVariant + their constants). Add `NormalizedSize`, `NormalizedButtonVariant`, `NormalizedButtonTone`, `NormalizedBadgeVariant` functions: each accepts the corresponding type and returns the safe default for zero-value input (per UI-SPEC: "normalizer pattern required for every enum"). Add class-builder functions `ButtonClass(variant ButtonVariant, tone ButtonTone, size Size) string` and `BadgeClass(variant BadgeVariant) string` returning class strings exactly as listed in the behavior block. Defaults applied via normalizers before string assembly.
+
+ Write `helpers.go`: a small `mergeAttrs(base, override templ.Attributes) templ.Attributes` helper (for use inside the templ components in Task 2) that copies base then overlays override keys. Keep it ~10 lines.
+
+ Write `base.css`: minimal global resets and a focus-ring base class. Per UI-SPEC §"Accessibility Floor": ensure `:focus-visible` has a visible ring. Use plain CSS (no `@apply`) so it works under Tailwind v4 standalone with `@source` scanning Go files. Include `html { -webkit-text-size-adjust: 100%; }` and an `*, *::before, *::after { box-sizing: border-box; }` reset. Keep under 30 lines.
+
+ Do NOT use `templ.Raw` anywhere. Do NOT import anything from `cmd/web` or other domain packages — `ui` has no upward dependencies.
+
+
+ cd backend && go build ./internal/web/ui/ && go vet ./internal/web/ui/
+
+
+ - All four files compile under `go build ./internal/web/ui/`
+ - `variants.go` declares every enum constant listed in the `` block (verifiable: `grep -c 'ButtonVariant\(Default\|Neutral\|Warning\|Success\|Danger\)' variants.go` ≥ 5)
+ - Each enum has a `Normalized*` function returning the safe default for zero value
+ - `ButtonClass` produces exactly `"ui-button ui-button-solid-default-md"` for (Default, Solid, MD) — asserted by Task 4's tests
+ - `helpers.go` exports `mergeAttrs` (used by Task 2)
+
+ UI enum surface and helpers compile; class strings match the contract in ``.
+
+
+
+ Task 2: UI templ components — Button, Card, Badge — and their CSS
+ backend/internal/web/ui/button.templ, backend/internal/web/ui/button.css, backend/internal/web/ui/card.templ, backend/internal/web/ui/card.css, backend/internal/web/ui/badge.templ, backend/internal/web/ui/badge.css
+
+ - .planning/phases/01-foundation/01-UI-SPEC.md (Component Library Contract — exact rendered HTML for Button shown in §HTMX Interaction Pattern; canonical Button usage block)
+ - go-backend/internal/web/ui/button.templ (reference shape — do NOT copy class soup verbatim; UI-SPEC defines the Phase 1 class names)
+ - go-backend/internal/web/ui/card.templ
+ - go-backend/internal/web/ui/badge.templ
+
+
+ - `@ui.Button(ui.ButtonProps{Label: "x", Type: "button"})` rendered to a `bytes.Buffer` produces HTML containing class `ui-button-solid-default-md` and the label "x"
+ - `@ui.Button(..., Attrs: templ.Attributes{"hx-get": "/demo/time"})` renders an `hx-get="/demo/time"` attribute on the `
+
+ Write `button.templ`: declare `templ Button(props ButtonProps)`. Inside, compute `class` from `ui.ButtonClass(NormalizedButtonVariant(props.Variant), NormalizedButtonTone(props.Tone), NormalizedSize(props.Size))`. Render a `
+
+ cd backend && templ generate && go build ./internal/web/ui/ && grep -q '\.ui-button-solid-default-md' internal/web/ui/button.css && grep -q '\.ui-card' internal/web/ui/card.css && grep -q '\.ui-badge-info' internal/web/ui/badge.css && grep -q '\.ui-badge-success' internal/web/ui/badge.css && grep -q '\.ui-badge-danger' internal/web/ui/badge.css
+
+
+ - `templ generate` produces `button_templ.go`, `card_templ.go`, `badge_templ.go` with no errors
+ - `go build ./internal/web/ui/` succeeds with the generated files present
+ - Each CSS file declares exactly the variants enumerated above (info/success/danger for badge; warning intentionally absent)
+ - Button component renders `type="button"` when `Type` field is empty
+ - Card component accepts and passes through `templ.Attributes`
+ - Pages MUST NOT need to know about the `.ui-*` class names — they consume the components only
+
+ UI primitives compile to Go, CSS files cover the Phase 1 variant set, and component HTML matches UI-SPEC's canonical render.
+
+
+
+ Task 3: UI package smoke tests (ui_test.go)
+ backend/internal/web/ui/ui_test.go
+
+ - .planning/phases/01-foundation/01-UI-SPEC.md (Component Library Contract — `ui_test.go` requirements: render each Phase-1 variant, assert root class appears, no panic)
+ - backend/internal/web/ui/button.templ (just written in Task 2)
+ - backend/internal/web/ui/card.templ
+ - backend/internal/web/ui/badge.templ
+
+
+ - `TestButton_DefaultSolidMD` renders `@ui.Button(ui.ButtonProps{Label: "Fetch server time"})` to a buffer, asserts the output contains `class="ui-button ui-button-solid-default-md"` and the literal "Fetch server time" and `type="button"`
+ - `TestButton_PassesThroughAttrs` renders a Button with `Attrs: templ.Attributes{"hx-get": "/demo/time", "hx-target": "#demo-out"}`, asserts both attributes appear in output
+ - `TestCard_RendersChildren` renders `@ui.Card(nil) {
x
}`, asserts output contains `x
`
+ - `TestBadge_InfoVariant` renders `@ui.Badge(BadgeProps{Label: "OK", Variant: BadgeVariantInfo})`, asserts output contains `class="ui-badge ui-badge-info"` and `OK`
+ - `TestBadge_SuccessVariant` renders `BadgeVariantSuccess`, asserts `ui-badge-success`
+ - `TestBadge_ZeroValueDefaultsToInfo` renders with empty Variant, asserts `ui-badge-info` (normalizer behavior)
+ - `TestButtonClass_String` asserts `ui.ButtonClass(ui.ButtonVariantDefault, ui.ButtonToneSolid, ui.SizeMD) == "ui-button ui-button-solid-default-md"`
+
+
+ Write `ui_test.go` with `package ui` (internal — so it can call unexported helpers if needed; but only public API is required by the behavior list). Use `bytes.Buffer` + `templ.Component.Render(ctx, &buf)` to capture output, then `strings.Contains` for assertions. Each test follows the standard table-driven Go pattern or a simple `t.Run` per case. Use `context.Background()` as the render context.
+
+ Required imports: `bytes`, `context`, `strings`, `testing`, `github.com/a-h/templ`.
+
+ Do NOT use any third-party test library — stdlib `testing` only (RESEARCH §Validation Architecture).
+
+
+ cd backend && templ generate && go test ./internal/web/ui/ -count=1
+
+
+ - `go test ./internal/web/ui/` exits 0 with all listed test names present in `-v` output
+ - Each test asserts at minimum: (a) the expected root CSS class name appears in rendered HTML, (b) the expected literal label/content appears
+ - `TestButtonClass_String` passes — proves the class-string contract Plan 03 relies on
+ - Zero-value-defaults test passes — proves normalizers are wired correctly
+
+ The ui package is fully GREEN: it compiles, generates, and all smoke tests pass. This becomes the design-system foundation for Plan 03's page rendering and every later phase.
+
+
+
+ Task 4: Failing handler tests (handlers_test.go) and pgxpool integration test
+ backend/internal/web/handlers_test.go, backend/internal/db/pool_test.go
+
+ - .planning/phases/01-foundation/01-RESEARCH.md (Code Examples §`/healthz` handler; §Pattern 4 RequestID; §Pattern 3 slog handler switch)
+ - .planning/phases/01-foundation/01-VALIDATION.md (Per-Task Verification Map — exact test names referenced)
+ - .planning/phases/01-foundation/01-UI-SPEC.md (HTMX Interaction Pattern — index page must contain `hx-get="/demo/time"`)
+
+
+ The following tests MUST be authored. They will FAIL during Plan 02's verify (because Plan 03's handlers don't exist yet). That failure is the RED step. Plan 03's GREEN step is making them pass.
+
+ In `internal/web/handlers_test.go` (package `web`):
+ - `TestHealthz_OK`: builds a `httptest.NewRecorder` + `httptest.NewRequest("GET", "/healthz", nil)`, calls `web.HealthzHandler(stubPinger{err: nil})`. Asserts `200`, `Content-Type: application/json`, body contains `"status":"ok"` and `"db":"ok"`.
+ - `TestHealthz_Down`: same shape but `stubPinger{err: errors.New("conn refused")}`. Asserts `503`, body contains `"status":"degraded"` and `"db":"down"`.
+ - `TestIndex_RendersHxGet`: GET `/` through `web.NewRouter(stubPinger{}, "./static")`. Asserts status 200, `Content-Type: text/html`, body contains `hx-get="/demo/time"`, `hx-target="#demo-out"`, `ui-button-solid-default-md` (proving the page consumes the ui package), and the literal label "Fetch server time" (UI-SPEC copywriting contract).
+ - `TestDemoTime_Fragment`: GET `/demo/time`. Asserts status 200, `Content-Type: text/html`, body matches ISO-8601 UTC pattern (regex `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`), body does NOT contain `
+
+ Write both test files. Stub types live in the same file as the tests that use them (no separate helpers file in Phase 1).
+
+ Required imports (handlers_test.go): `bytes`, `context`, `encoding/json`, `errors`, `net/http`, `net/http/httptest`, `regexp`, `strings`, `testing`. Use Go's stdlib `regexp` for the UUID + timestamp patterns.
+
+ These tests reference `web.NewRouter`, `web.HealthzHandler`, `web.NewSlogHandler`, `web.Pinger` interface, all of which Plan 03 introduces. The test file's `package web` declaration ensures it compiles as part of the package — if those symbols don't exist, `go test ./internal/web/` will fail with `undefined: NewRouter` etc. That is the desired RED state at end of Plan 02.
+
+ Do NOT add `// +build` constraints. Do NOT skip the failing tests with `t.Skip` — they must remain RED until Plan 03.
+
+
+ cd backend && test -f internal/web/handlers_test.go && test -f internal/db/pool_test.go && grep -c '^func Test' internal/web/handlers_test.go | awk '$1>=6{exit 0} {exit 1}' && grep -q 'TestPool_Connects' internal/db/pool_test.go && grep -q 't.Skip' internal/db/pool_test.go && (cd internal/web && ! go test -count=1 -run 'TestHealthz_OK' . 2>&1 | grep -q '^ok')
+
+
+ - `handlers_test.go` contains ≥6 `Test*` functions covering the behaviors listed
+ - `pool_test.go` contains `TestPool_Connects` with a `t.Skip` branch for missing `DATABASE_URL`
+ - Running `go test ./internal/web/` exits NON-ZERO at end of Plan 02 (handlers don't exist yet — this is the RED gate)
+ - Running `go test ./internal/db/` exits 0 (skips cleanly when `DATABASE_URL` unset) — the db package compiles because Plan 03 will add `pool.go`; until then this test file references `db.NewPool` which won't exist either. The verify command above only requires the FILE to exist with `t.Skip` content — Plan 03's verify proves the test runs green.
+ - Failure messages are informative (`undefined: web.NewRouter`) — this exact error is what Plan 03's executor will see and fix
+
+ Failing tests are committed. Plan 03 inherits a concrete, behavior-specified target list — no exploration needed.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Test process ↔ filesystem | Tests run from local checkout only; no external network |
+| `DATABASE_URL` env ↔ pool_test.go | Test reads env var; skips if absent — does NOT default to a hardcoded DSN |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-01-06 | I (Information disclosure) | `pool_test.go` logging | accept | Test logs use `t.Logf` only; never log full DSN. `t.Skip` message says "DATABASE_URL not set" — does not echo the value. |
+| T-01-07 | T (Tampering via test fixtures) | `stubPinger` and other in-test stubs | accept | Stubs are package-private to `_test.go` files; not reachable from production code (Go test files are excluded from non-test builds). |
+
+
+
+- `cd backend && go build ./internal/web/ui/` succeeds
+- `cd backend && templ generate && go test ./internal/web/ui/ -count=1` exits 0 (all ui smoke tests GREEN)
+- `cd backend && go test ./internal/web/` exits NON-ZERO with `undefined: NewRouter` (or similar) — confirms RED gate is set for Plan 03
+- `cd backend && go test ./internal/db/` exits 0 (TestPool_Connects skips cleanly) OR exits non-zero with `undefined: db.NewPool` — either is acceptable here since Plan 03 lands `pool.go`
+- All ten files under `internal/web/ui/` exist
+- All three test files exist and contain the required `Test*` functions
+- `internal/web/ui/ui_test.go` covers Button (default+attrs), Card, Badge (info+success+zero-value)
+
+
+
+1. `internal/web/ui/` package compiles, generates, and tests pass (GREEN)
+2. UI package mirrors the enum surface of `go-backend/internal/web/ui/` (Size, ButtonVariant, ButtonTone, BadgeVariant — all values present even if Phase 1 only renders a subset)
+3. UI package CSS files are pickable by Tailwind input CSS imports (already wired in Plan 01)
+4. `internal/web/handlers_test.go` declares the exact test names from VALIDATION.md and targets the exact handler signatures Plan 03 will implement
+5. `internal/db/pool_test.go` declares `TestPool_Connects` with a graceful skip when `DATABASE_URL` is unset
+6. RED gate established: `go test ./internal/web/` fails because production handlers don't exist yet — this is intentional and is the target Plan 03 fills in
+
+
+
diff --git a/.planning/phases/01-foundation/01-03-PLAN.md b/.planning/phases/01-foundation/01-03-PLAN.md
new file mode 100644
index 0000000..7e33450
--- /dev/null
+++ b/.planning/phases/01-foundation/01-03-PLAN.md
@@ -0,0 +1,406 @@
+---
+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 (the RED gate from Plan 02 is now turned over)"
+ - "`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)"
+ 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 `{ISO-8601-UTC}`
+ 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"
+---
+
+
+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.
+
+
+
+@/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
+
+
+
+@.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 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.
+```
+
+
+
+
+
+
+ Task 1: pgxpool wrapper + middleware (RequestID, slog) + slog handler switch
+ backend/internal/db/pool.go, backend/internal/web/middleware.go, backend/internal/web/slog.go
+
+ - 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)
+
+
+ - `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.
+
+
+ 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).
+
+
+ cd backend && templ generate && go build ./internal/db/ ./internal/web/ && go test ./internal/web/ -run 'TestRequestID_HeaderSet|TestSlog_HandlerSwitch' -count=1
+
+
+ - `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)
+
+ The three infrastructure files are GREEN; two of the six handler tests now pass; the pgxpool wrapper satisfies the integration test.
+
+
+
+ Task 2: Templates (layout, index, fragments) + handlers + router
+ backend/templates/layout.templ, backend/templates/index.templ, backend/templates/fragments.templ, backend/internal/web/handlers.go, backend/internal/web/router.go
+
+ - 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)
+
+
+ - `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 ``)
+ - `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 `
No time fetched yet.
`)
+ - `templates.TimeFragment(t time.Time)` renders `{t.UTC().Format(time.RFC3339)}` (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)))`.
+
+
+ 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 `` element interpolates `{title}`. The `` includes `` and `` + viewport. `