---
phase: 16-tablo-detail
plan: "03"
subsystem: templates
tags: [htmx, templ, kanban, etape-grouping, design-tokens]
dependency_graph:
  requires: [16-01, 16-02]
  provides: [kanban-etape-grouping, tasks-section-layout, task-row-layout, etape-strip-removed]
  affects: [16-04-files-templ]
tech_stack:
  added: []
  patterns: [groupTasksByEtape helper, EtapeGroup type, EtapeGroupHeader component, tasks-section kanban layout, task-row card pattern]
key_files:
  created: []
  modified:
    - backend/templates/tasks.templ
    - backend/templates/tablos.templ
    - backend/internal/web/handlers_tasks.go
decisions:
  - "EtapeGroup.EtapeColor always set to empty string — sqlc.Etape struct has no Color field in this schema (no etape color in DB migration)"
  - "uuid.UUID(t.EtapeID.Bytes).String() used for pgtype.UUID → string conversion — matches existing taskEtapeIDString helper in tasks_forms.go"
  - "tablos.templ KanbanBoard call updated in Task 2 (not Task 1) — build would fail if tasks.templ committed alone; treated as atomic with handlers_tasks.go updates"
metrics:
  duration: ~20min
  completed: 2026-05-16T22:30:00Z
  tasks_completed: 2
  files_modified: 3
---

# Phase 16 Plan 03: Kanban Restyle + Etape Grouping Summary

**One-liner:** Added `groupTasksByEtape` helper and `EtapeGroupHeader` component to `tasks.templ`; restyled `KanbanColumn` to `tasks-section` layout, `TaskCard` to `task-row` layout, `AddTaskTrigger` to `tasks-add-button`; removed `@EtapeStrip` OOB calls from `TaskCardGone`/`TaskCardOOB`; updated all three `KanbanBoard` call sites to accept `etapes []sqlc.Etape` as 5th parameter.

## Tasks Completed

| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | Add groupTasksByEtape helper, EtapeGroup type, EtapeGroupHeader component; update KanbanBoard/Column signatures and restyled column/card/trigger | 084fc0e | `backend/templates/tasks.templ` |
| 2 | Update all three KanbanBoard call sites (tablos.templ + handlers_tasks.go) and verify full test suite | e2ee434 | `backend/internal/web/handlers_tasks.go`, `backend/templates/tablos.templ` |

## What Was Built

**Task 1 — tasks.templ:**

- **EtapeGroup type:** Struct with `EtapeID string`, `EtapeTitle string`, `EtapeColor string`, `Tasks []sqlc.Task`. EtapeColor always empty (schema has no color field).

- **groupTasksByEtape helper:** Iterates tasks, uses `!t.EtapeID.Valid` to detect unassigned (pgtype.UUID nil check), converts valid UUIDs via `uuid.UUID(t.EtapeID.Bytes).String()` (same pattern as `taskEtapeIDString` in `tasks_forms.go`). Iterates etapes in declaration order, only adds groups with tasks. Appends unassigned group last only if any unassigned tasks exist.

- **EtapeGroupHeader component:** Renders `div class="etape-group-header"`. If `EtapeColor != ""` renders color dot span with inline style. Renders `span class="etape-group-label is-unassigned"` for unassigned group (`EtapeID == ""`), plain `etape-group-label` otherwise.

- **KanbanBoard signature:** Added `etapes []sqlc.Etape` as 5th parameter. Passes `etapes` through to each `@KanbanColumn(...)` call.

- **KanbanColumn restyled:** Outer `div class="kanban-column"` wraps `div class="tasks-section"`. Header: `div class="tasks-section-header"` with left side (h3 + badge) and right side (`div id="add-task-slot-{status}"` containing `@AddTaskTrigger`). Body: `div class="task-list sortable-column"` — empty state uses `p class="task-list-empty"`, else computes `groups := groupTasksByEtape(tasks, etapes)` and renders `@EtapeGroupHeader(group)` + task loop.

- **TaskCard restyled:** Inner div changed from `task-card bg-white rounded border...` to `task-row task-card`. Drag handle removed. HTMX edit attributes moved to the outer task-row div (making the whole row clickable). Children: `div class="task-check"` (round checkbox), `div class="task-body"` (task title p), `@ui.IconButton` with trash/danger/ghost for delete confirm.

- **AddTaskTrigger restyled:** Button class changed from `ui-button ui-button-soft ui-button-neutral ui-button-md w-full text-left text-sm mt-2` to `tasks-add-button`. All HTMX attributes preserved unchanged.

- **EtapeStrip OOB removal:** Deleted `@EtapeStrip(tabloID, etapes, counts, filter, csrfToken, true)` from both `TaskCardGone` and `TaskCardOOB`. Kept `etapes []sqlc.Etape` and `counts EtapeTaskCounts` params on both signatures (avoids handler signature changes that would break tests). Added `// TODO: remove etapes and counts params after Phase 16 cleanup` comments.

**Task 2 — call site updates:**

- `backend/templates/tablos.templ` `TasksTabFragment`: `@KanbanBoard(tablo.ID, csrfToken, tasks, filter)` → `@KanbanBoard(tablo.ID, csrfToken, tasks, filter, etapes)`. `etapes` is already a parameter of `TasksTabFragment`.

- `backend/internal/web/handlers_tasks.go` line 589 (single reorder path): `tasks, _, _, filter, ok` → `tasks, etapes, _, filter, ok`; KanbanBoard call updated to pass `etapes`.

- `backend/internal/web/handlers_tasks.go` line 639 (batch reorder path): same change.

## Verification

```
grep -c "groupTasksByEtape" tasks.templ → 3 (func decl + call inside KanbanColumn)
grep -c "EtapeGroupHeader" tasks.templ → 3 (templ decl + usage in KanbanColumn)
grep -c "kanban-column" tasks.templ → 1
grep -c "tasks-section" tasks.templ → 2 (tasks-section + tasks-section-header)
grep -c "task-row" tasks.templ → 1
grep -c "EtapeStrip" tasks.templ → 0
grep -c "KanbanBoard.*etapes" handlers_tasks.go → 2
grep -c "KanbanBoard.*etapes" tablos.templ → 1
go build ./backend/... → exit 0
go test ./backend/internal/web/... -count=1 → ok (all tests pass)
```

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] sqlc.Etape has no Color field**
- **Found during:** Task 1 — first build attempt
- **Issue:** Plan specified `etape.Color.Valid` and `etape.Color.String` field access on `sqlc.Etape`. The actual schema `type Etape struct` has no `Color` field — only `ID, TabloID, Title, Description, Position, CreatedAt, UpdatedAt`.
- **Fix:** Set `EtapeGroup.EtapeColor` to `""` unconditionally in `groupTasksByEtape`. `EtapeGroupHeader` will not render a color dot for any group (since `EtapeColor` is always empty). The `is-unassigned` label logic still works correctly.
- **Files modified:** `backend/templates/tasks.templ`
- **Commit:** 084fc0e

**2. [Rule 3 - Blocking] tablos.templ KanbanBoard call updated in Task 2 (not deferred)**
- **Found during:** Task 1 — `go build ./backend/...` fails after tasks_templ.go regeneration because `tablos_templ.go` still calls old 4-arg KanbanBoard
- **Issue:** Plan intended tablos.templ to be updated in Task 2, but the templ compiler regenerates `tablos_templ.go` from `tablos.templ` immediately, causing a compile-time argument count mismatch.
- **Fix:** Updated `@KanbanBoard(tablo.ID, csrfToken, tasks, filter)` → `@KanbanBoard(tablo.ID, csrfToken, tasks, filter, etapes)` in tablos.templ during the Task 1/Task 2 boundary (before the first commit), then committed tasks.templ alone and tablos.templ + handlers_tasks.go together. The work was still split into two commits as planned.
- **Files modified:** `backend/templates/tablos.templ`
- **Commit:** e2ee434

## Known Stubs

- **EtapeGroup color dot:** `EtapeColor` is always `""` because `sqlc.Etape` has no `Color` field. The `etape-group-color-dot` CSS and `EtapeGroupHeader` color dot rendering are wired but will never fire unless a `Color` field is added to the schema in a future phase.

## Threat Flags

No new trust boundaries introduced. All changes are HTML/CSS restructuring of existing authenticated routes. T-16-03-01 through T-16-03-04 dispositions unchanged (accept).

## Self-Check: PASSED

- `backend/templates/tasks.templ` — FOUND, contains `groupTasksByEtape`, `EtapeGroupHeader`, `kanban-column`, `tasks-section`, `task-row task-card`, `tasks-add-button`, zero `EtapeStrip`
- `backend/templates/tablos.templ` — FOUND, contains `KanbanBoard.*etapes`
- `backend/internal/web/handlers_tasks.go` — FOUND, contains 2x `KanbanBoard.*etapes`
- Commit 084fc0e — FOUND (Task 1)
- Commit e2ee434 — FOUND (Task 2)
- `go build ./backend/...` — exit 0
- `go test ./backend/internal/web/... -count=1` — PASSED (ok backend/internal/web + ok backend/internal/web/ui)
