---
phase: 04-tasks-kanban
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - backend/migrations/0004_tasks.sql
  - backend/internal/db/queries/tasks.sql
  - backend/internal/web/ui/button.css
  - backend/internal/web/handlers_tasks_test.go
  - backend/templates/tasks_forms.go
  - backend/justfile
autonomous: true
requirements:
  - TASK-01
  - TASK-02
  - TASK-03
  - TASK-04
  - TASK-05
  - TASK-06
  - TASK-07

must_haves:
  truths:
    - "migration 0004_tasks.sql applies cleanly with just migrate up"
    - "sqlc generate produces TaskStatus type and Task struct in internal/db/sqlc/"
    - "handlers_tasks_test.go compiles with RED stubs for all 7 TASK requirements"
    - "Sortable.js 1.15.7 downloaded to static/sortable.min.js via just bootstrap"
    - "ui-button-soft-danger-md CSS class exists in button.css"
  artifacts:
    - path: "backend/migrations/0004_tasks.sql"
      provides: "task_status ENUM + tasks table with position index"
      contains: "CREATE TYPE task_status AS ENUM"
    - path: "backend/internal/db/queries/tasks.sql"
      provides: "sqlc query source for all task CRUD operations"
      exports: ["ListTasksByTablo", "InsertTask", "GetTaskByID", "UpdateTask", "DeleteTask", "MaxPositionByTabloAndStatus"]
    - path: "backend/internal/web/handlers_tasks_test.go"
      provides: "RED test stubs for TASK-01..07"
      contains: "TestTasksKanbanRenders"
    - path: "backend/templates/tasks_forms.go"
      provides: "TaskCreateForm, TaskCreateErrors, TaskUpdateErrors structs"
    - path: "backend/internal/web/ui/button.css"
      provides: "ui-button-soft-danger-md CSS rule"
  key_links:
    - from: "backend/migrations/0004_tasks.sql"
      to: "backend/internal/db/sqlc/"
      via: "sqlc generate reads schema from migrations"
      pattern: "task_status|tasks"
    - from: "backend/internal/db/queries/tasks.sql"
      to: "backend/internal/db/sqlc/tasks.sql.go"
      via: "sqlc generate"
      pattern: "InsertTask|ListTasksByTablo"
---

<objective>
Wave 0 foundation: establish the DB schema, sqlc query source, test RED scaffold, Sortable.js asset, soft-danger button CSS, and form structs that all subsequent plans depend on. Nothing is executable from the user's perspective yet — this plan creates the preconditions for Plans 02 and 03 to deliver vertical slices.

Purpose: Ensure sqlc generate has the query source it needs; ensure setupTestDB applies the migration; ensure test functions compile as RED; ensure Sortable.js is available at /static/sortable.min.js.
Output: migration file, sqlc queries, RED test stubs, CSS rule, form structs, justfile update.
</objective>

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

<context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/ROADMAP.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/04-tasks-kanban/04-CONTEXT.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/04-tasks-kanban/04-RESEARCH.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/04-tasks-kanban/04-VALIDATION.md

<interfaces>
<!-- Key types and patterns the executor needs. Extracted from codebase. -->

From backend/internal/db/queries/tablos.sql (sqlc query style reference):
  -- name: ListTablosByUser :many
  SELECT ... FROM tablos WHERE user_id = $1 ORDER BY created_at DESC;
  -- name: InsertTablo :one
  INSERT INTO tablos (...) VALUES ($1, $2, $3, $4) RETURNING ...;
  -- name: DeleteTablo :exec
  DELETE FROM tablos WHERE id = $1 AND user_id = $2;

From backend/migrations/0003_tablos.sql (migration style reference):
  -- +goose Up
  CREATE TABLE tablos ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), ... );
  CREATE INDEX tablos_user_id_idx ON tablos(user_id);
  -- +goose Down
  DROP TABLE IF EXISTS tablos;

From backend/internal/web/handlers_tablos_test.go (test patterns):
  package web
  func newTabloTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler { ... }
  func loginUser(t *testing.T, router http.Handler, email, password string) []*http.Cookie { ... }
  setupTestDB(t) / preInsertUser / getCSRFToken are in testdb_test.go / handlers_test.go (same package)

From backend/internal/web/ui/button.css (CSS rule pattern for new soft-danger variant):
  Existing: .ui-button-soft-neutral-md { display: inline-flex; align-items: center;
    border-radius: 0.375rem; background-color: #f1f5f9; padding: 0.5rem 1rem;
    font-size: 1rem; font-weight: 600; color: #475569; border: 1px solid #e2e8f0; min-height: 44px; }

From backend/templates/tablos_forms.go (form struct pattern):
  package templates
  type TabloCreateForm struct { Title, Description, Color string }
  type TabloCreateErrors struct { Title, Color, General string }

From backend/justfile (bootstrap pattern):
  htmx_version := "2"
  curl -sSL -o static/htmx.min.js "https://unpkg.com/htmx.org@{{ htmx_version }}/dist/htmx.min.js"
  clean: rm -rf bin/ tmp/ static/htmx.min.js static/tailwind.css
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Migration 0004_tasks.sql + sqlc queries tasks.sql</name>
  <files>backend/migrations/0004_tasks.sql, backend/internal/db/queries/tasks.sql</files>
  <read_first>
    - backend/migrations/0003_tablos.sql (migration file style — goose Up/Down sections, uuid PK, indexes)
    - backend/internal/db/queries/tablos.sql (sqlc query style — :many/:one/:exec annotations, $N params)
    - backend/internal/web/handlers_tablos.go (how Queries methods are called — confirms field names)
  </read_first>
  <behavior>
    - After `just migrate up`, `SELECT id FROM tasks LIMIT 1` on a fresh DB does not error
    - After `just migrate down` (one step), tasks table and task_status type are gone
    - After `sqlc generate`, backend/internal/db/sqlc/ contains TaskStatus type with constants TaskStatusTodo, TaskStatusInProgress, TaskStatusInReview, TaskStatusDone
    - After `sqlc generate`, sqlc.Task struct has fields: ID uuid.UUID, TabloID uuid.UUID, Title string, Description pgtype.Text, Status TaskStatus, Position int32, CreatedAt pgtype.Timestamptz, UpdatedAt pgtype.Timestamptz
    - ListTasksByTablo query accepts a single uuid.UUID param and returns []Task ordered by status, position, created_at
    - InsertTask returns a single Task row (`:one`)
    - DeleteTask is `:exec` with params (id uuid, tablo_id uuid)
    - MaxPositionByTabloAndStatus uses COALESCE(MAX(position), 0)::integer and returns a single int32 row
  </behavior>
  <action>
    Create `backend/migrations/0004_tasks.sql` with goose Up/Down sections. Up section:
    (1) CREATE TYPE task_status AS ENUM ('todo', 'in_progress', 'in_review', 'done') — declaration order must match visual left-to-right column order (Pitfall 6 from RESEARCH.md).
    (2) CREATE TABLE tasks with columns: id uuid PRIMARY KEY DEFAULT gen_random_uuid(), tablo_id uuid NOT NULL REFERENCES tablos(id) ON DELETE CASCADE, title text NOT NULL, description text, status task_status NOT NULL DEFAULT 'todo', position integer NOT NULL DEFAULT 100, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now().
    (3) CREATE INDEX tasks_tablo_id_status_idx ON tasks(tablo_id, status, position).
    Down section: DROP TABLE IF EXISTS tasks; then DROP TYPE IF EXISTS task_status; — table MUST precede type (Pitfall 3 from RESEARCH.md).

    Create `backend/internal/db/queries/tasks.sql` with these named queries (use same SELECT column list in every query — no wildcards):
    - ListTasksByTablo :many — WHERE tablo_id = $1 ORDER BY status, position, created_at
    - InsertTask :one — INSERT with params (tablo_id, title, description, status, position) RETURNING all columns
    - GetTaskByID :one — WHERE id = $1 AND tablo_id = $2
    - UpdateTask :one — UPDATE SET title=$2, description=$3, status=$4, position=$5, updated_at=now() WHERE id=$1 RETURNING all columns
    - DeleteTask :exec — DELETE WHERE id=$1 AND tablo_id=$2
    - MaxPositionByTabloAndStatus :one — SELECT COALESCE(MAX(position), 0)::integer AS max_position WHERE tablo_id=$1 AND status=$2

    After creating both files, run: cd backend && just migrate up && just generate
    Verify sqlc output compiled by running: cd backend && go build ./...
  </action>
  <verify>
    <automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && just migrate up && just generate && go build ./...</automated>
  </verify>
  <done>
    `just migrate up` exits 0. `just generate` exits 0. `go build ./...` exits 0. backend/internal/db/sqlc/ contains file(s) with TaskStatus type and InsertTask/ListTasksByTablo/DeleteTask functions visible via grep.
  </done>
</task>

<task type="auto">
  <name>Task 2: RED test scaffold handlers_tasks_test.go + form structs tasks_forms.go</name>
  <files>backend/internal/web/handlers_tasks_test.go, backend/templates/tasks_forms.go</files>
  <read_first>
    - backend/internal/web/handlers_tablos_test.go (full file — test function names, newTabloTestRouter, loginUser, preInsertUser, getCSRFToken patterns to replicate)
    - backend/internal/web/router.go (NewRouter signature — see what params it takes; tasks router wiring will be added in Plan 02)
    - backend/internal/db/sqlc/ (generated Task, TaskStatus types — confirm field names before using in test setup)
    - backend/templates/tablos_forms.go (form struct pattern to mirror in tasks_forms.go)
  </read_first>
  <action>
    Create `backend/templates/tasks_forms.go` in package templates with these exported structs:
    - TaskCreateForm: fields Title string, Status string (holds the column status value for the form)
    - TaskCreateErrors: fields Title string, General string
    - TaskUpdateForm: fields Title string, Description string
    - TaskUpdateErrors: fields Title string, Description string, General string

    Create `backend/internal/web/handlers_tasks_test.go` in package web. The file must compile but all test functions must call t.Skip("handlers_tasks not yet implemented") or stub to t.Fatal so they are RED until Plan 02 implements the handlers. Include these test functions covering TASK-01 through TASK-07:
    - TestTasksKanbanRenders — GET /tablos/{id} by owner shows 4 column headers (TASK-01)
    - TestTaskCreate — POST /tablos/{id}/tasks creates task, returns 200+fragment for HTMX (TASK-02)
    - TestTaskCreateValidation — POST /tablos/{id}/tasks with empty title returns 422 (TASK-02)
    - TestTaskUpdate — POST /tablos/{id}/tasks/{task_id} updates title/desc, returns card fragment (TASK-03)
    - TestTaskReorderCrossColumn — POST /tablos/{id}/tasks/reorder changes task column (TASK-04)
    - TestTaskReorderSameColumn — POST /tablos/{id}/tasks/reorder changes position within column (TASK-05)
    - TestTaskDelete — POST /tablos/{id}/tasks/{task_id}/delete removes task, returns empty div (TASK-06)
    - TestTaskOrderPersists — GET /tablos/{id} after reorder shows tasks in new position order (TASK-07)
    - TestTaskOwnership — GET/POST task routes by non-owner return 404 (T-04-IDOR)

    Each function must declare a newTaskTestRouter helper (or reuse newTabloTestRouter — same params). The test router will need TasksDeps once Plan 02 adds it; stub it as:
      type TasksDeps struct{ Queries *sqlc.Queries }
    declared at the top of handlers_tasks_test.go (will be moved to handlers_tasks.go in Plan 02).

    Important: the test file compiles in the same `package web` as the other test files. Use setupTestDB, loginUser, preInsertUser, getCSRFToken, testCSRFKey from the existing test files — they are in the same package.

    Run `cd backend && go build ./... && go test ./internal/web/ -run TestTask -v -count=1` — expect compile success and all TestTask* functions to be skipped or report t.Skip.
  </action>
  <verify>
    <automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && go build ./... && go test ./internal/web/ -run TestTask -v -count=1 2>&1 | head -40</automated>
  </verify>
  <done>
    `go build ./...` exits 0. `go test ./internal/web/ -run TestTask -v` exits 0 with all TestTask* functions appearing as SKIP or PASS (not compile errors, not unexpected FAIL). tasks_forms.go exports TaskCreateForm, TaskCreateErrors, TaskUpdateForm, TaskUpdateErrors structs.
  </done>
</task>

<task type="auto">
  <name>Task 3: Sortable.js bootstrap + soft-danger button CSS</name>
  <files>backend/justfile, backend/internal/web/ui/button.css</files>
  <read_first>
    - backend/justfile (full file — bootstrap recipe pattern, version variable declarations, clean recipe)
    - backend/internal/web/ui/button.css (full file — existing soft-neutral CSS rule to mirror for soft-danger)
  </read_first>
  <action>
    In `backend/justfile`:
    (1) Add variable at the top with pinned tools section: `sortable_version := "1.15.7"`
    (2) In the `bootstrap` recipe, after the HTMX download line, add:
        `curl -sSL -o static/sortable.min.js "https://cdn.jsdelivr.net/npm/sortablejs@{{ sortable_version }}/Sortable.min.js"`
    (3) In the `clean` recipe, add `static/sortable.min.js` to the rm -rf list alongside `static/htmx.min.js`.

    In `backend/internal/web/ui/button.css`:
    Add the `.ui-button-soft-danger-md` CSS rule per UI-SPEC Component Inventory. The rule must include:
    display: inline-flex; align-items: center; border-radius: 0.375rem; background-color: #fee2e2; padding: 0.5rem 1rem; font-size: 1rem; font-weight: 600; color: #b91c1c; border: 1px solid #fecaca; min-height: 44px.
    Also add :hover rule (background-color: #fecaca) and :focus-visible rule (outline: 2px solid #b91c1c; outline-offset: 2px).

    After editing button.css, run `just generate` to regenerate tailwind.css (no button.css compilation needed — it's a plain CSS file imported by tailwind.input.css or similar).

    Then run `just bootstrap` to download sortable.min.js (requires network access).
  </action>
  <verify>
    <automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && grep -c 'ui-button-soft-danger-md' internal/web/ui/button.css && grep -c 'sortable_version' justfile && ls static/sortable.min.js</automated>
  </verify>
  <done>
    `grep -c 'ui-button-soft-danger-md' backend/internal/web/ui/button.css` returns 1 or more (rule exists). `grep -c 'sortable_version' backend/justfile` returns 1 or more. `static/sortable.min.js` exists on disk. `just generate` exits 0.
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Migration → DB | goose applies SQL schema; task_status ENUM values hardcoded in migration |
| sqlc queries → handler layer | Generated Go functions called by handlers; no user input reaches query source |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-04-01 | Tampering | 0004_tasks.sql Down migration | mitigate | DROP TABLE before DROP TYPE (Pitfall 3); verified by `just migrate down` in CI |
| T-04-02 | Information Disclosure | tasks table ENUM declaration order | accept | ENUM declaration order matches visual column order; Pitfall 6 documented |
</threat_model>

<verification>
After all three tasks complete:
- `cd backend && go build ./...` exits 0
- `cd backend && go test ./internal/web/ -run TestTask -v` exits 0 (all SKIP)
- `grep 'ui-button-soft-danger-md' backend/internal/web/ui/button.css` returns a match
- `ls backend/static/sortable.min.js` succeeds
- `ls backend/internal/db/queries/tasks.sql` succeeds
- `ls backend/migrations/0004_tasks.sql` succeeds
</verification>

<success_criteria>
Wave 0 complete when:
1. `just migrate up` applies 0004_tasks.sql cleanly against local Postgres
2. `just generate` produces TaskStatus type and Task struct in internal/db/sqlc/
3. `go build ./...` exits 0 — all packages compile
4. `go test ./internal/web/ -run TestTask -v` exits 0 with 9 TestTask* functions SKIP'd
5. button.css contains .ui-button-soft-danger-md rule
6. static/sortable.min.js exists at version 1.15.7
7. justfile has sortable_version variable and bootstrap downloads sortable.min.js
</success_criteria>

<output>
After completion, create `.planning/phases/04-tasks-kanban/04-01-SUMMARY.md`
</output>
