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>
16 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 04-tasks-kanban | 01 | execute | 1 |
|
true |
|
|
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.mdFrom 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> |
<success_criteria> Wave 0 complete when:
just migrate upapplies 0004_tasks.sql cleanly against local Postgresjust generateproduces TaskStatus type and Task struct in internal/db/sqlc/go build ./...exits 0 — all packages compilego test ./internal/web/ -run TestTask -vexits 0 with 9 TestTask* functions SKIP'd- button.css contains .ui-button-soft-danger-md rule
- static/sortable.min.js exists at version 1.15.7
- justfile has sortable_version variable and bootstrap downloads sortable.min.js </success_criteria>