xtablo-source/.planning/phases/04-tasks-kanban/04-01-PLAN.md
Arthur Belleville 7f58588f5a
docs(04): create phase 4 tasks-kanban plan (4 plans, 3 waves)
Wave 1 (Plan 01): DB migration, sqlc queries, RED test scaffold, Sortable.js bootstrap, soft-danger CSS.
Wave 2 (Plan 02): Kanban board render + task create + task delete vertical slice (TASK-01, TASK-02, TASK-06).
Wave 3 (Plan 03): Inline task edit + Sortable.js drag reorder/move (TASK-03, TASK-04, TASK-05, TASK-07).
Wave 4 (Plan 04): Human-verify checkpoint — full browser verification of all 7 TASK requirements.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 09:16:17 +02:00

16 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
04-tasks-kanban 01 execute 1
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
true
TASK-01
TASK-02
TASK-03
TASK-04
TASK-05
TASK-06
TASK-07
truths artifacts key_links
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
path provides contains
backend/migrations/0004_tasks.sql task_status ENUM + tasks table with position index CREATE TYPE task_status AS ENUM
path provides exports
backend/internal/db/queries/tasks.sql sqlc query source for all task CRUD operations
ListTasksByTablo
InsertTask
GetTaskByID
UpdateTask
DeleteTask
MaxPositionByTabloAndStatus
path provides contains
backend/internal/web/handlers_tasks_test.go RED test stubs for TASK-01..07 TestTasksKanbanRenders
path provides
backend/templates/tasks_forms.go TaskCreateForm, TaskCreateErrors, TaskUpdateErrors structs
path provides
backend/internal/web/ui/button.css ui-button-soft-danger-md CSS rule
from to via pattern
backend/migrations/0004_tasks.sql backend/internal/db/sqlc/ sqlc generate reads schema from migrations task_status|tasks
from to via pattern
backend/internal/db/queries/tasks.sql backend/internal/db/sqlc/tasks.sql.go sqlc generate InsertTask|ListTasksByTablo
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.

<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>

@/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

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

Task 1: Migration 0004_tasks.sql + sqlc queries tasks.sql backend/migrations/0004_tasks.sql, backend/internal/db/queries/tasks.sql - 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) - 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 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 ./...
cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && just migrate up && just generate && go build ./... `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. Task 2: RED test scaffold handlers_tasks_test.go + form structs tasks_forms.go backend/internal/web/handlers_tasks_test.go, backend/templates/tasks_forms.go - 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) 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.
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 `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. Task 3: Sortable.js bootstrap + soft-danger button CSS backend/justfile, backend/internal/web/ui/button.css - 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) 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).
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 `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.

<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>
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

<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>
After completion, create `.planning/phases/04-tasks-kanban/04-01-SUMMARY.md`