diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index e54be6f..15dc897 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -82,6 +82,12 @@ Plans:
**User-in-loop:** Approve the `tablos` table schema (ownership model, soft-delete vs hard-delete, slug strategy).
+**Plans:** 3 plans
+Plans:
+- [ ] 03-01-PLAN.md — Wave 0: migration 0003_tablos + sqlc queries + handlers_tablos_test.go RED scaffold + button.css danger/neutral variants
+- [ ] 03-02-PLAN.md — Vertical slice 1: dashboard list + inline-form create (HTMX OOB swap; TABLO-01, TABLO-02, TABLO-06)
+- [ ] 03-03-PLAN.md — Vertical slice 2: detail + inline edit (title/description) + inline-confirmation delete (TABLO-03, TABLO-04, TABLO-05, TABLO-06)
+
### Phase 4: Tasks (Kanban)
**Goal:** Inside a tablo, a user can run a kanban board — create, edit, move, reorder, and delete tasks across columns.
**Mode:** mvp
@@ -154,3 +160,4 @@ Plans:
---
*Roadmap created: 2026-05-14*
+*Phase 3 plans added: 2026-05-15*
diff --git a/.planning/STATE.md b/.planning/STATE.md
index fa36601..343c9f5 100644
--- a/.planning/STATE.md
+++ b/.planning/STATE.md
@@ -3,13 +3,13 @@ gsd_state_version: 1.0
milestone: v1.0
milestone_name: milestone
status: in_progress
-last_updated: "2026-05-14T21:36:00.262Z"
+last_updated: "2026-05-15T00:00:00.000Z"
progress:
total_phases: 7
completed_phases: 2
- total_plans: 11
+ total_plans: 14
completed_plans: 11
- percent: 100
+ percent: 79
---
# STATE
@@ -31,7 +31,7 @@ See: `.planning/PROJECT.md` (updated 2026-05-14)
|---|-------|--------|
| 1 | Foundation | ✓ Complete |
| 2 | Authentication | ✓ Complete — VERIFIED PASS (2026-05-14) |
-| 3 | Tablos CRUD | ○ Pending |
+| 3 | Tablos CRUD | ◆ Ready to execute — 3 plans (3 waves) |
| 4 | Tasks (Kanban) | ○ Pending |
| 5 | Files | ○ Pending |
| 6 | Background Worker | ○ Pending |
@@ -39,7 +39,7 @@ See: `.planning/PROJECT.md` (updated 2026-05-14)
## Active Phase
-**Phase 2: Authentication** — Verified PASS. All 7/7 plans complete, all AUTH-01..07 requirements satisfied. Phase 3: Tablos CRUD is next.
+**Phase 3: Tablos CRUD** — Planned. 3 plans in 3 sequential waves. Ready to execute. Requirements: TABLO-01..06 covered. Planned 2026-05-15.
## Verification Record
diff --git a/.planning/phases/03-tablos-crud/03-01-PLAN.md b/.planning/phases/03-tablos-crud/03-01-PLAN.md
new file mode 100644
index 0000000..55471e0
--- /dev/null
+++ b/.planning/phases/03-tablos-crud/03-01-PLAN.md
@@ -0,0 +1,313 @@
+---
+phase: 03-tablos-crud
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - backend/migrations/0003_tablos.sql
+ - backend/internal/db/queries/tablos.sql
+ - backend/internal/db/sqlc/tablos.sql.go
+ - backend/internal/db/sqlc/models.go
+ - backend/internal/web/handlers_tablos_test.go
+ - backend/internal/web/ui/button.css
+autonomous: true
+requirements:
+ - TABLO-01
+ - TABLO-02
+ - TABLO-03
+ - TABLO-04
+ - TABLO-05
+ - TABLO-06
+tags:
+ - go
+ - postgres
+ - sqlc
+ - goose
+ - integration-tests
+
+must_haves:
+ truths:
+ - "Migration 0003_tablos creates tablos table with all columns from D-02"
+ - "sqlc generates Tablo struct with pgtype.Text for nullable description and color"
+ - "handlers_tablos_test.go compiles and runs (all tests fail/skip because handlers do not exist yet)"
+ - "Button CSS gains .ui-button-solid-danger-md and .ui-button-soft-neutral-md rules"
+ artifacts:
+ - path: "backend/migrations/0003_tablos.sql"
+ provides: "tablos table + tablos_user_id_idx"
+ contains: "CREATE TABLE tablos"
+ - path: "backend/internal/db/queries/tablos.sql"
+ provides: "5 sqlc queries: ListTablosByUser, GetTabloByID, InsertTablo, UpdateTablo, DeleteTablo"
+ contains: "name: ListTablosByUser"
+ - path: "backend/internal/db/sqlc/tablos.sql.go"
+ provides: "Generated sqlc query bindings"
+ contains: "ListTablosByUser"
+ - path: "backend/internal/web/handlers_tablos_test.go"
+ provides: "Integration test scaffold for all TABLO-01..06 paths"
+ contains: "func TestTabloList"
+ - path: "backend/internal/web/ui/button.css"
+ provides: "Danger and neutral-soft button variants"
+ contains: "ui-button-solid-danger-md"
+ key_links:
+ - from: "backend/migrations/0003_tablos.sql"
+ to: "users(id)"
+ via: "REFERENCES users(id) ON DELETE CASCADE"
+ pattern: "REFERENCES users\\(id\\) ON DELETE CASCADE"
+ - from: "backend/internal/db/queries/tablos.sql"
+ to: "backend/internal/db/sqlc/tablos.sql.go"
+ via: "sqlc generate"
+ pattern: "ListTablosByUser"
+---
+
+
+Lay the data + test foundation for Phase 3 Tablos CRUD: the goose migration, sqlc query file, generated bindings, and the Wave 0 integration test scaffold that downstream plans will turn green. Add the two new button CSS variants so subsequent template work can render the delete/cancel buttons without inline styles.
+
+Purpose: Establish the schema, type-safe DB layer, and failing-test baseline before any handler code is written. Plans 02 and 03 turn this red into green.
+
+Output:
+- 0003_tablos.sql migration
+- tablos.sql query file + generated tablos.sql.go + extended models.go
+- handlers_tablos_test.go covering all TABLO-01..06 paths (RED, compiles, fails because handlers/routes do not yet exist)
+- button.css with danger + neutral-soft variants
+
+
+## Phase Goal
+
+The ROADMAP `**Goal:**` line is task-shaped ("A logged-in user can list, create, view, edit, and delete..."), not a user story. Recording derived user story for MVP_MODE; if mismatch matters, run `/gsd mvp-phase 03` to formalise.
+
+**As a** signed-in Xtablo user, **I want to** create, view, edit, and delete my own tablos through HTMX-driven flows, **so that** I can organise my work without leaving the dashboard or seeing full-page reloads.
+
+
+@/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/03-tablos-crud/03-CONTEXT.md
+@.planning/phases/03-tablos-crud/03-RESEARCH.md
+@.planning/phases/03-tablos-crud/03-PATTERNS.md
+@.planning/phases/03-tablos-crud/03-UI-SPEC.md
+@.planning/phases/03-tablos-crud/03-VALIDATION.md
+@backend/migrations/0002_auth.sql
+@backend/internal/db/queries/users.sql
+@backend/internal/db/sqlc/models.go
+@backend/internal/web/handlers_auth_test.go
+@backend/internal/web/testdb_test.go
+@backend/internal/web/ui/button.css
+@backend/internal/web/ui/variants.go
+
+
+
+
+Expected sqlc-generated Tablo struct (in backend/internal/db/sqlc/models.go):
+- ID uuid.UUID
+- UserID uuid.UUID
+- Title string
+- Description pgtype.Text (Valid + String)
+- Color pgtype.Text
+- CreatedAt pgtype.Timestamptz
+- UpdatedAt pgtype.Timestamptz
+
+Expected sqlc-generated Queries methods (backend/internal/db/sqlc/tablos.sql.go):
+- ListTablosByUser(ctx, userID uuid.UUID) ([]Tablo, error)
+- GetTabloByID(ctx, id uuid.UUID) (Tablo, error)
+- InsertTablo(ctx, InsertTabloParams) (Tablo, error) -- params: UserID, Title, Description (pgtype.Text), Color (pgtype.Text)
+- UpdateTablo(ctx, UpdateTabloParams) (Tablo, error) -- params: ID, Title, Description; sets updated_at = now()
+- DeleteTablo(ctx, id uuid.UUID) error
+
+Existing reusable infra (do NOT reimplement):
+- backend/internal/web/testdb_test.go: setupTestDB(t) — boots a per-test schema using goose.Up against `backend/migrations`, returns *pgxpool.Pool. Phase 3 migrations are picked up automatically by goose.
+- backend/internal/auth: RequireAuth middleware, Authed(ctx), Store
+- backend/internal/web/handlers_auth_test.go: pattern for newAuthedRequest helper, cookie+session+csrf wiring
+- backend/internal/web/ui/variants.go already declares ButtonVariantDanger, ButtonVariantNeutral, ButtonToneSoft — only the CSS classes are missing.
+
+
+
+
+
+
+ Task 1: Migration 0003_tablos.sql + sqlc query file + regenerate sqlc
+ backend/migrations/0003_tablos.sql, backend/internal/db/queries/tablos.sql, backend/internal/db/sqlc/tablos.sql.go, backend/internal/db/sqlc/models.go
+
+ backend/migrations/0002_auth.sql
+ backend/internal/db/queries/users.sql
+ backend/internal/db/sqlc/models.go
+ backend/internal/db/sqlc/users.sql.go
+ backend/sqlc.yaml
+ backend/justfile
+ .planning/phases/03-tablos-crud/03-CONTEXT.md (D-01, D-02, D-03)
+ .planning/phases/03-tablos-crud/03-RESEARCH.md (Migration + sqlc Queries sections)
+ .planning/phases/03-tablos-crud/03-PATTERNS.md (analog migration + queries blocks)
+
+
+ - Migration up creates tablos table with columns: id uuid PK DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, title text NOT NULL, description text NULL, color text NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now().
+ - Index tablos_user_id_idx on tablos(user_id).
+ - Migration down: DROP TABLE IF EXISTS tablos.
+ - sqlc generates Tablo struct matching the interfaces block above (pgtype.Text for description/color, pgtype.Timestamptz for created_at/updated_at).
+ - UpdateTablo SQL must SET updated_at = now() explicitly (Pitfall 7).
+
+
+ Create backend/migrations/0003_tablos.sql following the 0002_auth.sql header style ("-- migrations/0003_tablos.sql\n-- Phase 3: Tablos CRUD") with goose Up/Down sections. Schema per D-02 (CONTEXT.md): id uuid PK DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, title text NOT NULL, description text, color text, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(); CREATE INDEX tablos_user_id_idx ON tablos(user_id); Down drops tablos.
+
+ Create backend/internal/db/queries/tablos.sql with exactly five named queries matching the RESEARCH.md "sqlc Queries" code block:
+ - ListTablosByUser :many (WHERE user_id = $1 ORDER BY created_at DESC)
+ - GetTabloByID :one (WHERE id = $1)
+ - InsertTablo :one (INSERT user_id,title,description,color RETURNING all columns)
+ - UpdateTablo :one (SET title=$2, description=$3, updated_at=now() WHERE id=$1 RETURNING all columns) — do NOT include color (per RESEARCH "color not editable in Phase 3")
+ - DeleteTablo :exec (DELETE FROM tablos WHERE id = $1)
+
+ Use explicit column lists in SELECTs (no SELECT *), positional $N parameters, and the same -- name: header style as users.sql.
+
+ Run `just generate` (or `just sqlc` if the justfile separates them) to produce backend/internal/db/sqlc/tablos.sql.go and update backend/internal/db/sqlc/models.go. Do not hand-edit the generated files.
+
+
+ cd backend && just generate && grep -q "CREATE TABLE tablos" migrations/0003_tablos.sql && grep -q "tablos_user_id_idx" migrations/0003_tablos.sql && grep -q "name: ListTablosByUser" internal/db/queries/tablos.sql && grep -q "name: UpdateTablo" internal/db/queries/tablos.sql && grep -q "updated_at = now()" internal/db/queries/tablos.sql && grep -q "func (q \\*Queries) ListTablosByUser" internal/db/sqlc/tablos.sql.go && grep -q "type Tablo struct" internal/db/sqlc/models.go && go build ./...
+
+
+ - File backend/migrations/0003_tablos.sql exists and contains "CREATE TABLE tablos", "REFERENCES users(id) ON DELETE CASCADE", "tablos_user_id_idx", "-- +goose Up", "-- +goose Down".
+ - File backend/internal/db/queries/tablos.sql exists and contains exactly the 5 query names (ListTablosByUser, GetTabloByID, InsertTablo, UpdateTablo, DeleteTablo).
+ - tablos.sql contains "ORDER BY created_at DESC" (D-03) and "updated_at = now()" (Pitfall 7) in UpdateTablo.
+ - tablos.sql UpdateTablo does NOT include color (Phase 3 scope per RESEARCH §sqlc Queries note).
+ - backend/internal/db/sqlc/tablos.sql.go exists with exported methods ListTablosByUser, GetTabloByID, InsertTablo, UpdateTablo, DeleteTablo on *Queries.
+ - backend/internal/db/sqlc/models.go contains `type Tablo struct` with Description and Color fields typed as pgtype.Text.
+ - `go build ./...` from backend/ exits 0.
+
+ Migration committed, query file committed, sqlc-generated files committed, build green.
+
+
+
+ Task 2: handlers_tablos_test.go — RED integration test scaffold for TABLO-01..06
+ backend/internal/web/handlers_tablos_test.go
+
+ backend/internal/web/handlers_auth_test.go
+ backend/internal/web/testdb_test.go
+ backend/internal/web/router.go
+ backend/internal/web/csrf_test.go
+ backend/internal/db/sqlc/tablos.sql.go (from Task 1)
+ backend/internal/db/sqlc/models.go (Tablo struct shape)
+ .planning/phases/03-tablos-crud/03-VALIDATION.md (per-task verification map)
+ .planning/phases/03-tablos-crud/03-RESEARCH.md (Phase Requirements → Test Map)
+
+
+ File must compile and `go test` must execute every named test. All tests are expected to FAIL or panic for now (handlers/routes do not exist) — that is the RED state Plans 02 and 03 will fix. Each test must exist as a real Go function (no t.Skip stubs) so the failing assertions document the contract.
+
+ Tests required (function names exact, per 03-VALIDATION.md):
+ - TestTabloList — authed GET / shows current user's tablos, newest-first; only the calling user's rows present.
+ - TestTabloList_Empty — authed GET / with no tablos renders empty-state copy "No tablos yet" + "Create your first tablo to get started.".
+ - TestTabloCreate — authed POST /tablos with HX-Request:true and valid title/description/color: returns 200, body contains the new title; HX-Retarget header == "#tablos-list", HX-Reswap header == "afterbegin"; response body contains an OOB element `id="create-form-slot"` with hx-swap-oob="true"; DB row exists with user_id = caller. Sub-asserts cover TABLO-06: non-HTMX POST returns 303 to "/".
+ - TestTabloCreate_Validation — POST /tablos with empty title returns 422; response body contains "Title is required.".
+ - TestTabloDetail_Owner — GET /tablos/{id} as owner returns 200 with title rendered.
+ - TestTabloDetail_NonOwner — GET /tablos/{id} as a different authed user returns 404 (not 403).
+ - TestTabloDetail_InvalidID — GET /tablos/not-a-uuid returns 404.
+ - TestTabloUpdate — authed POST /tablos/{id} with new title/description: returns 200, body contains updated title; DB row title/updated_at refreshed.
+ - TestTabloDeleteConfirm — GET /tablos/{id}/delete-confirm returns 200 with body containing "Delete tablo?" and "Yes, delete".
+ - TestTabloDelete — authed POST /tablos/{id}/delete: DB row deleted; HTMX request gets 200 + HX-Redirect:/ header (or HX-Retarget #tablos-list on dashboard delete); non-HTMX gets 303 to /.
+
+
+ Create backend/internal/web/handlers_tablos_test.go in package web. Reuse setupTestDB (from testdb_test.go) and the CSRF/cookie helpers exactly as handlers_auth_test.go does — use a duplicated helper if needed (per STATE.md decision: "setupTestDB duplicated into web package test file"). Drive requests through NewRouter so the full middleware chain (RequestID, ResolveSession, csrf.Protect, RequireAuth) runs.
+
+ For each test:
+ 1. Call setupTestDB(t) to get *pgxpool.Pool. Build *sqlc.Queries.
+ 2. Insert two test users via Queries.InsertUser (reusing handlers_auth_test.go pattern).
+ 3. Build TablosDeps{Queries: q} and pass into web.NewRouter using the NEW signature: NewRouter(pinger, "./static", AuthDeps{...}, web.TablosDeps{Queries: q}, csrfKey, "test", "localhost"). Plans 02/03 must add TablosDeps to web package; if symbol does not yet exist when test compiles, expect a compile error — that is part of the RED state. To keep the test FILE compilable now while still being RED at runtime, declare a local stub: `var _ = web.TablosDeps{}` referencing the type Plan 02 introduces. If that prevents compilation, gate the test scaffold with the same TablosDeps stub introduced in Plan 02 Task 0 (interface skeleton).
+ 4. Acquire a session by POSTing /signup with HX-Request:false (same flow as handlers_auth_test.go), capture cookie + CSRF token via subsequent GET /.
+ 5. Issue the test's HTTP request via httptest.NewRecorder + router.ServeHTTP.
+ 6. Assert status, headers (HX-Retarget, HX-Reswap, HX-Redirect), and body substrings per the behavior block.
+ 7. Verify DB state by calling Queries.GetTabloByID / ListTablosByUser as appropriate.
+
+ Use t.Run subtests where one Test* name covers multiple assertions (e.g. TestTabloCreate covers HTMX-path + non-HTMX-path).
+
+ DO NOT implement handlers in this task — only the test scaffold. Tests are expected to fail until Plans 02 and 03 land.
+
+
+ cd backend && just generate && go test ./internal/web/... -run TestTablo -count=1 2>&1 | tee /tmp/test_out.log; grep -E "TestTabloList|TestTabloCreate|TestTabloDetail|TestTabloUpdate|TestTabloDelete" /tmp/test_out.log | grep -cE "(FAIL|RUN)" | xargs -I{} test {} -ge 10 && echo "scaffold present"
+
+
+ - File backend/internal/web/handlers_tablos_test.go exists in package web.
+ - grep -c '^func Test' backend/internal/web/handlers_tablos_test.go returns >= 10 (covers the 10 named tests above).
+ - Each of these substrings appears in the file: "TestTabloList(", "TestTabloList_Empty(", "TestTabloCreate(", "TestTabloCreate_Validation(", "TestTabloDetail_Owner(", "TestTabloDetail_NonOwner(", "TestTabloDetail_InvalidID(", "TestTabloUpdate(", "TestTabloDeleteConfirm(", "TestTabloDelete(".
+ - File references HX-Retarget and HX-Reswap as expected response headers in TestTabloCreate.
+ - File references http.StatusSeeOther (303) and HX-Redirect for the post-delete navigation assertion.
+ - File contains an ownership-cross-user assertion (creates user A's tablo, requests it as user B, expects 404) in TestTabloDetail_NonOwner.
+ - Test file's intended state is RED — Plan 01 acceptance is that the file compiles after Plan 02 introduces the TablosDeps stub. To unblock Plan 01 alone, if web.TablosDeps does not yet exist, this task additionally creates a one-line stub `type TablosDeps struct { Queries *sqlc.Queries }` at the top of handlers_tablos.go (new empty file) so the test file compiles.
+ - `go vet ./internal/web/...` exits 0.
+
+ Test scaffold compiles; running `go test ./internal/web/... -run TestTablo` lists all 10 tests as FAIL (no implementation yet).
+
+
+
+ Task 3: Add ui-button-solid-danger-md and ui-button-soft-neutral-md CSS rules
+ backend/internal/web/ui/button.css
+
+ backend/internal/web/ui/button.css
+ backend/internal/web/ui/variants.go
+ .planning/phases/03-tablos-crud/03-UI-SPEC.md (New Button Variants Required + Color sections)
+ .planning/phases/03-tablos-crud/03-PATTERNS.md (button.css analog block)
+
+
+ Append two new top-level rule sets to backend/internal/web/ui/button.css following the existing convention (NO CSS nesting — all pseudo-class selectors as separate top-level rules; class name format `.ui-button-{tone}-{variant}-{size}`).
+
+ Class `.ui-button-solid-danger-md`: background-color #b91c1c (red-700), color #ffffff, padding 0.5rem 1rem, border-radius 0.375rem, font-size 1rem, font-weight 600, display inline-flex, align-items center, min-height 44px (WCAG 2.5.5 per UI-SPEC). Hover rule: background-color #991b1b. Focus-visible rule: outline 2px solid #b91c1c, outline-offset 2px.
+
+ Class `.ui-button-soft-neutral-md`: background-color #f1f5f9 (slate-100), color #334155 (slate-700), padding 0.5rem 1rem, border-radius 0.375rem, font-size 1rem, font-weight 400, border 1px solid #e2e8f0, display inline-flex, align-items center, min-height 44px. Hover rule: background-color #e2e8f0. Focus-visible rule: outline 2px solid #64748b, outline-offset 2px.
+
+ Run `just generate` to rebuild static/tailwind.css (Pitfall 4: imported CSS passes through).
+
+
+ cd backend && grep -c "^\\.ui-button-solid-danger-md" internal/web/ui/button.css | xargs -I{} test {} -ge 1 && grep -c "^\\.ui-button-soft-neutral-md" internal/web/ui/button.css | xargs -I{} test {} -ge 1 && grep -q "#b91c1c" internal/web/ui/button.css && grep -q "#f1f5f9" internal/web/ui/button.css && grep -q "min-height: 44px" internal/web/ui/button.css && just generate && grep -q "ui-button-solid-danger-md" static/tailwind.css
+
+
+ - button.css contains top-level selectors `.ui-button-solid-danger-md`, `.ui-button-solid-danger-md:hover`, `.ui-button-solid-danger-md:focus-visible`, `.ui-button-soft-neutral-md`, `.ui-button-soft-neutral-md:hover`, `.ui-button-soft-neutral-md:focus-visible` (each as its own top-level rule — no `&` nesting).
+ - Hex codes #b91c1c, #991b1b, #f1f5f9, #e2e8f0, #334155, #64748b present.
+ - `min-height: 44px` appears for both new classes.
+ - After `just generate`, backend/static/tailwind.css contains the strings `ui-button-solid-danger-md` and `ui-button-soft-neutral-md`.
+ - `go build ./...` from backend/ exits 0 (sanity — no Go changes here but verifying nothing breaks).
+
+ CSS classes present, Tailwind regenerated, no other files modified.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Browser → web server | Untrusted form input crosses into POST /tablos handlers (Plans 02/03); Phase 1 sets the test surface but introduces no new boundary. |
+| web server → Postgres | SQL parameters from sqlc bindings are parameterised ($N) — no string concat. |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-03-01 | Information disclosure | tablos table FK to users | mitigate | ON DELETE CASCADE on user_id ensures orphan-free deletes; tablos_user_id_idx supports per-user query bounded by user_id (no cross-tenant scans). |
+| T-03-02 | Tampering | sqlc query layer | mitigate | All five queries use positional $N parameters (no string interpolation); sqlc generates type-safe Go bindings consumed by Plans 02/03. |
+| T-03-03 | DoS | tablos.title column | accept (mitigated by Plans 02/03) | Schema accepts arbitrary-length text; per-request length validation (<=255) happens in Plans 02/03 handlers. Documented here so reviewers know length-cap is layered above the DB. |
+| T-03-04 | Repudiation | tablos audit trail | accept | No audit log table in v1 per ROADMAP scope; created_at/updated_at provide minimum forensic signal. |
+| T-03-05 | Tampering | Integration test fixtures | mitigate | handlers_tablos_test.go uses setupTestDB per-test schema isolation (D-09 carryover from Phase 2) — no test pollutes another's data. |
+
+
+
+- `cd backend && just generate` runs without error.
+- `cd backend && go build ./...` exits 0.
+- `cd backend && go test ./internal/web/... -run TestTablo` lists all 10 TABLO tests, all FAIL (RED state expected).
+- `grep -c "^func Test" backend/internal/web/handlers_tablos_test.go` >= 10.
+- `psql` against the local compose DB shows tablos table with the 7 columns from D-02 after `just migrate up`.
+- New button CSS classes appear in backend/static/tailwind.css.
+
+
+
+1. backend/migrations/0003_tablos.sql exists and applies cleanly to a fresh DB via goose.
+2. backend/internal/db/queries/tablos.sql defines the 5 queries; sqlc regeneration succeeds.
+3. backend/internal/db/sqlc/tablos.sql.go and updated models.go are committed (generated, not hand-written).
+4. backend/internal/web/handlers_tablos_test.go compiles, declares all 10 named test functions, and currently fails at runtime (handlers missing — Plans 02/03 turn it green).
+5. backend/internal/web/ui/button.css declares the two new variants; Tailwind rebuild emits them into static/tailwind.css.
+6. No regression: existing `just test` suite (Phase 1/2 tests) still passes.
+
+
+
diff --git a/.planning/phases/03-tablos-crud/03-02-PLAN.md b/.planning/phases/03-tablos-crud/03-02-PLAN.md
new file mode 100644
index 0000000..d75554e
--- /dev/null
+++ b/.planning/phases/03-tablos-crud/03-02-PLAN.md
@@ -0,0 +1,324 @@
+---
+phase: 03-tablos-crud
+plan: 02
+type: execute
+wave: 2
+depends_on: [01]
+files_modified:
+ - backend/internal/web/handlers_tablos.go
+ - backend/templates/tablos.templ
+ - backend/templates/tablos_templ.go
+ - backend/templates/layout.templ
+ - backend/templates/layout_templ.go
+ - backend/templates/index.templ
+ - backend/templates/index_templ.go
+ - backend/internal/web/handlers.go
+ - backend/internal/web/router.go
+ - backend/cmd/web/main.go
+autonomous: true
+requirements:
+ - TABLO-01
+ - TABLO-02
+ - TABLO-06
+tags:
+ - go
+ - htmx
+ - templ
+ - chi
+ - csrf
+
+must_haves:
+ truths:
+ - "Signed-in user visiting GET / sees a dashboard titled 'Your Tablos' listing only their tablos newest-first"
+ - "Empty state renders 'No tablos yet' + 'Create your first tablo to get started.' when user has zero tablos"
+ - "Clicking 'New tablo' fetches an inline form into #create-form-slot via hx-get /tablos/new"
+ - "Submitting a valid POST /tablos via HTMX inserts the row, returns 200 with HX-Retarget: #tablos-list + HX-Reswap: afterbegin, prepends a new TabloCard, and clears #create-form-slot via OOB swap"
+ - "Submitting POST /tablos without HTMX inserts the row and 303-redirects to /"
+ - "Empty-title POST returns 422 with 'Title is required.' field error"
+ artifacts:
+ - path: "backend/internal/web/handlers_tablos.go"
+ provides: "TablosDeps, TablosListHandler, TablosNewHandler, TablosCreateHandler"
+ contains: "TablosCreateHandler"
+ - path: "backend/templates/tablos.templ"
+ provides: "TablosDashboard, TablosEmptyState, TabloCard, TabloCreateFormFragment, TabloCardWithOOBFormClear"
+ contains: "TabloCardWithOOBFormClear"
+ - path: "backend/internal/web/router.go"
+ provides: "GET / → TablosListHandler, GET /tablos/new, POST /tablos route registration"
+ contains: "TablosListHandler"
+ - path: "backend/cmd/web/main.go"
+ provides: "Construct TablosDeps and pass to NewRouter"
+ contains: "TablosDeps{Queries: q}"
+ key_links:
+ - from: "backend/internal/web/router.go"
+ to: "TablosCreateHandler"
+ via: "r.Post(\"/tablos\", ...)"
+ pattern: "r\\.Post\\(\"/tablos\""
+ - from: "backend/templates/tablos.templ"
+ to: "ui.CSRFField"
+ via: "@ui.CSRFField(csrfToken) inside create form"
+ pattern: "ui\\.CSRFField"
+ - from: "TablosCreateHandler"
+ to: "Queries.InsertTablo"
+ via: "sqlc binding"
+ pattern: "InsertTablo"
+---
+
+
+First vertical slice of Phase 3: a signed-in user can SEE their tablos on the dashboard and CREATE a new tablo end-to-end via HTMX, with a graceful non-JS POST fallback. Turns TestTabloList, TestTabloList_Empty, TestTabloCreate, TestTabloCreate_Validation green (TABLO-01, TABLO-02, TABLO-06 partially).
+
+Purpose: Deliver real user-visible value as the first slice — the dashboard renders, the create flow works, and the user can see a brand-new tablo appear without a full reload.
+
+Output:
+- handlers_tablos.go with TablosDeps + List/New/Create handlers
+- tablos.templ with dashboard page, empty state, card, create form, OOB-clear template
+- router.go updated: replace IndexHandler with TablosListHandler; mount /tablos/new and /tablos
+- main.go constructs TablosDeps
+- layout.templ footer "Phase 3 · Tablos"
+- index.templ + IndexHandler removed (or emptied) — dashboard now lives in tablos.templ
+
+
+
+@/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/phases/03-tablos-crud/03-CONTEXT.md
+@.planning/phases/03-tablos-crud/03-RESEARCH.md
+@.planning/phases/03-tablos-crud/03-PATTERNS.md
+@.planning/phases/03-tablos-crud/03-UI-SPEC.md
+@.planning/phases/03-tablos-crud/03-01-SUMMARY.md
+@backend/internal/web/handlers_auth.go
+@backend/internal/web/router.go
+@backend/cmd/web/main.go
+@backend/templates/auth_signup.templ
+@backend/templates/index.templ
+@backend/templates/layout.templ
+@backend/internal/web/ui/button.templ
+@backend/internal/web/ui/card.templ
+@backend/internal/web/ui/variants.go
+@backend/internal/db/sqlc/tablos.sql.go
+
+
+
+
+TablosDeps (declared in backend/internal/web/handlers_tablos.go):
+- Queries *sqlc.Queries
+
+Handler constructors (signature pattern matches AuthDeps):
+- TablosListHandler(deps TablosDeps) http.HandlerFunc → GET /
+- TablosNewHandler(deps TablosDeps) http.HandlerFunc → GET /tablos/new (returns TabloCreateFormFragment)
+- TablosCreateHandler(deps TablosDeps) http.HandlerFunc → POST /tablos
+
+NewRouter new signature (replaces existing):
+ func NewRouter(pinger Pinger, staticDir string, deps AuthDeps, tabloDeps TablosDeps, csrfKey []byte, env string, trustedOrigins ...string) http.Handler
+
+Templates (callable by Plan 03):
+- TablosDashboard(user *auth.User, csrfToken string, tablos []sqlc.Tablo) — full page
+- TablosEmptyState() — empty list state
+- TabloCard(t sqlc.Tablo, csrfToken string) — single card on dashboard; wraps in ui.Card with class "tablo-delete-zone" on delete button container
+- TabloCreateFormFragment(form TabloCreateForm, errs TabloCreateErrors, csrfToken string) — inline create form
+- TabloCardWithOOBFormClear(t sqlc.Tablo, csrfToken string) — card + OOB div clearing #create-form-slot
+
+Form types (declared in backend/templates/tablos.templ or a sibling tablos_forms.go file, mirroring auth_forms.go):
+- TabloCreateForm { Title, Description, Color string }
+- TabloCreateErrors { Title, General string }
+
+
+
+
+
+
+ Task 1: tablos.templ — dashboard, empty state, card, create form, OOB-clear (+ generate)
+ backend/templates/tablos.templ, backend/templates/tablos_templ.go, backend/templates/tablos_forms.go, backend/templates/layout.templ, backend/templates/layout_templ.go
+
+ backend/templates/auth_signup.templ
+ backend/templates/auth_forms.go
+ backend/templates/auth_form_errors.templ
+ backend/templates/index.templ
+ backend/templates/layout.templ
+ backend/internal/web/ui/card.templ
+ backend/internal/web/ui/button.templ
+ backend/internal/web/ui/variants.go
+ backend/internal/web/ui/csrf_field.templ
+ backend/internal/db/sqlc/models.go (Tablo struct)
+ .planning/phases/03-tablos-crud/03-UI-SPEC.md (Component Inventory, Interaction Contracts §1+§2, Copywriting Contract)
+ .planning/phases/03-tablos-crud/03-PATTERNS.md (tablos.templ analog block + OOB swap template shape)
+ .planning/phases/03-tablos-crud/03-RESEARCH.md (Pattern 4 dual-target swap, Pitfall 5 top-level OOB)
+
+
+ - TablosDashboard renders inside @Layout("Tablos — Xtablo", user, csrfToken).
+ - Page heading "Your Tablos" using `text-[28px] font-semibold leading-tight` per UI-SPEC Typography.
+ - "New tablo" button (ui.ButtonVariantDefault, ui.ButtonToneSolid, ui.SizeMD, Type "button") fires hx-get="/tablos/new", hx-target="#create-form-slot", hx-swap="innerHTML".
+ - `` rendered above `
`.
+ - When tablos slice is empty → @TablosEmptyState() rendered inside #tablos-list.
+ - Empty state copy exactly: heading "No tablos yet", body "Create your first tablo to get started.", CTA button "New tablo" with aria-label "Create your first tablo" and same hx-get="/tablos/new" attrs.
+ - TabloCard renders inside @ui.Card with: title (Heading style `text-xl font-semibold leading-snug`), optional description paragraph only when `tablo.Description.Valid && tablo.Description.String != ""` (Pitfall 6), optional color dot (only when `tablo.Color.Valid && tablo.Color.String != ""`) using `inline-block w-2.5 h-2.5 rounded-full` with inline `style="background-color: {tablo.Color.String}"`, link "View" to `/tablos/{id}`, and a `.tablo-delete-zone` wrapper containing the Delete button. (Plan 03 wires delete-confirm; Plan 02 just needs the zone div + a Delete button stub.)
+ - TabloCreateFormFragment renders a `