diff --git a/.planning/STATE.md b/.planning/STATE.md
index 7a88648..2a54c1d 100644
--- a/.planning/STATE.md
+++ b/.planning/STATE.md
@@ -2,15 +2,15 @@
gsd_state_version: 1.0
milestone: v2.0
milestone_name: Collaboration, planning, and social sign-in
-status: planning
-last_updated: "2026-05-15T20:25:48.078Z"
-last_activity: 2026-05-15 -- Phase 09 UI-SPEC approved
+status: executing
+last_updated: "2026-05-15T20:29:51.890Z"
+last_activity: 2026-05-15 -- Phase 09 planning complete
progress:
total_phases: 5
completed_phases: 1
- total_plans: 5
+ total_plans: 9
completed_plans: 5
- percent: 100
+ percent: 56
---
# STATE
@@ -30,16 +30,16 @@ See: `.planning/PROJECT.md` (updated 2026-05-15)
Phase: 9 — Etapes
Plan: —
-Status: Ready for planning
-Last activity: 2026-05-15 -- Phase 09 UI-SPEC approved
-Resume file: .planning/phases/09-etapes/09-UI-SPEC.md
+Status: Ready to execute
+Last activity: 2026-05-15 -- Phase 09 planning complete
+Resume file: .planning/phases/09-etapes/09-01-PLAN.md
## Phase Status
| # | Phase | Status |
|---|-------|--------|
| 8 | Social Sign-in | ✓ Complete |
-| 9 | Etapes | ◐ UI-SPEC approved |
+| 9 | Etapes | ◆ Ready to execute |
| 10 | Events | ○ Pending |
| 11 | Individual Planning | ○ Pending |
| 12 | Native Tablo Chat | ○ Pending |
diff --git a/.planning/phases/09-etapes/09-01-PLAN.md b/.planning/phases/09-etapes/09-01-PLAN.md
new file mode 100644
index 0000000..e56bc79
--- /dev/null
+++ b/.planning/phases/09-etapes/09-01-PLAN.md
@@ -0,0 +1,153 @@
+---
+phase: 09-etapes
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - backend/migrations/0007_etapes.sql
+ - backend/internal/db/queries/etapes.sql
+ - backend/internal/db/queries/tasks.sql
+ - backend/internal/web/handlers_etapes_test.go
+ - backend/internal/web/handlers_tasks_test.go
+ - backend/internal/web/handlers_etapes.go
+ - backend/internal/web/handlers_tasks.go
+ - backend/internal/web/router.go
+ - backend/templates/etapes.templ
+ - backend/templates/tasks.templ
+ - backend/templates/tablos.templ
+autonomous: true
+requirements: [ETAPE-01, ETAPE-02, ETAPE-03, ETAPE-04, ETAPE-05, ETAPE-06]
+
+must_haves:
+ truths:
+ - "D-01/D-02/D-03: Tasks tab renders a compact top etape strip with All, Unassigned, and created etape chips with counts"
+ - "D-05/D-06/D-07: Selecting an etape chip filters the existing four-column kanban without changing TaskColumns"
+ - "D-08/D-09/D-10: task position remains status-scoped; no task position is scoped by etape"
+ - "D-11/D-12: task create form includes an Etape selector with No etape and created etapes"
+ - "D-14/D-15/D-16: migration models etapes as a tablo-owned table with no parent column and tasks.etape_id nullable ON DELETE SET NULL"
+ - "POST /tablos/{id}/etapes creates an etape and the response makes it visible in the strip"
+ - "POST /tablos/{id}/tasks can create a task assigned to the active etape"
+ artifacts:
+ - path: "backend/migrations/0007_etapes.sql"
+ provides: "etapes table plus nullable tasks.etape_id foreign key"
+ - path: "backend/internal/db/queries/etapes.sql"
+ provides: "sqlc etape CRUD queries"
+ - path: "backend/templates/etapes.templ"
+ provides: "EtapeStrip and create form fragments"
+ - path: "backend/internal/web/handlers_etapes.go"
+ provides: "first etape create/list/filter handlers"
+---
+
+
+Vertical slice 1: a user can create an etape inside a tablo, see it as a compact chip in the Tasks tab, filter the existing kanban by that etape, and create a task assigned to it from the existing add-task flow. This plan also establishes the schema and generated query surface needed for every later etape slice.
+
+
+
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-CONTEXT.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-RESEARCH.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-UI-SPEC.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-VALIDATION.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-PATTERNS.md
+
+
+
+T-09-01 Cross-tablo etape assignment: task create/update must reject an `etape_id` that does not belong to the same tablo as the task.
+T-09-02 Cross-user etape access: non-owners must receive 404 from etape routes through `loadOwnedTablo`.
+T-09-03 CSRF: every etape/task mutation form must include `@ui.CSRFField(csrfToken)` and use existing CSRF middleware.
+T-09-04 XSS: etape titles/descriptions must be rendered through normal templ escaped output, never raw HTML.
+T-09-05 Data loss: deleting an etape must use `ON DELETE SET NULL` behavior for tasks, not task deletion.
+
+
+
+
+
+ Task 1: Add RED coverage for first etape slice
+
+ - backend/internal/web/handlers_etapes_test.go
+ - backend/internal/web/handlers_tasks_test.go
+
+
+ - backend/internal/web/handlers_tasks_test.go
+ - backend/internal/web/handlers_tablos_test.go
+ - backend/migrations/0004_tasks.sql
+ - .planning/phases/09-etapes/09-VALIDATION.md
+
+
+ Add failing DB-backed handler tests for the first slice:
+ 1. `TestEtapeCreateRendersChipAndCount` creates an owned tablo, POSTs `/tablos/{id}/etapes` with title `Design`, and expects response/body or subsequent Tasks tab render to contain `Design`, `Unassigned`, and `All`.
+ 2. `TestTaskCreateAssignsEtape` creates an etape, POSTs `/tablos/{id}/tasks` with `etape_id`, and verifies `GetTaskByID` returns that etape id.
+ 3. `TestEtapeFilterRendersExistingKanbanColumns` creates assigned and unassigned tasks, GETs `/tablos/{id}/tasks?etape={etape_id}`, expects only assigned task title plus all four column labels.
+ Keep tests in the existing integration style using `setupTestDB`, `preInsertUser`, `getCSRFToken`, and authenticated cookies.
+
+
+ cd backend && go test ./internal/web -run 'TestEtapeCreateRendersChipAndCount|TestTaskCreateAssignsEtape|TestEtapeFilterRendersExistingKanbanColumns' -count=1
+
+
+ - `backend/internal/web/handlers_etapes_test.go` contains `TestEtapeCreateRendersChipAndCount`.
+ - `backend/internal/web/handlers_tasks_test.go` or `handlers_etapes_test.go` contains `TestTaskCreateAssignsEtape`.
+ - `backend/internal/web/handlers_etapes_test.go` contains `TestEtapeFilterRendersExistingKanbanColumns`.
+ - The targeted tests fail before implementation because etape schema/routes/query fields are missing.
+
+
+
+
+ Task 2: Implement the thinnest create-filter-assigned-task slice
+
+ - backend/migrations/0007_etapes.sql
+ - backend/internal/db/queries/etapes.sql
+ - backend/internal/db/queries/tasks.sql
+ - backend/internal/web/handlers_etapes.go
+ - backend/internal/web/handlers_tasks.go
+ - backend/internal/web/router.go
+ - backend/templates/etapes.templ
+ - backend/templates/tasks.templ
+ - backend/templates/tablos.templ
+
+
+ - backend/migrations/0004_tasks.sql
+ - backend/migrations/0005_files.sql
+ - backend/internal/db/queries/tasks.sql
+ - backend/internal/web/handlers_tasks.go
+ - backend/internal/web/handlers_files.go
+ - backend/internal/web/router.go
+ - backend/templates/tasks.templ
+ - backend/templates/tablos.templ
+ - .planning/phases/09-etapes/09-UI-SPEC.md
+
+
+ Add `backend/migrations/0007_etapes.sql` with `etapes` table (`id`, `tablo_id`, `title`, `description`, `position`, timestamps), `etapes_tablo_id_position_idx`, nullable `tasks.etape_id` referencing `etapes(id) ON DELETE SET NULL`, and `tasks_tablo_id_etape_id_idx`. Do not add `parent_id`.
+ Add etape sqlc queries for list/create/get/max position. Extend task queries to select and preserve `etape_id`; `InsertTask` accepts nullable etape id; `UpdateTask` accepts nullable etape id.
+ Add minimal etape create/list/filter handlers and route mounting under the protected `/tablos/{id}` group. Create `EtapeStrip` plus minimal create form; wrap Tasks tab in `#tasks-tab`.
+ Extend task create handler/form so `etape_id` can be submitted and validated against the same tablo before insert. When active filter is a concrete etape, the add-task form submits that etape id.
+ Run `cd backend && just generate` after migration/query/template changes.
+
+
+ cd backend && just generate && go test ./internal/web -run 'TestEtapeCreateRendersChipAndCount|TestTaskCreateAssignsEtape|TestEtapeFilterRendersExistingKanbanColumns|TestTaskCreate' -count=1
+
+
+ - `backend/migrations/0007_etapes.sql` contains `CREATE TABLE etapes`.
+ - `backend/migrations/0007_etapes.sql` contains `ON DELETE SET NULL`.
+ - `backend/migrations/0007_etapes.sql` does not contain `parent_id`.
+ - `backend/internal/db/queries/etapes.sql` contains `ListEtapesByTablo`, `InsertEtape`, and `GetEtapeByID`.
+ - `backend/internal/db/queries/tasks.sql` selects `etape_id` in `ListTasksByTablo` and `GetTaskByID`.
+ - `backend/templates/tablos.templ` renders a `#tasks-tab` wrapper before `@KanbanBoard`.
+ - Targeted tests listed in verify pass.
+
+
+
+
+
+
+Run:
+- `cd backend && just generate`
+- `cd backend && go test ./internal/web -count=1`
+- `git diff --check`
+
+
+
+- ETAPE-01 is partially delivered through create + render chip.
+- ETAPE-03 is partially delivered through create-task assignment.
+- ETAPE-05 is partially delivered through etape filtering over existing columns.
+- ETAPE-06 is delivered at schema level by no nested etape relationship.
+
diff --git a/.planning/phases/09-etapes/09-02-PLAN.md b/.planning/phases/09-etapes/09-02-PLAN.md
new file mode 100644
index 0000000..74a4b9d
--- /dev/null
+++ b/.planning/phases/09-etapes/09-02-PLAN.md
@@ -0,0 +1,130 @@
+---
+phase: 09-etapes
+plan: 02
+type: execute
+wave: 2
+depends_on:
+ - 09-01-PLAN.md
+files_modified:
+ - backend/internal/db/queries/etapes.sql
+ - backend/internal/web/handlers_etapes.go
+ - backend/internal/web/router.go
+ - backend/templates/etapes.templ
+ - backend/templates/tablos.templ
+ - backend/internal/web/handlers_etapes_test.go
+autonomous: true
+requirements: [ETAPE-01, ETAPE-02, ETAPE-04, ETAPE-05]
+
+must_haves:
+ truths:
+ - "D-01/D-02/D-03/D-04: etape management remains a compact top strip, not a card panel or separate page"
+ - "D-08: etape order is independent through etapes.position"
+ - "D-14: deleting an etape keeps tasks and moves them to Unassigned"
+ - "D-05/D-06: chip active state and counts update after create/edit/delete/reorder"
+ - "UI-SPEC: delete confirmation copy says tasks stay in the tablo and move to Unassigned"
+ artifacts:
+ - path: "backend/internal/web/handlers_etapes.go"
+ provides: "Etape edit, delete, and reorder handlers"
+ - path: "backend/templates/etapes.templ"
+ provides: "Etape edit/delete/reorder fragments"
+ - path: "backend/internal/web/handlers_etapes_test.go"
+ provides: "CRUD, delete-unassign, reorder, and ownership tests"
+---
+
+
+Vertical slice 2: a user can manage etapes from the top strip: edit title/description, delete an etape without deleting tasks, and reorder etape chips. After this plan, etapes are a usable organizing surface independent of task assignment form refinements.
+
+
+
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-CONTEXT.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-RESEARCH.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-UI-SPEC.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-PATTERNS.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-01-PLAN.md
+
+
+
+T-09-02 Cross-user etape access: all edit/delete/reorder routes must call `loadOwnedTablo` and bind etape id to tablo id.
+T-09-03 CSRF: edit/delete/reorder forms must include `@ui.CSRFField(csrfToken)`.
+T-09-04 XSS: title and description output must be templ escaped.
+T-09-05 Data loss: delete route must delete only from `etapes`; tasks survive through nullable foreign key behavior.
+
+
+
+
+
+ Task 1: Add RED coverage for etape management
+
+ - backend/internal/web/handlers_etapes_test.go
+
+
+ - backend/internal/web/handlers_etapes_test.go
+ - backend/internal/web/handlers_tasks_test.go
+ - backend/internal/web/handlers_files.go
+ - .planning/phases/09-etapes/09-UI-SPEC.md
+
+
+ Add failing tests:
+ 1. `TestEtapeUpdateChangesTitleAndDescription` posts `/tablos/{id}/etapes/{etape_id}` and verifies rendered/body and DB state.
+ 2. `TestEtapeDeleteUnassignsTasks` creates an etape and assigned task, posts delete, verifies task row still exists with null etape id.
+ 3. `TestEtapeReorderPersistsPosition` creates two etapes, posts `/tablos/{id}/etapes/reorder`, and verifies `ListEtapesByTablo` ordering.
+ 4. `TestEtapeOwnershipReturns404` verifies a non-owner receives 404 for edit/delete/reorder attempts.
+
+
+ cd backend && go test ./internal/web -run 'TestEtapeUpdateChangesTitleAndDescription|TestEtapeDeleteUnassignsTasks|TestEtapeReorderPersistsPosition|TestEtapeOwnershipReturns404' -count=1
+
+
+ - `handlers_etapes_test.go` contains all four named tests.
+ - Tests fail before implementation because update/delete/reorder routes or behavior are missing.
+
+
+
+
+ Task 2: Implement edit, delete-unassign, and reorder
+
+ - backend/internal/db/queries/etapes.sql
+ - backend/internal/web/handlers_etapes.go
+ - backend/internal/web/router.go
+ - backend/templates/etapes.templ
+ - backend/templates/tablos.templ
+
+
+ - backend/internal/db/queries/etapes.sql
+ - backend/internal/web/handlers_etapes.go
+ - backend/internal/web/handlers_tasks.go
+ - backend/templates/etapes.templ
+ - backend/templates/tasks.templ
+ - backend/templates/tablos.templ
+
+
+ Add `UpdateEtape`, `DeleteEtape`, and `UpdateEtapePosition` queries. Implement GET edit, POST update, GET delete-confirm, POST delete, and POST reorder handlers.
+ Delete confirmation must use heading `Delete etape?`, body `Tasks in this etape will stay in the tablo and move to Unassigned.`, submit label `Delete etape`, and cancel label `Keep etape`.
+ Reordering may use explicit up/down controls or a compact Sortable.js strip. If Sortable.js is reused, use a separate group from `kanban` and do not target `.sortable-column`.
+ After delete/update/reorder, return a Tasks-tab or etape-strip fragment that refreshes chip titles, counts, active state, and kanban contents.
+
+
+ cd backend && just generate && go test ./internal/web -run 'TestEtapeUpdateChangesTitleAndDescription|TestEtapeDeleteUnassignsTasks|TestEtapeReorderPersistsPosition|TestEtapeOwnershipReturns404' -count=1
+
+
+ - `backend/internal/db/queries/etapes.sql` contains `UpdateEtape`, `DeleteEtape`, and `UpdateEtapePosition`.
+ - `backend/templates/etapes.templ` contains `Delete etape?` and `Tasks in this etape will stay in the tablo and move to Unassigned.`
+ - Non-owner etape management tests expect and receive 404.
+ - Delete-unassign test proves task row remains after etape deletion.
+ - Targeted tests listed in verify pass.
+
+
+
+
+
+
+Run:
+- `cd backend && just generate`
+- `cd backend && go test ./internal/web -count=1`
+- `git diff --check`
+
+
+
+- ETAPE-02 is delivered for edit/delete/reorder.
+- ETAPE-04 is delivered through delete-unassign behavior.
+- Etape chip order and counts remain usable from the Tasks tab.
+
diff --git a/.planning/phases/09-etapes/09-03-PLAN.md b/.planning/phases/09-etapes/09-03-PLAN.md
new file mode 100644
index 0000000..b8e01cf
--- /dev/null
+++ b/.planning/phases/09-etapes/09-03-PLAN.md
@@ -0,0 +1,131 @@
+---
+phase: 09-etapes
+plan: 03
+type: execute
+wave: 3
+depends_on:
+ - 09-02-PLAN.md
+files_modified:
+ - backend/internal/db/queries/tasks.sql
+ - backend/internal/web/handlers_tasks.go
+ - backend/internal/web/handlers_files.go
+ - backend/templates/tasks.templ
+ - backend/templates/tablos.templ
+ - backend/internal/web/handlers_tasks_test.go
+autonomous: true
+requirements: [ETAPE-03, ETAPE-05]
+
+must_haves:
+ truths:
+ - "D-11/D-12: task create and edit forms include an Etape selector with No etape"
+ - "D-13: quick dropdowns on task cards are not implemented in this phase"
+ - "D-05/D-06: filters support concrete etape ids and Unassigned"
+ - "D-09/D-10: task update preserves current status and position while changing assignment"
+ - "UI-SPEC: selector label is Etape and empty option is No etape"
+ artifacts:
+ - path: "backend/templates/tasks.templ"
+ provides: "Etape selector in task create/edit fragments"
+ - path: "backend/internal/web/handlers_tasks.go"
+ provides: "task assignment parsing and same-tablo validation"
+ - path: "backend/internal/web/handlers_files.go"
+ provides: "Tasks tab handler updated to load etapes and filtered tasks"
+---
+
+
+Vertical slice 3: a user can assign and unassign tasks from etapes in the task create/edit forms, and the Tasks tab filter works for All, Unassigned, and each etape while preserving the existing kanban layout.
+
+
+
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-CONTEXT.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-RESEARCH.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-UI-SPEC.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-PATTERNS.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-02-PLAN.md
+
+
+
+T-09-01 Cross-tablo etape assignment: create/update handlers must validate submitted etape id with `GetEtapeByID(id, tablo_id)` before writing it to a task.
+T-09-02 Cross-user access: filtering by an etape id from another user's tablo must return 404 or ignore it without leaking the etape title.
+T-09-03 CSRF: task create/edit forms continue to include CSRF tokens after adding the selector.
+T-09-04 XSS: etape option labels must be templ escaped.
+
+
+
+
+
+ Task 1: Add RED coverage for assignment and filters
+
+ - backend/internal/web/handlers_tasks_test.go
+
+
+ - backend/internal/web/handlers_tasks_test.go
+ - backend/internal/web/handlers_etapes_test.go
+ - backend/templates/tasks.templ
+ - .planning/phases/09-etapes/09-UI-SPEC.md
+
+
+ Add failing tests:
+ 1. `TestTaskEditRendersEtapeSelector` verifies GET edit renders label `Etape`, option `No etape`, and existing etape title.
+ 2. `TestTaskUpdateAssignsAndUnassignsEtape` posts task update with an etape id, verifies DB assignment, then posts with empty etape value and verifies null assignment.
+ 3. `TestTaskAssignmentRejectsForeignEtape` creates two tablos and verifies assigning a task to another tablo's etape fails without updating the task.
+ 4. `TestTasksTabUnassignedFilter` verifies `/tablos/{id}/tasks?etape=unassigned` renders unassigned tasks and hides assigned task titles while keeping all four status headers.
+
+
+ cd backend && go test ./internal/web -run 'TestTaskEditRendersEtapeSelector|TestTaskUpdateAssignsAndUnassignsEtape|TestTaskAssignmentRejectsForeignEtape|TestTasksTabUnassignedFilter' -count=1
+
+
+ - `handlers_tasks_test.go` contains all four named tests.
+ - Tests fail before implementation because selector/update/filter behavior is incomplete.
+
+
+
+
+ Task 2: Implement form assignment and filter-aware task loading
+
+ - backend/internal/db/queries/tasks.sql
+ - backend/internal/web/handlers_tasks.go
+ - backend/internal/web/handlers_files.go
+ - backend/templates/tasks.templ
+ - backend/templates/tablos.templ
+
+
+ - backend/internal/db/queries/tasks.sql
+ - backend/internal/web/handlers_tasks.go
+ - backend/internal/web/handlers_files.go
+ - backend/templates/tasks.templ
+ - backend/templates/tablos.templ
+ - .planning/phases/09-etapes/09-RESEARCH.md
+ - .planning/phases/09-etapes/09-UI-SPEC.md
+
+
+ Extend task create/edit template signatures to receive etapes and selected etape id. Render a select labeled `Etape` with first option `No etape`.
+ Extend `TaskCreateHandler`, `TaskEditHandler`, and `TaskUpdateHandler` to load etapes for the owned tablo, parse optional `etape_id`, validate same-tablo ownership, and preserve existing status/position.
+ Update `TabloTasksTabHandler` and full detail rendering to load etapes, counts, and current filter. Support `?etape=unassigned` and `?etape={uuid}`. Use existing four kanban columns for filtered task slices.
+ Do not add quick assignment dropdowns on task cards.
+
+
+ cd backend && just generate && go test ./internal/web -run 'TestTaskEditRendersEtapeSelector|TestTaskUpdateAssignsAndUnassignsEtape|TestTaskAssignmentRejectsForeignEtape|TestTasksTabUnassignedFilter|TestTaskUpdate|TestTasksKanbanRenders' -count=1
+
+
+ - `backend/templates/tasks.templ` contains visible label text `Etape` and option text `No etape`.
+ - `backend/internal/web/handlers_tasks.go` validates submitted etape ids against the current tablo before insert/update.
+ - `TaskUpdateHandler` still preserves existing status and position.
+ - `TabloTasksTabHandler` supports `etape=unassigned` and concrete etape UUID filters.
+ - Targeted tests listed in verify pass.
+
+
+
+
+
+
+Run:
+- `cd backend && just generate`
+- `cd backend && go test ./internal/web -count=1`
+- `git diff --check`
+
+
+
+- ETAPE-03 is fully delivered.
+- ETAPE-05 filtering behavior is delivered for All, Unassigned, and specific etapes.
+- Existing task create/update tests still pass.
+
diff --git a/.planning/phases/09-etapes/09-04-PLAN.md b/.planning/phases/09-etapes/09-04-PLAN.md
new file mode 100644
index 0000000..4b4258c
--- /dev/null
+++ b/.planning/phases/09-etapes/09-04-PLAN.md
@@ -0,0 +1,147 @@
+---
+phase: 09-etapes
+plan: 04
+type: execute
+wave: 4
+depends_on:
+ - 09-03-PLAN.md
+files_modified:
+ - backend/internal/web/handlers_tasks.go
+ - backend/templates/tasks.templ
+ - backend/templates/etapes.templ
+ - backend/internal/web/handlers_tasks_test.go
+ - backend/internal/web/handlers_etapes_test.go
+autonomous: false
+requirements: [ETAPE-01, ETAPE-02, ETAPE-03, ETAPE-04, ETAPE-05, ETAPE-06]
+
+must_haves:
+ truths:
+ - "D-08/D-09/D-10: drag/drop reorder preserves etape_id and remains status-scoped"
+ - "D-01 through D-16: all context decisions are represented in source behavior or verified manual behavior"
+ - "UI-SPEC: top strip, chips, active state, selector copy, and delete copy are visually present"
+ - "Research Pitfall 2: every UpdateTask call preserves etape_id unless assignment is intentionally changing"
+ artifacts:
+ - path: "backend/internal/web/handlers_tasks.go"
+ provides: "reorder paths that preserve etape assignment"
+ - path: "backend/internal/web/handlers_tasks_test.go"
+ provides: "filtered reorder and etape preservation regression tests"
+---
+
+
+Vertical slice 4: harden the completed etape feature by proving drag/drop reorder still works with etape filters and assignments, then run final automated and browser verification. This closes the phase against all ETAPE requirements and the UI contract.
+
+
+
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-CONTEXT.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-RESEARCH.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-UI-SPEC.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-VALIDATION.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/09-etapes/09-03-PLAN.md
+
+
+
+T-09-01 Cross-tablo assignment remains covered by prior tests and must not regress.
+T-09-05 Data loss remains covered by delete-unassign test and browser verification.
+T-09-06 Integrity regression: task reorder must never clear or change `etape_id` unless assignment form submitted that change.
+
+
+
+
+
+ Task 1: Add reorder preservation regression tests
+
+ - backend/internal/web/handlers_tasks_test.go
+
+
+ - backend/internal/web/handlers_tasks_test.go
+ - backend/internal/web/handlers_tasks.go
+ - .planning/phases/09-etapes/09-RESEARCH.md
+
+
+ Add failing regression tests:
+ 1. `TestTaskReorderPreservesEtapeAssignment` creates assigned task, posts single-task reorder to another status, and verifies `etape_id` remains unchanged.
+ 2. `TestFilteredTaskReorderPreservesHiddenTasks` creates tasks in the same status assigned to different etapes, submits reorder for only one visible filtered task, and verifies the hidden task still exists with its original `etape_id`.
+ 3. `TestTaskOrderPersistsWithEtapeFilter` verifies filtered board ordering remains stable after reload.
+
+
+ cd backend && go test ./internal/web -run 'TestTaskReorderPreservesEtapeAssignment|TestFilteredTaskReorderPreservesHiddenTasks|TestTaskOrderPersistsWithEtapeFilter' -count=1
+
+
+ - `handlers_tasks_test.go` contains all three named regression tests.
+ - Tests fail before implementation if reorder does not preserve `etape_id`.
+
+
+
+
+ Task 2: Preserve etape assignment through reorder and refresh counts
+
+ - backend/internal/web/handlers_tasks.go
+ - backend/templates/tasks.templ
+ - backend/templates/etapes.templ
+
+
+ - backend/internal/web/handlers_tasks.go
+ - backend/templates/tasks.templ
+ - backend/templates/etapes.templ
+ - .planning/phases/09-etapes/09-RESEARCH.md
+
+
+ Update every `UpdateTaskParams` call in `TaskReorderHandler` to pass the existing task's `etape_id`. Confirm the single-task path and array Sortable.js path both preserve assignment.
+ Ensure reorder responses under an active etape filter return the filtered board or whole Tasks tab consistently enough that chip counts and active state remain correct after HTMX settles.
+ Keep task position assignment status-scoped; do not add `(etape_id, status, position)` ordering semantics.
+
+
+ cd backend && just generate && go test ./internal/web -run 'TestTaskReorderPreservesEtapeAssignment|TestFilteredTaskReorderPreservesHiddenTasks|TestTaskOrderPersistsWithEtapeFilter|TestTaskReorderCrossColumn|TestTaskReorderSameColumn|TestTaskOrderPersists' -count=1
+
+
+ - Every `UpdateTaskParams` call in `handlers_tasks.go` includes the current `EtapeID` or intentional parsed assignment value.
+ - `TaskReorderHandler` does not parse or write assignment from drag/drop payload.
+ - Reorder regression tests and existing reorder tests pass.
+
+
+
+
+ Task 3: Final automated and browser verification checkpoint
+
+
+ - .planning/phases/09-etapes/09-UI-SPEC.md
+ - .planning/phases/09-etapes/09-VALIDATION.md
+ - backend/internal/web/handlers_tasks_test.go
+ - backend/internal/web/handlers_etapes_test.go
+
+
+ Run full generated-code and backend verification, then perform browser verification:
+ 1. `cd backend && just generate`
+ 2. `cd backend && go test ./... -count=1`
+ 3. `git diff --check`
+ 4. Start `cd backend && just dev`.
+ 5. In a browser, open a tablo Tasks tab, create two etapes, create assigned and unassigned tasks, filter by an etape, filter by `Unassigned`, edit assignment from the task form, drag a filtered task across columns, delete an etape, and confirm affected tasks move to Unassigned rather than disappearing.
+
+
+ cd backend && just generate && go test ./... -count=1
+ Browser confirms all ETAPE-01..06 success criteria and UI-SPEC interaction contract.
+
+
+ - `cd backend && go test ./... -count=1` exits 0.
+ - `git diff --check` exits 0.
+ - Browser verification confirms top-strip chips, counts, filters, assignment selector, reorder, and delete-unassign behavior.
+ - User approves the checkpoint before Phase 9 is marked complete.
+
+
+
+
+
+
+Run:
+- `cd backend && just generate`
+- `cd backend && go test ./... -count=1`
+- `git diff --check`
+- browser verification from Task 3
+
+
+
+- All ETAPE-01..06 requirements are complete.
+- Existing kanban status and ordering behavior remains intact.
+- UI-SPEC is satisfied in the first functional UI.
+- Human verification checkpoint is approved.
+
diff --git a/.planning/phases/09-etapes/09-PATTERNS.md b/.planning/phases/09-etapes/09-PATTERNS.md
new file mode 100644
index 0000000..4daeb9f
--- /dev/null
+++ b/.planning/phases/09-etapes/09-PATTERNS.md
@@ -0,0 +1,56 @@
+# Phase 09 - Pattern Map
+
+## Purpose
+
+Concrete code patterns Phase 9 should reuse when adding etapes to the Go/HTMX task board.
+
+## Closest Analogs
+
+### Schema And SQLC
+
+- `backend/migrations/0004_tasks.sql` - task enum/table/index pattern; down migration order matters.
+- `backend/migrations/0005_files.sql` - tablo-owned child table pattern for resources under a tablo.
+- `backend/internal/db/queries/tasks.sql` - current task CRUD query shape and `GetTaskByID` ownership binding.
+- `backend/internal/db/queries/files.sql` - tablo-scoped file query analog if present during execution.
+
+### Handlers
+
+- `backend/internal/web/handlers_tasks.go` - task CRUD, inline HTMX fragments, status parsing, reorder behavior.
+- `backend/internal/web/handlers_files.go` - additional tablo-scoped resource handlers and ownership helpers.
+- `backend/internal/web/handlers_tablos.go` - canonical `loadOwnedTablo` helper and 404-on-authz-failure pattern.
+
+### Templates
+
+- `backend/templates/tasks.templ` - kanban board, task card, create/edit/delete fragments, OOB add-slot reset.
+- `backend/templates/tablos.templ` - tab content boundary and `TasksTabFragment` integration point.
+- `backend/internal/web/ui/button.css` and `backend/internal/web/ui` helpers - existing button/card/badge styling.
+
+### Tests
+
+- `backend/internal/web/handlers_tasks_test.go` - DB-backed handler tests for task create/update/reorder/delete/ownership.
+- `backend/internal/web/handlers_files_test.go` - useful analog for tablo-scoped resource ownership tests if present during execution.
+
+## Reuse Rules
+
+- Keep `TaskColumns` and the current kanban column DOM stable.
+- New etape routes should live under `/tablos/{id}/...` and call `loadOwnedTablo` before any etape lookup.
+- Etape-specific lookup must bind both `id` and `tablo_id`, mirroring `GetTaskByID`.
+- HTMX filter operations should swap `#tasks-tab` or `#tab-content`, not just `#kanban-board`, so chip active state and counts stay accurate.
+- Task reorder must preserve `etape_id` once the task row grows that field.
+- Use `just generate` after migration, sqlc query, or templ changes.
+
+## Planned File Map
+
+| Target file | Role | Closest analog |
+|-------------|------|----------------|
+| `backend/migrations/0007_etapes.sql` | Add etape table and nullable task assignment | `backend/migrations/0004_tasks.sql`, `backend/migrations/0005_files.sql` |
+| `backend/internal/db/queries/etapes.sql` | Etape CRUD and position queries | `backend/internal/db/queries/tasks.sql` |
+| `backend/internal/db/queries/tasks.sql` | Add nullable etape assignment and filtered task lists | existing same file |
+| `backend/internal/web/handlers_etapes.go` | Etape CRUD/reorder handlers | `backend/internal/web/handlers_tasks.go`, `backend/internal/web/handlers_files.go` |
+| `backend/internal/web/handlers_tasks.go` | Task assignment and filter-aware task loading | existing same file |
+| `backend/internal/web/router.go` | Mount etape routes and preserve task routes | existing task/file route groups |
+| `backend/templates/etapes.templ` | Etape strip, chips, forms, delete confirmation | `backend/templates/tasks.templ` |
+| `backend/templates/tasks.templ` | Add etape selector and preserve kanban | existing same file |
+| `backend/templates/tablos.templ` | Expand `TasksTabFragment` signature and wrapper | existing same file |
+| `backend/internal/web/handlers_etapes_test.go` | Etape behavior and ownership coverage | `backend/internal/web/handlers_tasks_test.go` |
+| `backend/internal/web/handlers_tasks_test.go` | Assignment/filter/reorder regression coverage | existing same file |