---
phase: 20-tablo-detail-kanban-restyle
plan: 02
type: execute
wave: 2
depends_on:
  - 20-01
files_modified:
  - go-backend/internal/web/views/tablo_detail.templ
  - go-backend/internal/web/views/tablo_detail_view.go
  - go-backend/internal/web/handlers/tablo_detail_tab.go
autonomous: true
requirements:
  - DETAIL-01
  - TASK-01

must_haves:
  truths:
    - "Tablo detail page renders a header with tablo name as h1 (font-size 1.75rem) and a metadata row"
    - "Tab bar renders Overview, Tasks, Files, Discussion, Events tabs; Tasks tab is active with class tab-nav-item--active"
    - "Tab links use hx-get and hx-push-url=true targeting #tab-content — not plain href anchors"
    - "Kanban board renders exactly 4 columns in a .tablo-kanban-board flex container"
    - "Each column uses class tablo-kanban-column with data-status attribute set to the column ID"
    - "Each column contains a hidden reorder form with id reorder-form-{status} for Sortable.js onEnd"
    - "Each task card uses class task-card and carries data-task-id"
    - "Drag handle element uses class task-drag-handle and is a child of .task-card"
    - "Empty column renders a .tablo-kanban-empty element with text 'Aucune tâche'"
    - "Rendered HTML from GET /tablos/{id} contains substring `initTabloDetailSortable`"
    - "Etapes section renders below the kanban board listing each etape name and its task count"
  artifacts:
    - path: "go-backend/internal/web/views/tablo_detail.templ"
      provides: "TabloDetailPage + all sub-components (header, tab bar, kanban board, task card, etapes section)"
      exports: ["TabloDetailPage"]
    - path: "go-backend/internal/web/views/tablo_detail_view.go"
      provides: "TabloDetailPage real templ component replaces stub from Plan 01"
      contains: "func TabloDetailPage"
    - path: "go-backend/internal/web/handlers/tablo_detail_tab.go"
      provides: "GET /tablos/{tabloID}/{tab} handler for tab content swaps"
      exports: ["GetTabloDetailTab"]
  key_links:
    - from: "go-backend/internal/web/views/tablo_detail.templ"
      to: "go-backend/internal/web/views/tablo_detail_view.go"
      via: "TabloDetailViewModel struct fields"
      pattern: "TabloDetailViewModel"
    - from: ".tablo-kanban-board .sortable-column"
      to: "POST /tablos/{id}/tasks/reorder"
      via: "Sortable.js onEnd -> #reorder-form-{status} submit"
      pattern: "reorder-form-"
    - from: ".tablo-tab-bar a[hx-get]"
      to: "GET /tablos/{tabloID}/{tab}"
      via: "HTMX hx-get + hx-push-url=true"
      pattern: "hx-get"
---

<objective>
Build the tablo_detail.templ components that render the tablo detail page: header section, HTMX tab bar, kanban board with 4 columns (each with a hidden reorder form), task cards, empty state, etapes section, and Sortable.js initialization script. Replace the stub TabloDetailPage function in tablo_detail_view.go with the real templ component. Add a minimal tab handler for the tasks tab.

Purpose: This plan produces the full HTML surface for DETAIL-01 and TASK-01. All CSS class names used here are defined in Plan 03's CSS work, so the visual output is unstyled at first but structurally correct.

Output: tablo_detail.templ with all sub-components; tablo_detail_view.go stub removed and replaced by templ-generated component; tablo_detail_tab.go with GetTabloDetailTab handler.
</objective>

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

<context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/ROADMAP.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/20-tablo-detail-kanban-restyle/20-UI-SPEC.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/20-tablo-detail-kanban-restyle/20-RESEARCH.md

<interfaces>
<!-- Key types and contracts the executor needs. -->

From go-backend/internal/web/views/tablo_detail_view.go (created in Plan 01):
```go
type TabloDetailTaskView struct {
    ID         string
    Title      string
    DeleteHref string
    EditHref   string
}

type TabloDetailColumnView struct {
    ID         string  // "todo" | "in_progress" | "in_review" | "done"
    Label      string  // "À faire" | "En cours" | "En révision" | "Terminé"
    Tasks      []TabloDetailTaskView
    CreateHref string  // "/tablos/{id}/tasks/create?status={colID}"
}

type TabloDetailEtapeView struct {
    ID        string
    Name      string
    TaskCount int
}

type TabloDetailViewModel struct {
    TabloID       string
    TabloName     string
    Color         string
    Initial       string
    OwnerName     string
    DueDate       string  // empty string → show "—" in template
    StatusLabel   string
    StatusTone    string  // "warning" | "success" | "info"
    Progress      int     // 0-100
    ProgressLabel string  // "{N}%"
    Columns       []TabloDetailColumnView  // always 4 columns
    Etapes        []TabloDetailEtapeView   // may be empty
}
```

From go-backend/internal/web/ui/badge.templ:
```go
// ui.Badge(ui.BadgeProps{Label: string, Variant: string}) templ.Component
// badge variants: use badgeVariantForTone(tone string) -> string (function in views package)
// tone strings: "success", "warning", "info", "danger", "default"
```

From go-backend/internal/web/ui/icon_button.templ:
```go
// ui.IconButton(ui.IconButtonProps{Label, Icon, Variant, Tone, Type, Attrs}) templ.Component
// IconButtonVariantNeutral / IconButtonToneGhost — for ghost action buttons
// IconButtonVariantDanger + IconButtonToneGhost — for delete button
```

From go-backend/internal/web/views/tablos.templ (for ActionIcon usage):
```go
// ActionIcon(kind string) templ.Component — available in views package
// Icons used: "calendar", "message-circle", "user"
```

From go-backend/internal/web/views/tasks.templ (for drag-and-drop reorder pattern):
```
// Sortable.js init fires on DOMContentLoaded AND htmx:afterSettle
// Uses: .sortable-column class on task list containers
// Uses: data-status attribute on column containers
// Uses: .task-drag-handle handle class
// Uses: .task-card draggable class
// Reorder form: hidden form that submits task order to POST /tablos/{id}/tasks/reorder
//   IMPORTANT: form id must be "reorder-form-{status}" — Sortable.js onEnd looks up
//   getElementById('reorder-form-' + el.dataset.status)
// Guard: if (el._sortable) return; prevents double-init after HTMX swap (Pitfall 4)
```

CSS class contract (defined in Plan 03, used here):
```
.tablo-detail-page          — outer container, px-6 pt-6
.tablo-detail-header        — header element
.tablo-detail-title-row     — flex row with avatar + h1
.tablo-detail-avatar        — 48×48 colored circle with initial
.tablo-detail-title         — h1, font-size 1.75rem, weight 600
.tablo-metadata-row         — flex row with gap:24px, padding-block:16px
.tablo-meta-segment         — each metadata segment in the row
.tablo-meta-progress        — the progress segment (bar + label)
.tablo-progress-bar         — progress fill (uses brand-primary, NOT project-color)
.tablo-tab-bar              — tab navigation below header
.tab-nav-item               — each tab item (existing class)
.tab-nav-item--active       — active tab (existing class)
.tablo-kanban-board         — flex container for columns
.tablo-kanban-column        — each 18rem column
.tablo-kanban-column-header — column header area
.tablo-kanban-column-title  — h3 inside column header
.tablo-kanban-task-count    — count pill
.tablo-kanban-add-link      — "+ Ajouter" ghost link
.task-list.sortable-column  — the sortable task list container
.task-card                  — individual task card (replaces .task-row)
.task-card-top-row          — row with drag handle, title, delete icon
.task-drag-handle           — drag handle (opacity 0 at rest, 1 on card hover)
.task-card-title            — task title text
.task-card-delete           — delete icon button (opacity 0 at rest)
.tablo-kanban-empty         — empty column message
.tablo-etapes-section       — etapes section below kanban board
.tablo-etape-row            — each etape row (name + task count)
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: TabloDetailPage templ component — header, HTMX tab bar, kanban board, etapes section</name>
  <files>go-backend/internal/web/views/tablo_detail.templ, go-backend/internal/web/handlers/tablo_detail_tab.go</files>
  <read_first>
    - go-backend/internal/web/views/tablo_detail_view.go (TabloDetailViewModel fields — just created in Plan 01)
    - go-backend/internal/web/views/tablos.templ (TabloGridCard pattern, ActionIcon usage, badgeVariantForTone function name)
    - go-backend/internal/web/views/tasks.templ (TasksKanbanLayout — for contrast; tablo-detail kanban is SEPARATE and must NOT reuse TasksKanbanLayout; also read the Sortable.js init script and reorder form pattern)
    - go-backend/internal/web/ui/badge.templ (BadgeProps fields)
    - go-backend/internal/web/ui/icon_button.templ (IconButtonProps fields)
    - go-backend/internal/web/ui/app.css lines 882-900 (existing .tab-nav, .tab-nav-item classes)
    - go-backend/internal/web/views/discussion_view.go (for badgeVariantForTone function — confirm it's in views package)
    - go-backend/internal/web/handlers/tablos.go (renderTablosResponse pattern — for how DashboardContentSwapWithMainClass is called, to replicate in tab handler)
  </read_first>
  <action>
    Create go-backend/internal/web/views/tablo_detail.templ in package views. Import "xtablo-backend/internal/web/ui" and "strconv".

    Define the following templ components in this order:

    1. TabloDetailPage(vm TabloDetailViewModel) — outer wrapper:
       - Outer div class="tablo-detail-page"
       - @TabloDetailHeader(vm)
       - @TabloDetailTabBar(vm.TabloID)
       - div id="tab-content":
         * @TabloDetailKanbanBoard(vm.Columns, vm.TabloID)
         * if len(vm.Etapes) > 0: @TabloDetailEtapesSection(vm.Etapes)
       - @TabloDetailSortableScript(vm.TabloID)

    2. TabloDetailHeader(vm TabloDetailViewModel) — header section:
       - header element class="tablo-detail-header"
       - Inner div class="tablo-detail-title-row":
         * div class="tablo-detail-avatar" style={ "background:" + vm.Color } containing { vm.Initial }
         * h1 class="tablo-detail-title" containing { vm.TabloName }
       - div class="tablo-metadata-row":
         * span class="tablo-meta-segment" containing: 24×24 avatar div with owner initial + " " + { vm.OwnerName }
         * span class="tablo-meta-segment" containing: @ActionIcon("calendar") + if vm.DueDate != "" { vm.DueDate } else { "—" }
         * span class="tablo-meta-segment" containing: @ui.Badge(ui.BadgeProps{Label: vm.StatusLabel, Variant: badgeVariantForTone(vm.StatusTone)})
         * span class="tablo-meta-segment tablo-meta-progress" containing:
           - div class="project-progress-track" style="min-width:120px" with inner div class="tablo-progress-bar" style={ "width:" + vm.ProgressLabel }
           - strong { vm.ProgressLabel }

    3. TabloDetailTabBar(tabloID string) — HTMX tab navigation:
       - nav element class="tablo-tab-bar"
       - 5 anchor elements for: "Vue d'ensemble", "Tâches", "Fichiers", "Discussion", "Événements"
       - Tab slugs: "overview", "tasks", "files", "discussion", "events"
       - Each anchor MUST use HTMX attributes (per UI-SPEC locked interaction contract):
           hx-get="/tablos/{tabloID}/{slug}"
           hx-target="#tab-content"
           hx-push-url="true"
           class="tab-nav-item"
       - "Tâches" tab (tasks slug): class="tab-nav-item tab-nav-item--active" (Phase 20 always shows tasks tab active)
       - Do NOT use plain href="#slug" anchors — the UI-SPEC requires HTMX push-url navigation for all tabs.
       - Use templ.SafeURL for the hx-get value to satisfy templ's URL safety requirements.

    4. TabloDetailKanbanBoard(columns []TabloDetailColumnView, tabloID string) — renders the kanban board:
       - Outer div class="tablo-kanban-board"
       - For each column: @TabloDetailKanbanColumn(col, tabloID)

    5. TabloDetailKanbanColumn(col TabloDetailColumnView, tabloID string) — single column:
       - div class="tablo-kanban-column" data-status={ col.ID }
       - div class="tablo-kanban-column-header":
         * span class="tablo-kanban-column-title" containing { col.Label }
         * span class="tablo-kanban-task-count" containing { strconv.Itoa(len(col.Tasks)) }
         * a class="tablo-kanban-add-link" href={ templ.SafeURL(col.CreateHref) } containing "+ Ajouter"
       - div id={ "task-list-" + col.ID } class="task-list sortable-column" data-status={ col.ID }:
         * if len(col.Tasks) == 0: div class="tablo-kanban-empty" { "Aucune tâche" }
         * else: for _, task := range col.Tasks { @TabloDetailTaskCard(task, tabloID) }
       - Hidden reorder form (REQUIRED for Sortable.js onEnd):
           <form id={ "reorder-form-" + col.ID }
                 style="display:none"
                 hx-post={ "/tablos/" + tabloID + "/tasks/reorder?status=" + col.ID }
                 hx-target={ "#task-list-" + col.ID }
                 hx-swap="innerHTML">
             <!-- Sortable.js onEnd will populate this form's inputs before calling requestSubmit() -->
           </form>
       - div id={ "create-zone-" + col.ID } (empty create zone for HTMX task create swap)

    6. TabloDetailTaskCard(task TabloDetailTaskView, tabloID string) — task card:
       - article element: class="task-card" data-task-id={ task.ID }
       - div class="task-card-top-row":
         * span class="task-drag-handle" aria-hidden="true" { "⠿" }
         * span class="task-card-title" { task.Title }
         * @ui.IconButton(ui.IconButtonProps{
             Label: "Supprimer la tâche", Icon: "trash",
             Variant: ui.IconButtonVariantDanger, Tone: ui.IconButtonToneGhost,
             Type: "button",
             Attrs: templ.Attributes{
               "class": "task-card-delete",
               "hx-delete": task.DeleteHref,
               "hx-target": "#app-main-content",
               "hx-swap": "outerHTML",
               "hx-confirm": "Supprimer cette tâche ?",
             },
           })

    7. TabloDetailEtapesSection(etapes []TabloDetailEtapeView) — etapes list below kanban:
       - section element class="tablo-etapes-section"
       - h2 or h3 heading: "Étapes" (French)
       - ul element: for each etape, render a li class="tablo-etape-row" containing:
           span class="tablo-etape-name" { etape.Name }
           span class="tablo-etape-count" { strconv.Itoa(etape.TaskCount) + " tâche(s)" }

    8. TabloDetailSortableScript(tabloID string) — JavaScript block:
       Use templ.Raw to emit the Sortable.js init script that:
       - Defines function initTabloDetailSortable()
       - Calls document.querySelectorAll('.sortable-column').forEach(function(el) { if (el._sortable) return; el._sortable = Sortable.create(el, { group: 'tablo-tasks', animation: 150, handle: '.task-drag-handle', draggable: '.task-card', onEnd: function(evt) { var form = document.getElementById('reorder-form-' + el.dataset.status); if (form) form.requestSubmit(); } }); })
       - Calls document.addEventListener('DOMContentLoaded', initTabloDetailSortable)
       - Calls document.addEventListener('htmx:afterSettle', initTabloDetailSortable)
       Wrap in <script> tag.

    Create go-backend/internal/web/handlers/tablo_detail_tab.go in package handlers. This handles GET /tablos/{tabloID}/{tab} for HTMX tab swaps:
    - Define GetTabloDetailTab() http.HandlerFunc on *AuthHandler
    - Auth check; redirect /login on failure
    - Parse tabloID from path; 400 on invalid UUID
    - Parse tab slug from path (chi: r.PathValue("tab"))
    - For tab == "tasks": fetch tablo + tasks, build TabloDetailViewModel, return views.TabloDetailKanbanBoard(vm.Columns, tabloID.String()) rendered as HTMX partial (no dashboard layout wrapper — just the fragment)
    - For other tabs ("overview", "files", "discussion", "events"): return a simple HTML fragment: `<div class="tab-coming-soon">Cette section arrive bientôt.</div>`
    - Register route in router.go: mux.Get("/tablos/{tabloID}/{tab}", authHandler.GetTabloDetailTab()) — add immediately after the mux.Get("/tablos/{tabloID}", ...) line

    CRITICAL rules:
    - Tab links MUST use hx-get + hx-target="#tab-content" + hx-push-url="true" — plain href="#slug" anchors are FORBIDDEN per the UI-SPEC interaction contract
    - Each kanban column MUST contain a hidden form with id="reorder-form-{col.ID}" — Sortable.js onEnd calls getElementById('reorder-form-' + el.dataset.status)
    - Do NOT import or call TasksKanbanLayout — tablo-detail kanban is a separate surface (RESEARCH Anti-Patterns)
    - Do NOT add a task view switcher (Board/List/Gantt) — UI-SPEC says this is Phase 21
    - All strings in French per UI-SPEC Copywriting Contract
    - Use strconv.Itoa for integer→string conversion in templ (not fmt.Sprintf)
    - The tablo-progress-bar uses class "tablo-progress-bar" NOT "project-progress-bar" to avoid Pitfall 5 (project-color vs brand-primary)
    - import "strconv" in the templ file header
  </action>
  <verify>
    <automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/go-backend && just generate 2>&1 | tail -5 && go build ./... 2>&1</automated>
  </verify>
  <done>just generate succeeds (tablo_detail_templ.go generated); go build ./... exits 0; tablo_detail.templ defines TabloDetailPage, TabloDetailHeader, TabloDetailTabBar, TabloDetailKanbanBoard, TabloDetailKanbanColumn, TabloDetailTaskCard, TabloDetailEtapesSection, TabloDetailSortableScript; each column contains a hidden reorder form; tab links use hx-get.</done>
</task>

<task type="auto">
  <name>Task 2: Replace TabloDetailPage stub + wire templ component + run handler tests</name>
  <files>go-backend/internal/web/views/tablo_detail_view.go, go-backend/router.go</files>
  <read_first>
    - go-backend/internal/web/views/tablo_detail_view.go (current stub — remove the ComponentFunc stub)
    - go-backend/internal/web/views/tablo_detail.templ (just created — TabloDetailPage is now a real templ component)
    - go-backend/internal/web/views/tablo_detail_templ.go (generated by just generate — confirm TabloDetailPage signature)
    - go-backend/internal/web/handlers/tablo_detail_test.go (handler tests that verify 200 + tablo name in body + "initTabloDetailSortable" in body)
    - go-backend/router.go (add GET /tablos/{tabloID}/{tab} route for GetTabloDetailTab)
  </read_first>
  <action>
    Edit go-backend/internal/web/views/tablo_detail_view.go:
    - Remove the stub TabloDetailPage function (the templ.ComponentFunc one added in Plan 01 Task 2)
    - Remove "context", "fmt", "io", and "github.com/a-h/templ" imports added for the stub (they are no longer needed in the view.go file since TabloDetailPage is now generated by templ from tablo_detail.templ)
    - Keep all struct definitions and NewTabloDetailViewModel + computeTabloProgress functions

    After removing the stub, the real TabloDetailPage comes from tablo_detail_templ.go (generated by just generate). Verify with go build.

    Edit go-backend/router.go:
    - Add mux.Get("/tablos/{tabloID}/{tab}", authHandler.GetTabloDetailTab()) immediately after the mux.Get("/tablos/{tabloID}", ...) line added in Plan 01

    Then run the full handler tests to confirm the integration is correct:
    - TestGetTabloDetailPage_Returns200 should now render real HTML with the tablo name inside the TabloDetailHeader h1
    - TestGetTabloDetailPage_ContainsSortableScript should find "initTabloDetailSortable" in the rendered HTML (now from the real templ component's script block)
    - Update any test assertions if needed: the response body should contain the tablo name string
  </action>
  <verify>
    <automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/go-backend && go test ./... -run "TestGetTabloDetailPage|TestComputeTabloProgress|TestNewTabloDetail|TestTabloDetail" -count=1 -v 2>&1 | tail -30</automated>
  </verify>
  <done>All TestGetTabloDetailPage_* and view model tests pass; go build ./... exits 0; no stub function remains in tablo_detail_view.go; generated tablo_detail_templ.go exports TabloDetailPage(TabloDetailViewModel) templ.Component; router.go contains both /tablos/{tabloID} and /tablos/{tabloID}/{tab} routes.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Template rendering | TabloDetailViewModel data flows from handler into templ — ensure no raw HTML injection |
| Tab slug routing | {tab} path value used to select partial response — must not allow arbitrary content injection |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-20-04 | Tampering | TabloDetailTaskCard — task.Title rendered in HTML | mitigate | templ auto-escapes all { expr } interpolations — no raw HTML emission for user data |
| T-20-05 | Tampering | Sortable.js onEnd — data-status attribute used in form ID | mitigate | data-status comes from hardcoded column IDs ("todo", "in_progress", "in_review", "done") — not user input |
| T-20-07 | Tampering | GetTabloDetailTab — {tab} slug used in switch, not reflected into HTML | mitigate | Tab slug used only as switch key; "coming soon" text is hardcoded; tab slug never interpolated into HTML |
| T-20-SC | Tampering | npm/pip/cargo installs | accept | No package installs — pure templ + CSS |
</threat_model>

<verification>
After Plan 02:
- `just generate` exits 0 with tablo_detail_templ.go created
- `go build ./...` exits 0
- `go test ./... -run "TestGetTabloDetailPage|TestComputeTabloProgress|TestNewTabloDetail|TestTabloDetail" -count=1` all pass
- `go test ./... -count=1` full suite green
- tablo_detail.templ defines 8 templ components including TabloDetailPage, TabloDetailTaskCard, TabloDetailEtapesSection
- Task cards use class="task-card" and data-task-id attribute
- Task list containers use class="task-list sortable-column"
- Drag handle uses class="task-drag-handle"
- Empty column uses class="tablo-kanban-empty" with text "Aucune tâche"
- Each column has a hidden form with id="reorder-form-{status}"
- Tab links use hx-get + hx-target="#tab-content" + hx-push-url="true"
- Etapes section renders below kanban board when vm.Etapes is non-empty
</verification>

<success_criteria>
- GET /tablos/{validID} (authenticated) returns 200 with h1 containing tablo name, .tablo-kanban-board div, 4 .tablo-kanban-column elements, hidden reorder forms, and etapes section in the response body
- Full test suite green
- Sortable.js init script present in rendered HTML with DOMContentLoaded and htmx:afterSettle listeners
- Tab links use hx-get targeting #tab-content (not plain href anchors)
- Hidden reorder forms present per column: id="reorder-form-todo", id="reorder-form-in_progress", etc.
- No TasksKanbanLayout referenced in tablo_detail.templ
</success_criteria>

<output>
Create `.planning/phases/20-tablo-detail-kanban-restyle/20-02-SUMMARY.md` when done.
</output>
