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

<objective>
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.
</objective>

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

<threat_model>
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.
</threat_model>

<tasks>

<task type="auto">
  <name>Task 1: Add RED coverage for first etape slice</name>
  <files>
    - backend/internal/web/handlers_etapes_test.go
    - backend/internal/web/handlers_tasks_test.go
  </files>
  <read_first>
    - 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
  </read_first>
  <action>
    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.
  </action>
  <verify>
    <automated>cd backend && go test ./internal/web -run 'TestEtapeCreateRendersChipAndCount|TestTaskCreateAssignsEtape|TestEtapeFilterRendersExistingKanbanColumns' -count=1</automated>
  </verify>
  <acceptance_criteria>
    - `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.
  </acceptance_criteria>
</task>

<task type="auto">
  <name>Task 2: Implement the thinnest create-filter-assigned-task slice</name>
  <files>
    - 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
  </files>
  <read_first>
    - 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
  </read_first>
  <action>
    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.
  </action>
  <verify>
    <automated>cd backend && just generate && go test ./internal/web -run 'TestEtapeCreateRendersChipAndCount|TestTaskCreateAssignsEtape|TestEtapeFilterRendersExistingKanbanColumns|TestTaskCreate' -count=1</automated>
  </verify>
  <acceptance_criteria>
    - `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.
  </acceptance_criteria>
</task>

</tasks>

<verification>
Run:
- `cd backend && just generate`
- `cd backend && go test ./internal/web -count=1`
- `git diff --check`
</verification>

<success_criteria>
- 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.
</success_criteria>
