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) + + + +After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md` documenting: +- Pinned versions actually installed (Tailwind 4.x patch, HTMX 2.x patch chosen at bootstrap time) +- Any deviations from the RESEARCH-locked stack (there should be none) +- Path forward for Plan 02 (Wave 2) — handler tests + ui design-system package + 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 `