diff --git a/.planning/phases/04-tasks-kanban/04-RESEARCH.md b/.planning/phases/04-tasks-kanban/04-RESEARCH.md new file mode 100644 index 0000000..2f77666 --- /dev/null +++ b/.planning/phases/04-tasks-kanban/04-RESEARCH.md @@ -0,0 +1,775 @@ +# Phase 4: Tasks (Kanban) - Research + +**Researched:** 2026-05-15 +**Domain:** Go HTMX Kanban — Postgres ENUM ordering, Sortable.js drag-drop, templ fragments +**Confidence:** HIGH + +--- + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions + +**Column Architecture** +- D-01: Fixed 4 columns: `todo`, `in_progress`, `in_review`, `done` — hardcoded as Go constants, no `task_columns` table. No column CRUD routes in this phase. +- D-02: Column stored on each task row as a Postgres ENUM type: `CREATE TYPE task_status AS ENUM ('todo','in_progress','in_review','done')`. sqlc generates a Go type; invalid values rejected at DB layer. +- D-03: Hard-delete only — no `deleted_at` column on tasks. Inline confirmation before firing (reuse D-07 pattern from Phase 3). + +**Task Ordering** +- D-04: `position INTEGER NOT NULL` on each task row. First task = 100, gap of 100. Midpoint insertion between two tasks. Rebalance when adjacent gap < 2 (reassign multiples of 100 for that column). O(n) rebalance acceptable. +- D-05: Last-write-wins for concurrent edits — documented in code, no optimistic locking. + +**Move/Reorder Interaction** +- D-06: Drag-and-drop via Sortable.js (standalone ~30KB, not a JS framework). +- D-07: Sortable fires `end` event. Inline ` +``` + +**Server-side:** Handler reads `r.Form["task_id"]` (ordered array) and `r.Form["task_col"]` (same length). Position is computed as index * 100 for simplicity; a rebalance pass runs if needed. + +**CSRF note:** The shared form includes `@ui.CSRFField(csrfToken)`. Because the script triggers a normal form `submit` (not a custom HTMX event), gorilla/csrf reads the `_csrf` field from `r.PostFormValue("_csrf")` as usual. No `X-CSRF-Token` header gymnastics needed. + +### Pattern 4: Task Handler Structure (mirrors TablosDeps) + +**What:** `TasksDeps` struct, handler constructor functions returning `http.HandlerFunc`. +**When to use:** All task handlers. + +```go +// Source: handlers_tablos.go pattern [VERIFIED: codebase] +type TasksDeps struct { + Queries *sqlc.Queries +} + +// loadOwnedTabloForTask is a shared preamble: parses tablo UUID, verifies +// ownership via user_id filter (same as loadOwnedTablo in handlers_tablos.go), +// parses task UUID from URL, fetches task verifying tablo_id match. +func loadOwnedTabloForTask(w http.ResponseWriter, r *http.Request, deps TasksDeps) (sqlc.Tablo, sqlc.Task, *auth.User, bool) { + // ... mirrors loadOwnedTablo with extra task fetch +} +``` + +### Pattern 5: HTMX Fragment Response for Task Operations + +**What:** Reuse Phase 3's `HX-Request` detection. HTMX path returns 200 + HTML fragment; non-HTMX path returns 303 redirect. + +```go +// Source: handlers_tablos.go — verbatim pattern [VERIFIED: codebase] +if r.Header.Get("HX-Request") == "true" { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _ = templates.TaskCard(task, csrfToken).Render(ctx, w) + return +} +http.Redirect(w, r, "/tablos/"+tabloID.String(), http.StatusSeeOther) +``` + +**Delete success (HTMX):** Use `HX-Reswap: delete` with the card as target, OR use OOB swap to empty the card element. Simplest: return a 200 with an OOB swap that removes the card `
`. + +### Anti-Patterns to Avoid + +- **CDN script tags in templates:** Sortable.js must be downloaded to `static/sortable.min.js` via `just bootstrap`, consistent with HTMX no-CDN rule (D-10). Never `