diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index fed0787..d755dcd 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -111,6 +111,7 @@ Plans:
**Mode:** mvp
**Status:** Pending
**Requirements:** DETAIL-01, DETAIL-02, DETAIL-03, DETAIL-04
+**Plans:** 4 plans
**Success Criteria:**
1. Tablo detail header uses project-card-top layout with title, avatar, and action controls
2. Task kanban uses tasks-section design: section header with add button, task rows with checkbox and meta
@@ -118,7 +119,20 @@ Plans:
4. Files section uses the table component with consistent row actions
5. All existing task, etape, and file handler tests pass unchanged
-**User-in-loop:** Approve kanban layout shape (whether columns stay horizontal or fold to list) and etape grouping UI before implementation.
+Plans:
+**Wave 1**
+- [ ] 16-01-PLAN.md — CSS foundation + icons: append CSS Sections 19–25 to app.css; add download + chat icon cases to UIIcon switch
+
+**Wave 2** *(blocked on Wave 1 completion)*
+- [ ] 16-02-PLAN.md — Header + tab nav + overview tab: restyle TabloDetailPage header (project-card-top), metadata row, tab nav (design token classes), move desc to overview tab, remove EtapeStrip call from TasksTabFragment
+
+**Wave 3** *(blocked on Wave 2 completion)*
+- [ ] 16-03-PLAN.md — Kanban + etape grouping: groupTasksByEtape helper, restyled KanbanBoard/Column/TaskCard, EtapeStrip OOB removal, handlers_tasks.go call sites updated
+
+**Wave 4** *(blocked on Wave 3 completion)*
+- [ ] 16-04-PLAN.md — Files section: @ui.Table, @ui.EmptyState, FileListRow as tr, FileDeleteConfirmFragment as tr, browser verify checkpoint
+
+**User-in-loop:** Browser verify checkpoint in Plan 04 — approve visual result before considering the phase complete.
### Phase 17: Chat & Planning
**Goal:** Restyle the discussion view and planning page using design system components to make them visually consistent with the rest of the app.
diff --git a/.planning/phases/16-tablo-detail/16-01-PLAN.md b/.planning/phases/16-tablo-detail/16-01-PLAN.md
new file mode 100644
index 0000000..9b80d08
--- /dev/null
+++ b/.planning/phases/16-tablo-detail/16-01-PLAN.md
@@ -0,0 +1,202 @@
+---
+phase: 16-tablo-detail
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - backend/internal/web/ui/icon_button.templ
+ - backend/internal/web/ui/app.css
+autonomous: true
+requirements:
+ - DETAIL-01
+ - DETAIL-02
+ - DETAIL-03
+ - DETAIL-04
+
+must_haves:
+ truths:
+ - "icon_button.templ UIIcon switch contains a case for 'download' that emits a download SVG arrow"
+ - "icon_button.templ UIIcon switch contains a case for 'chat' that emits a speech-bubble SVG"
+ - "app.css contains .tasks-section, .task-row, .task-check, .task-list, .task-body, .task-meta, .tasks-add-button CSS rules using var(--...) tokens"
+ - "app.css contains .project-progress-track and .project-progress-bar CSS rules"
+ - "app.css contains .tab-nav, .tab-nav-item, .tab-nav-item.is-active, .tab-nav-item:hover CSS rules"
+ - "app.css contains .tablo-metadata-row and .tablo-metadata-date CSS rules"
+ - "app.css contains .kanban-column and scoped .kanban-column .tasks-section-header h3 { font-size: 1rem } override"
+ - "app.css contains .etape-group-header, .etape-group-color-dot, .etape-group-label CSS rules"
+ - "app.css contains .task-list-empty CSS rule"
+ - "templ generate && go build ./... exits 0 after changes"
+ artifacts:
+ - path: backend/internal/web/ui/icon_button.templ
+ provides: Download and chat icon SVG cases in UIIcon switch
+ contains: "case \"download\":"
+ - path: backend/internal/web/ui/app.css
+ provides: All new CSS sections for Phase 16 (19–25)
+ contains: ".tasks-section {"
+ key_links:
+ - from: backend/internal/web/ui/icon_button.templ
+ to: backend/internal/web/ui/icon_button_templ.go
+ via: templ generate
+ pattern: "case \"download\""
+ - from: backend/internal/web/ui/app.css
+ to: browser render
+ via: static asset embed
+ pattern: ".tab-nav-item.is-active"
+---
+
+## Phase Goal
+
+**As a** tablo owner, **I want to** see a fully restyled tablo detail page — header, kanban, etapes, and files — **so that** the detail view looks visually consistent with the design system established in Phases 13–15.
+
+
+Add the two missing icon SVG cases (`download`, `chat`) to `UIIcon` in `icon_button.templ`, and append all Phase 16 CSS sections (19–25) to `app.css`. This is the shared substrate that plans 02–04 build on — without it, `@ui.IconButton` with `Icon: "download"` or `Icon: "chat"` renders the icon name as raw text.
+
+Purpose: Unblock header restyling (Discussion/Download IconButtons), kanban restyling (task-row CSS), and files restyling (table + icons).
+Output: Updated `icon_button.templ` with `download` and `chat` cases; `app.css` with Sections 19–25 appended.
+
+
+
+@/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
+
+
+
+@.planning/ROADMAP.md
+@.planning/phases/16-tablo-detail/16-CONTEXT.md
+@.planning/phases/16-tablo-detail/16-RESEARCH.md
+@.planning/phases/16-tablo-detail/16-PATTERNS.md
+@.planning/phases/16-tablo-detail/16-UI-SPEC.md
+
+
+
+
+From backend/internal/web/ui/icon_button.templ (current UIIcon switch structure):
+- Package: `package ui`
+- Switch starts at line ~18, cases: "plus", "grid3x3", "list", "filter", "search", "calendar", "pencil", "trash"
+- `default:` case emits `{kind}` (renders icon name as text — this is the bug)
+- Insert `case "download":` and `case "chat":` BEFORE the `default:` case
+
+From backend/internal/web/ui/app.css (current state):
+- File is 450 lines; ends after Section 18 (project-card-top, project-avatar)
+- Line 355-361: `.overview-section-heading h3, .tasks-section-header h3` shared rule exists (do NOT add a second `.tasks-section-header h3` rule; add only the scoped `.kanban-column .tasks-section-header h3 { font-size: 1rem; }` override)
+- Sections to append: 19 (tasks-section block), 20 (progress bar), 21 (tab nav), 22 (metadata row), 23 (kanban-column wrapper), 24 (etape group), 25 (task-list-empty)
+- All token names: `var(--color-surface-default)`, `var(--color-border-subtle)`, `var(--color-border-muted)`, `var(--color-border-strong)`, `var(--color-text-secondary)`, `var(--color-text-muted)`, `var(--color-text-faint)`, `var(--color-text-primary)`, `var(--color-text-brand)`, `var(--color-text-brand-strong)`, `var(--color-text-inverse)`, `var(--color-surface-muted)`, `var(--color-surface-neutral-hover)`, `var(--color-brand-primary)`, `var(--color-project-fallback)`, `var(--project-color, var(--color-project-fallback))`
+
+
+
+
+
+
+ Task 1: Add `download` and `chat` icon cases to UIIcon switch
+ backend/internal/web/ui/icon_button.templ
+
+ - backend/internal/web/ui/icon_button.templ (read entire file — ~80 lines — before editing; understand the full switch structure and the `default:` position)
+
+
+ Read the full `icon_button.templ` file. Find the UIIcon switch's `default:` case. Insert two new cases immediately before `default:`:
+
+ Case "download": emit an SVG with `viewBox="0 0 24 24"`, `fill="none"`, `stroke="currentColor"`, `stroke-width="2"`, `stroke-linecap="round"`, `stroke-linejoin="round"`, `aria-hidden="true"`. Contents: `path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"`, `polyline points="7 10 12 15 17 10"`, `line x1="12" x2="12" y1="15" y2="3"`.
+
+ Case "chat": emit an SVG with same attributes. Contents: `path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"`.
+
+ Do NOT modify any other part of the file. Do NOT change the `default:` case or any existing cases.
+
+ After editing, run: `just generate` (or `templ generate ./backend/...`) to regenerate `icon_button_templ.go`.
+
+
+ grep -c 'case "download"' /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/internal/web/ui/icon_button.templ && grep -c 'case "chat"' /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/internal/web/ui/icon_button.templ && cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source && go build ./backend/...
+
+
+ - `icon_button.templ` contains `case "download":` with a polyline + line SVG body
+ - `icon_button.templ` contains `case "chat":` with a path SVG body
+ - Both new cases appear before the `default:` case in the switch
+ - `go build ./backend/...` exits 0 (templ-generated file updated, no compile errors)
+ - The `default:` case and all 8 existing cases are unchanged
+
+ UIIcon switch contains "download" and "chat" cases; project compiles cleanly.
+
+
+
+ Task 2: Append CSS Sections 19–25 to app.css
+ backend/internal/web/ui/app.css
+
+ - backend/internal/web/ui/app.css (read lines 340–450 to confirm the end of the file and the existing shared `.tasks-section-header h3` rule at lines 355–361; verify no duplication before appending)
+ - .planning/phases/16-tablo-detail/16-PATTERNS.md (Pattern Assignments section: "New CSS blocks to append — Blocks 1, 2, 3" — these are the verbatim CSS blocks to append)
+ - .planning/phases/16-tablo-detail/16-UI-SPEC.md (New CSS Classes Required section — additional property values that refine the blocks)
+
+
+ Append the following seven CSS sections to the END of `backend/internal/web/ui/app.css`. Do NOT modify any existing rules. The shared `.tasks-section-header h3` rule at lines ~355-361 already exists — do NOT add another one; only add the scoped `.kanban-column .tasks-section-header h3 { font-size: 1rem; }` override.
+
+ Append in order:
+
+ Section 19 — Tasks section (ported from go-backend): `.tasks-section`, `.tasks-section-header`, `.tasks-add-button`, `.task-list`, `.task-row`, `.task-row:hover`, `.task-check`, `.task-check.is-complete`, `.task-body`, `.task-body p`, `.task-row.is-complete .task-body p`, `.task-meta`. All values from PATTERNS.md Block 1 verbatim.
+
+ Section 20 — Progress bar: `.project-progress-track`, `.project-progress-bar`. All values from PATTERNS.md Block 2 verbatim.
+
+ Section 21 — Tab nav: `.tab-nav`, `.tab-nav-item`, `.tab-nav-item.is-active`, `.tab-nav-item:hover:not(.is-active)`. Values from PATTERNS.md Block 3 / UI-SPEC.
+
+ Section 22 — Tablo detail metadata row: `.tablo-metadata-row`, `.tablo-metadata-date`, `.tablo-metadata-date svg`. Values from PATTERNS.md Block 3 / UI-SPEC.
+
+ Section 23 — Kanban column wrapper: `.kanban-column { flex-shrink: 0; width: 18rem; }` and `.kanban-column .tasks-section-header h3 { font-size: 1rem; }`.
+
+ Section 24 — Etape group sub-headings: `.etape-group-header`, `.etape-group-color-dot`, `.etape-group-label`, `.etape-group-label.is-unassigned`. Values from PATTERNS.md Block 3 / UI-SPEC.
+
+ Section 25 — Empty task list: `.task-list-empty { color: var(--color-text-faint); font-size: 0.875rem; font-style: italic; padding: 0.75rem 1rem; }`.
+
+ Every CSS value must use `var(--...)` tokens. No hardcoded hex values. No Tailwind utility classes.
+
+
+ grep -c "\.tasks-section {" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/internal/web/ui/app.css && grep -c "\.tab-nav-item\.is-active" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/internal/web/ui/app.css && grep -c "\.kanban-column" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/internal/web/ui/app.css && grep -c "\.etape-group-header" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/internal/web/ui/app.css && grep -c "\.task-list-empty" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/internal/web/ui/app.css
+
+
+ - `app.css` contains exactly one `.tasks-section {` rule (no duplication)
+ - `app.css` contains `.tab-nav-item.is-active` with `border-bottom-color: var(--color-brand-primary)` and `color: var(--color-text-brand)`
+ - `app.css` contains `.kanban-column` with `width: 18rem`
+ - `app.css` contains `.kanban-column .tasks-section-header h3` with `font-size: 1rem`
+ - `app.css` contains `.etape-group-header` with `background: var(--color-surface-muted)`
+ - `app.css` contains `.task-list-empty` with `font-style: italic`
+ - `app.css` contains `.project-progress-track` and `.project-progress-bar`
+ - `grep -c "#[0-9a-fA-F]\{3,6\}" app.css` returns 0 for newly appended lines (no hardcoded hex in new rules)
+ - `go build ./backend/...` exits 0 (CSS is a static asset, build verifies no templ regressions)
+
+ app.css has all Phase 16 CSS sections appended; no hardcoded hex values; project compiles cleanly.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Static asset → browser | CSS and templ-generated HTML served as static embed; no user input processed |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-16-01-01 | Tampering | app.css static embed | accept | CSS is embedded at build time; no runtime user modification possible; existing CSP controls unchanged |
+| T-16-01-02 | Information Disclosure | SVG icon markup | accept | SVG icons are decorative, contain no user data; aria-hidden="true" on all decorative SVGs |
+
+No new trust boundaries. No user input, no authentication changes, no handler changes.
+
+
+
+After both tasks complete:
+```bash
+cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source
+go test ./backend/internal/web/... -count=1
+```
+All existing tests must pass. The CSS and icon changes do not affect test behavior (tests verify handler output, not CSS class names in most cases).
+
+
+
+- `icon_button.templ` has `case "download":` and `case "chat":` before `default:`
+- `app.css` has 7 new sections (19–25) appended with no hardcoded hex values
+- `go test ./backend/internal/web/... -count=1` passes with no new failures
+- `go build ./backend/...` exits 0
+
+
+
diff --git a/.planning/phases/16-tablo-detail/16-02-PLAN.md b/.planning/phases/16-tablo-detail/16-02-PLAN.md
new file mode 100644
index 0000000..ba1491f
--- /dev/null
+++ b/.planning/phases/16-tablo-detail/16-02-PLAN.md
@@ -0,0 +1,235 @@
+---
+phase: 16-tablo-detail
+plan: 02
+type: execute
+wave: 2
+depends_on:
+ - 16-01
+files_modified:
+ - backend/templates/tablos.templ
+autonomous: true
+requirements:
+ - DETAIL-01
+
+must_haves:
+ truths:
+ - "Tablo detail page header uses .project-card-top layout with color avatar, inline-editable title zone, and action controls"
+ - "Action controls use @ui.IconButton for Discussion and Delete, @ui.Button for Invite Member — no hardcoded #804EEC hex"
+ - "Metadata row uses .tablo-metadata-row with created date, @ui.Badge status pill, and .project-progress-track/bar"
+ - "Tab nav uses .tab-nav / .tab-nav-item / .tab-nav-item.is-active classes replacing hardcoded Tailwind hex classes"
+ - "Description zone renders inside TabloOverviewTabFragment (not in the persistent header)"
+ - "EtapeStrip is no longer called from TasksTabFragment; KanbanBoard call passes etapes parameter"
+ - "All existing tablo handler tests pass unchanged"
+ artifacts:
+ - path: backend/templates/tablos.templ
+ provides: Restyled TabloDetailPage header, tab nav, overview tab, TasksTabFragment EtapeStrip removal
+ contains: ".project-card-top"
+ - path: backend/templates/tablos_templ.go
+ provides: Generated Go from restyled tablos.templ
+ exports: []
+ key_links:
+ - from: backend/templates/tablos.templ TabloDetailPage
+ to: backend/internal/web/ui/app.css .project-card-top
+ via: CSS class on div element
+ pattern: "project-card-top"
+ - from: backend/templates/tablos.templ TabloOverviewTabFragment
+ to: TabloDescDisplay / TabloDescEditFragment
+ via: "@TabloDescDisplay(tablo, csrfToken)" call inside overview tab
+ pattern: "TabloDescDisplay"
+ - from: backend/templates/tablos.templ TasksTabFragment
+ to: KanbanBoard
+ via: "@KanbanBoard(tablo.ID, csrfToken, tasks, filter, etapes)"
+ pattern: "KanbanBoard.*etapes"
+---
+
+
+Restyle `TabloDetailPage` header area (project-card-top layout, color avatar, action controls), metadata row, tab nav bar, and overview tab content. Move the description zone from the persistent header into `TabloOverviewTabFragment`. Remove the `@EtapeStrip` call from `TasksTabFragment` and update the `KanbanBoard` call to pass `etapes`. All HTMX wiring, inline-edit zones, delete flow, and tab routing are preserved unchanged.
+
+Purpose: Deliver DETAIL-01 — the tablo detail header matches the project-card-top design.
+Output: Restyled `tablos.templ` with no hardcoded hex values in the detail page section.
+
+
+
+@/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
+
+
+
+@.planning/ROADMAP.md
+@.planning/phases/16-tablo-detail/16-CONTEXT.md
+@.planning/phases/16-tablo-detail/16-RESEARCH.md
+@.planning/phases/16-tablo-detail/16-PATTERNS.md
+@.planning/phases/16-tablo-detail/16-UI-SPEC.md
+@.planning/phases/16-tablo-detail/16-01-SUMMARY.md
+
+
+
+
+From backend/templates/tablos.templ (current state — read the file before editing):
+
+TabloDetailPage signature (line ~200):
+ templ TabloDetailPage(tablo sqlc.Tablo, tasks []sqlc.Task, etapes []sqlc.Etape, counts EtapeTaskCounts, filter EtapeFilter, csrfToken string, activeTab string, user auth.User)
+
+Current header structure uses raw inline Tailwind classes with hardcoded #804EEC hex. Replace with design token classes.
+
+Color avatar pattern from TabloProjectCard (lines ~103–112) — reuse for detail header, adding first-character text:
+ if tablo.Color.Valid && tablo.Color.String != "" {
+ span class="project-avatar" style="background-color: {tablo.Color.String}"
+ if len(tablo.Title) > 0 { string([]rune(tablo.Title)[0:1]) }
+ } else {
+ span class="project-avatar"
+ if len(tablo.Title) > 0 { string([]rune(tablo.Title)[0:1]) }
+ }
+
+Tab nav active state — current pattern (lines ~306–325) uses inline Tailwind hex classes per-tab. Replace ALL 5 tabs (overview, tasks, files, discussion, events):
+ if activeTab == "overview" || activeTab == "" { class="tab-nav-item is-active" } else { class="tab-nav-item" }
+
+EtapeStrip call in TasksTabFragment (line ~412): DELETE this call
+ @EtapeStrip(tablo.ID, etapes, counts, filter, csrfToken, false)
+
+KanbanBoard call in TasksTabFragment (line ~413): UPDATE to add etapes parameter
+ @KanbanBoard(tablo.ID, csrfToken, tasks, filter)
+ → @KanbanBoard(tablo.ID, csrfToken, tasks, filter, etapes)
+
+Delete button in detail header — per Pitfall 6: do NOT modify TabloDeleteButtonFragment (used by dashboard cards).
+Create an INLINE IconButton inside a .tablo-delete-zone div in TabloDetailPage header:
+ div class="tablo-delete-zone"
+ @ui.IconButton(ui.IconButtonProps{Label: "Delete tablo", Icon: "trash", Variant: ui.IconButtonVariantDanger, Tone: ui.IconButtonToneGhost, Type: "button", Attrs: templ.Attributes{"hx-get": "/tablos/{id}/delete-confirm", "hx-target": "closest .tablo-delete-zone", "hx-swap": "outerHTML"}})
+
+Discussion action in header: @ui.IconButton with Icon "chat", Label "Discussion", linking/navigating to discussion tab (ghost/neutral variant). Since Discussion is navigation, wrap in an tag or use hx-get to load the discussion tab fragment:
+ a href="/tablos/{id}/discussion" hx-get="/tablos/{id}/discussion" hx-target="#tab-content" hx-swap="innerHTML" hx-push-url="true" class="tab-nav-item" aria-label="Go to Discussion tab"
+ @ui.IconButton(...Icon: "chat"...) -- OR render the button label directly as link text with chat icon
+
+Invite button: @ui.Button(ui.ButtonProps{Label: "Invite Member", Variant: ui.ButtonVariantGhost, Tone: ui.ButtonToneSolid, Size: ui.SizeMD, Type: "button"})
+
+Metadata row:
+ div class="tablo-metadata-row"
+ div class="tablo-metadata-date"
+ [calendar SVG 1rem aria-hidden]
+ span "Created"
+ span { tablo.CreatedAt.Time.Format("Jan 2, 2006") }
+ @ui.Badge(ui.BadgeProps{Label: "In progress", Variant: ui.BadgeVariantPrimary})
+ div class="project-progress-track"
+ div class="project-progress-bar" style="width: 0%;"
+
+Overview tab (TabloOverviewTabFragment): move the tablo-desc-zone call here (currently in persistent header). The component already exists — just relocate the @TabloDescDisplay(tablo, csrfToken) call inside a div class="tablo-desc-zone" inside TabloOverviewTabFragment.
+
+Tab nav wrapper: change the outer div for the tab links to div class="tab-nav" (replaces existing raw flex Tailwind classes).
+
+ui package variant identifiers (from backend/internal/web/ui/variants.go):
+ ui.ButtonVariantGhost, ui.ButtonVariantDefault, ui.ButtonToneSolid
+ ui.IconButtonVariantDanger, ui.IconButtonVariantNeutral, ui.IconButtonToneGhost
+ ui.BadgeVariantPrimary, ui.BadgeVariantInfo
+ ui.SizeMD
+
+
+
+
+
+
+ Task 1: Restyle TabloDetailPage header, metadata row, and tab nav
+ backend/templates/tablos.templ
+
+ - backend/templates/tablos.templ (read the FULL file before editing — you need to understand the current header structure, tab nav structure, action control positions, and the persistent desc zone location; the file is ~650 lines)
+ - backend/internal/web/ui/variants.go (read to confirm exact Go identifiers for variants/tones: ButtonVariantGhost, IconButtonVariantDanger, IconButtonToneGhost, BadgeVariantPrimary, SizeMD — use these exact names)
+ - .planning/phases/16-tablo-detail/16-PATTERNS.md (section "backend/templates/tablos.templ" — color avatar pattern, tab nav pattern, IconButton for delete, metadata row pattern, description zone relocation)
+
+
+ Edit `backend/templates/tablos.templ` to restyle `TabloDetailPage`. Make the following targeted changes:
+
+ 1. HEADER — replace the current header div (which uses inline Tailwind flex classes with #804EEC) with a `.project-card-top` layout:
+ - Outer div: `class="project-card-top"`
+ - Left: `div class="project-card-title-row"` containing the project-avatar span (with tablo color + first char of title per PATTERNS.md color avatar pattern) followed by the existing `.tablo-title-zone` inline-edit zone (`@TabloTitleDisplay(tablo, csrfToken)` preserved as-is)
+ - Right: action controls row with gap, containing: (a) Discussion `@ui.IconButton` with `Icon: "chat"`, `Label: "Discussion"`, ghost/neutral variant, wrapped so it navigates to the discussion tab (use hx-get="/tablos/{id}/discussion" hx-target="#tab-content" hx-swap="innerHTML" hx-push-url="true" as Attrs); (b) `@ui.Button` for "Invite Member" with `ButtonVariantGhost`; (c) a `.tablo-delete-zone` div containing the trash `@ui.IconButton` (DO NOT use or modify `@TabloDeleteButtonFragment`)
+
+ 2. METADATA ROW — replace the current metadata span/div elements (which use hardcoded color classes) with `div class="tablo-metadata-row"` containing: tablo-metadata-date div (calendar SVG + "Created" + formatted date), `@ui.Badge` for "In progress" with `BadgeVariantPrimary`, and the progress track/bar divs using `.project-progress-track` + `.project-progress-bar` CSS classes (per PATTERNS.md metadata row pattern)
+
+ 3. TAB NAV — change the outer container of the 5 tab links to `div class="tab-nav"`. For each of the 5 tabs (overview, tasks, files, discussion, events): replace the long inline Tailwind class strings (which contain `text-[#804EEC]`, `border-[#804EEC]`, etc.) with the simple conditional: `if activeTab == "{tab}" { class="tab-nav-item is-active" } else { class="tab-nav-item" }`. Preserve all `hx-get`, `hx-target`, `hx-swap`, `hx-push-url`, and `href` attributes on each tab link unchanged.
+
+ 4. DESCRIPTION ZONE RELOCATION — Remove the `tablo-desc-zone` div (containing `@TabloDescDisplay`) from the persistent header area (above the tab nav). Move it into `TabloOverviewTabFragment` as the first element inside that component. The `TabloDescDisplay` and `TabloDescEditFragment` components themselves are not modified — only the call site location changes.
+
+ After editing, run: `just generate` to regenerate `tablos_templ.go`.
+
+
+ grep -c "project-card-top" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tablos.templ && grep -c "tab-nav-item is-active" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tablos.templ && grep -c "tablo-metadata-row" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tablos.templ && grep -c "804EEC" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tablos.templ && cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source && go test ./backend/internal/web/... -run TestTablos -count=1
+
+
+ - `tablos.templ` contains `class="project-card-top"` in TabloDetailPage header
+ - `tablos.templ` contains `class="tab-nav-item is-active"` (applied conditionally to the active tab)
+ - `tablos.templ` contains `class="tablo-metadata-row"`
+ - `tablos.templ` contains `@ui.Badge(ui.BadgeProps{Label: "In progress", Variant: ui.BadgeVariantPrimary})`
+ - `tablos.templ` contains `.project-progress-track` and `.project-progress-bar` class references
+ - `grep -c "#804EEC" backend/templates/tablos.templ` returns 0 (all hardcoded hex removed from detail page)
+ - `tablos.templ` contains `@TabloDescDisplay` call inside `TabloOverviewTabFragment` (not in the persistent header)
+ - `go test ./backend/internal/web/... -run TestTablos -count=1` passes
+ - `TabloDeleteButtonFragment` function body is unchanged (grep its content to confirm)
+
+ TabloDetailPage header uses project-card-top layout; tab nav uses design token classes; description is in overview tab; no hardcoded hex values remain in the detail page header/nav section.
+
+
+
+ Task 2: Remove EtapeStrip call from TasksTabFragment and update KanbanBoard call site
+ backend/templates/tablos.templ
+
+ - backend/templates/tablos.templ (already read in Task 1 — find TasksTabFragment, line ~407–420; confirm EtapeStrip and KanbanBoard call positions)
+ - .planning/phases/16-tablo-detail/16-RESEARCH.md (Pattern 4: EtapeStrip Removal and OOB Impact — explains which EtapeStrip call sites exist in tablos.templ; this task handles Site 1 only; Sites 2 and 3 in tasks.templ are handled in Plan 03)
+
+
+ In `TasksTabFragment` inside `tablos.templ`:
+
+ 1. DELETE the line: `@EtapeStrip(tablo.ID, etapes, counts, filter, csrfToken, false)` — remove this entire call (per D-E01; the EtapeStrip UI strip is removed). Also remove any surrounding `div id="etape-strip"` wrapper if one exists.
+
+ 2. UPDATE the KanbanBoard call: change `@KanbanBoard(tablo.ID, csrfToken, tasks, filter)` to `@KanbanBoard(tablo.ID, csrfToken, tasks, filter, etapes)` — adding `etapes` as the 5th argument. Note: the `KanbanBoard` templ signature update happens in Plan 03; this call site change MUST be done simultaneously with that signature change to avoid a compile error. If Plan 03 has not yet run, add a TODO comment next to this line noting the parameter will be added when KanbanBoard signature is updated in Plan 03. The `templ generate && go build ./...` gate in Plan 03 will catch any mismatch.
+
+ After editing, run: `just generate` to regenerate `tablos_templ.go`.
+
+
+ grep -c "EtapeStrip" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tablos.templ && grep "KanbanBoard" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tablos.templ
+
+
+ - `grep "EtapeStrip" backend/templates/tablos.templ` returns 0 (the EtapeStrip call is gone from TasksTabFragment)
+ - `grep "KanbanBoard" backend/templates/tablos.templ` shows the call includes `etapes` as a parameter (or has a TODO comment pending Plan 03)
+ - `go test ./backend/internal/web/... -run TestTablos -count=1` passes (or compile error from KanbanBoard signature mismatch is expected and noted — will be resolved in Plan 03)
+
+ EtapeStrip is not called from TasksTabFragment; KanbanBoard call is updated to pass etapes.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Go template → HTML response | tablos.templ renders tablo data from DB into HTML; existing ownership guard (`loadOwnedTablo`) unchanged |
+| HTMX GET requests | hx-get endpoints for tab fragments are authenticated routes; no change to auth middleware |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-16-02-01 | Tampering | TabloDetailPage inline-edit zones | accept | tablo-title-zone and tablo-desc-zone HTMX endpoints validate CSRF token and ownership — unchanged; only CSS classes change |
+| T-16-02-02 | Spoofing | Discussion/Delete action controls | accept | Auth middleware on all /tablos/{id}/* routes unchanged; restyling does not change route protection |
+| T-16-02-03 | Information Disclosure | tablo.Color inline style | accept | tablo.Color is user-supplied but rendered as a CSS value in a style attribute; no script injection possible via `background-color: {value}` — browsers do not execute style values as JS |
+
+
+
+```bash
+cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source
+go test ./backend/internal/web/... -count=1
+```
+All existing tablo tests pass. No hardcoded #804EEC hex in tablos.templ for the detail page section.
+
+
+
+- `tablos.templ` header uses `.project-card-top`, `.project-avatar` (with first char), `.tablo-title-zone`
+- All 5 tab nav items use `.tab-nav-item` / `.tab-nav-item.is-active` (no hardcoded hex)
+- Description zone is inside `TabloOverviewTabFragment`
+- `EtapeStrip` is not called from `TasksTabFragment`
+- `KanbanBoard` call passes `etapes`
+- `go test ./backend/internal/web/... -count=1` passes
+
+
+
diff --git a/.planning/phases/16-tablo-detail/16-03-PLAN.md b/.planning/phases/16-tablo-detail/16-03-PLAN.md
new file mode 100644
index 0000000..e33292e
--- /dev/null
+++ b/.planning/phases/16-tablo-detail/16-03-PLAN.md
@@ -0,0 +1,274 @@
+---
+phase: 16-tablo-detail
+plan: 03
+type: execute
+wave: 3
+depends_on:
+ - 16-02
+files_modified:
+ - backend/templates/tasks.templ
+ - backend/internal/web/handlers_tasks.go
+autonomous: true
+requirements:
+ - DETAIL-02
+ - DETAIL-03
+
+must_haves:
+ truths:
+ - "KanbanBoard templ signature accepts etapes []sqlc.Etape as 5th parameter"
+ - "KanbanColumn renders tasks grouped by etape using groupTasksByEtape helper; unassigned tasks appear last"
+ - "Each kanban column uses .kanban-column / .tasks-section / .tasks-section-header / .task-list CSS layout"
+ - "TaskCard uses .task-row layout: .task-check + .task-body + trash @ui.IconButton"
+ - "AddTaskTrigger uses .tasks-add-button class (not raw ui-button classes)"
+ - "EtapeStrip OOB calls are removed from TaskCardGone and TaskCardOOB (etapes/counts params kept)"
+ - "Both KanbanBoard call sites in handlers_tasks.go pass etapes as 5th argument"
+ - "All 19 existing task handler tests pass unchanged"
+ artifacts:
+ - path: backend/templates/tasks.templ
+ provides: groupTasksByEtape helper, EtapeGroup type, EtapeGroupHeader component, restyled KanbanBoard/Column/TaskCard
+ contains: "groupTasksByEtape"
+ - path: backend/internal/web/handlers_tasks.go
+ provides: Updated KanbanBoard call sites (lines ~594, ~645)
+ contains: "KanbanBoard.*etapes"
+ key_links:
+ - from: backend/templates/tasks.templ groupTasksByEtape
+ to: backend/templates/tasks.templ KanbanColumn
+ via: "groups := groupTasksByEtape(tasks, etapes)" expression inside KanbanColumn
+ pattern: "groupTasksByEtape"
+ - from: backend/internal/web/handlers_tasks.go
+ to: backend/templates/tasks.templ KanbanBoard
+ via: "templates.KanbanBoard(tablo.ID, csrf.Token(r), tasks, filter, etapes)"
+ pattern: "KanbanBoard.*etapes"
+---
+
+
+Restyle the task kanban board (DETAIL-02) and implement server-side etape grouping (DETAIL-03). Changes: add `groupTasksByEtape` helper and `EtapeGroup` type to `tasks.templ`; add `EtapeGroupHeader` templ component; update `KanbanBoard` and `KanbanColumn` signatures to accept `etapes []sqlc.Etape`; restyle `KanbanColumn` with `.tasks-section` layout; restyle `TaskCard` with `.task-row` layout; restyle `AddTaskTrigger` with `.tasks-add-button`; remove `@EtapeStrip` OOB calls from `TaskCardGone` and `TaskCardOOB`; update both `KanbanBoard` call sites in `handlers_tasks.go`.
+
+Purpose: Deliver DETAIL-02 (tasks-section kanban) and DETAIL-03 (etape grouping, EtapeStrip removal).
+Output: Restyled kanban with etape-grouped task rows; all 19 task handler tests pass.
+
+
+
+@/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
+
+
+
+@.planning/ROADMAP.md
+@.planning/phases/16-tablo-detail/16-CONTEXT.md
+@.planning/phases/16-tablo-detail/16-RESEARCH.md
+@.planning/phases/16-tablo-detail/16-PATTERNS.md
+@.planning/phases/16-tablo-detail/16-UI-SPEC.md
+@.planning/phases/16-tablo-detail/16-02-SUMMARY.md
+
+
+
+
+From backend/templates/tasks.templ (current state — read the file before editing):
+
+Existing groupTasksByStatus (lines ~10-18) — model for groupTasksByEtape:
+ func groupTasksByStatus(tasks []sqlc.Task) map[sqlc.TaskStatus][]sqlc.Task {
+ result := make(map[sqlc.TaskStatus][]sqlc.Task, len(TaskColumns))
+ for _, t := range tasks {
+ result[t.Status] = append(result[t.Status], t)
+ }
+ return result
+ }
+
+Current KanbanBoard signature (line ~23):
+ templ KanbanBoard(tabloID uuid.UUID, csrfToken string, tasks []sqlc.Task, filter EtapeFilter)
+
+Target KanbanBoard signature:
+ templ KanbanBoard(tabloID uuid.UUID, csrfToken string, tasks []sqlc.Task, filter EtapeFilter, etapes []sqlc.Etape)
+
+KanbanColumn — receives tasks for one status. After change, also receives etapes:
+ templ KanbanColumn(tabloID uuid.UUID, status sqlc.TaskStatus, tasks []sqlc.Task, csrfToken string, filter EtapeFilter, etapes []sqlc.Etape)
+
+EtapeGroup type to add (before groupTasksByEtape func):
+ type EtapeGroup struct {
+ EtapeID string
+ EtapeTitle string
+ EtapeColor string
+ Tasks []sqlc.Task
+ }
+
+groupTasksByEtape logic:
+ - Build etapeID→Etape map from etapes slice for O(1) lookup
+ - Use etapes slice ORDER to determine group order (not map iteration)
+ - For each etape in order: collect tasks whose EtapeID matches; only add group if non-empty
+ - sqlc.Task.EtapeID field: check the actual type (likely pgtype.UUID or *uuid.UUID) — use appropriate nil/zero check
+ - At the end: collect tasks with nil/zero EtapeID → append as EtapeGroup{EtapeID: "", EtapeTitle: "No etape", EtapeColor: "", Tasks: unassigned}
+ - If zero etapes are defined (etapes slice is empty), treat all tasks as unassigned
+
+TaskCardGone current signature (line ~384): has etapes []sqlc.Etape and counts EtapeTaskCounts params
+TaskCardOOB current signature (line ~396): same
+Action: remove the @EtapeStrip(..., true) OOB call from both; KEEP the etapes and counts params (to avoid handler signature changes); add TODO comment
+
+KanbanBoard call sites in handlers_tasks.go:
+ Line ~594: templates.KanbanBoard(tablo.ID, csrf.Token(r), tasks, filter) → add etapes (from loadTasksTabData return)
+ Line ~645: templates.KanbanBoard(tablo.ID, csrf.Token(r), tasks, filter) → add etapes (etapes is already available in scope)
+
+loadTasksTabData return signature (check actual): returns (tasks, etapes, counts, filter, ok bool) — etapes is the 2nd return value
+
+From backend/internal/web/ui/variants.go (for TaskCard IconButton):
+ ui.IconButtonVariantDanger, ui.IconButtonToneGhost
+
+From backend/internal/web/ui/app.css (Section 23 added in Plan 01):
+ .kanban-column { flex-shrink: 0; width: 18rem; }
+ .tasks-section { border: 1px solid var(--color-border-subtle); border-radius: 1rem; overflow: hidden; }
+ .tasks-section-header { display: flex; justify-content: space-between; align-items: center; padding: 1.2rem 1rem; }
+ .task-list { display: flex; flex-direction: column; }
+ .task-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.9rem 1rem; border-bottom: 1px solid var(--color-border-muted); }
+ .tasks-add-button { display: inline-flex; align-items: center; gap: 0.5rem; ... }
+
+
+
+
+
+
+ Task 1: Add groupTasksByEtape helper, EtapeGroup type, EtapeGroupHeader component; update KanbanBoard/Column signatures and restyled column/card/trigger
+ backend/templates/tasks.templ
+
+ - backend/templates/tasks.templ (read the FULL file before editing — ~420 lines; you need the current: groupTasksByStatus pattern, KanbanBoard body, KanbanColumn body, TaskCard body, AddTaskTrigger body, TaskCardGone and TaskCardOOB bodies, existing import block, sqlc.Task.EtapeID field type)
+ - backend/internal/db/sqlc/querier.go or backend/internal/db/sqlc/models.go (grep for "EtapeID" field on Task struct to confirm the exact type — pgtype.UUID, *uuid.UUID, or sql.NullString)
+ - .planning/phases/16-tablo-detail/16-PATTERNS.md (sections: groupTasksByEtape helper, KanbanBoard signature change, KanbanColumn restyled, TaskCard restyled, EtapeGroupHeader component, EtapeStrip OOB removal)
+
+
+ Edit `backend/templates/tasks.templ` to make the following changes. Read the entire file first to understand current structure before starting any edits.
+
+ 1. ADD EtapeGroup type and groupTasksByEtape function after the existing `groupTasksByStatus` function (before the `KanbanBoard` templ declaration):
+ - `EtapeGroup` struct: fields EtapeID string, EtapeTitle string, EtapeColor string, Tasks []sqlc.Task
+ - `groupTasksByEtape(tasks []sqlc.Task, etapes []sqlc.Etape) []EtapeGroup`:
+ * Build index map from etape ID string to sqlc.Etape for O(1) lookup
+ * Iterate etapes in slice order; for each etape, collect tasks matching that etape ID; only append a group if it has tasks
+ * Append an unassigned group (EtapeID: "", EtapeTitle: "No etape", EtapeColor: "") for tasks with nil/zero EtapeID — check the actual EtapeID field type (pgtype.UUID: use !t.EtapeID.Valid; *uuid.UUID: use t.EtapeID == nil; uuid.UUID zero value: use t.EtapeID == uuid.Nil)
+ * If all tasks are assigned (no unassigned), omit the unassigned group entirely
+
+ 2. ADD EtapeGroupHeader templ component (after the groupTasksByEtape function):
+ `templ EtapeGroupHeader(group EtapeGroup)` rendering a `div class="etape-group-header"`:
+ - If group.EtapeColor != "": render `span class="etape-group-color-dot" style="background-color: {group.EtapeColor}"`
+ - If group.EtapeID == "": render `span class="etape-group-label is-unassigned"` with group.EtapeTitle
+ - Else: render `span class="etape-group-label"` with group.EtapeTitle
+
+ 3. UPDATE KanbanBoard signature: add `etapes []sqlc.Etape` as 5th parameter. Inside KanbanBoard, pass `etapes` through to each `@KanbanColumn(...)` call.
+
+ 4. UPDATE KanbanColumn signature: add `etapes []sqlc.Etape` as 6th parameter. Restyle the column structure:
+ - Outer: `div class="kanban-column"` (replaces the current flex-shrink-0 w-72 div)
+ - Inner: `div class="tasks-section"` containing:
+ * `div class="tasks-section-header"`: left side has h3 with TaskColumnLabels[status] + span with task-count-badge `@ui.Badge(...BadgeVariantInfo...)`, right side has `div id="add-task-slot-{status}"` containing `@AddTaskTrigger(...)`
+ * `div class="task-list sortable-column" data-status="{status}" id="column-{status}" aria-label="{label} column"`:
+ - If len(tasks) == 0: render `p class="task-list-empty" "No tasks yet"`
+ - Else: compute `groups := groupTasksByEtape(tasks, etapes)`, then `for _, group := range groups { @EtapeGroupHeader(group); for _, task := range group.Tasks { @TaskCard(tabloID, task, csrfToken) } }`
+ - Preserve existing `data-status` and `id="column-..."` attributes for Sortable.js compatibility
+
+ 5. RESTYLE TaskCard: replace the current `.task-card bg-white rounded border border-slate-200...` inner div with `.task-row task-card` div:
+ - Outer zone wrapper: keep `div class="task-card-zone" id="task-{task.ID.String()}"` unchanged (HTMX swap target)
+ - Inner: `div class="task-row task-card" data-task-id="{task.ID.String()}" hx-get=... hx-target="closest .task-card-zone" hx-swap="outerHTML" role="button" aria-label="Edit task: {task.Title}"`
+ - Children: `div class="task-check"` (round checkbox, role="checkbox" aria-checked="false") + `div class="task-body"` (p with task.Title) + trash `@ui.IconButton(ui.IconButtonProps{Label: "Delete task: {task.Title}", Icon: "trash", Variant: ui.IconButtonVariantDanger, Tone: ui.IconButtonToneGhost, Type: "button", Attrs: templ.Attributes{"hx-get": "/tablos/{tabloID}/tasks/{task.ID}/delete-confirm", "hx-target": "closest .task-card-zone", "hx-swap": "outerHTML"}})`
+ - Remove the old drag handle div (⠿)
+
+ 6. RESTYLE AddTaskTrigger: change the button class from the current `ui-button ui-button-soft...` compound to simply `tasks-add-button`. Preserve all hx-* attributes.
+
+ 7. REMOVE EtapeStrip OOB calls: in TaskCardGone and TaskCardOOB, delete the `@EtapeStrip(tabloID, etapes, counts, filter, csrfToken, true)` lines. Keep the `etapes []sqlc.Etape` and `counts EtapeTaskCounts` parameters on both component signatures. Add `// TODO: remove etapes and counts params after Phase 16 cleanup` comment.
+
+ After editing, run: `just generate` to regenerate `tasks_templ.go`.
+
+
+ grep -c "groupTasksByEtape" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tasks.templ && grep -c "EtapeGroupHeader" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tasks.templ && grep -c "kanban-column" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tasks.templ && grep -c "tasks-section" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tasks.templ && grep -c "task-row" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tasks.templ && grep -c "EtapeStrip" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tasks.templ
+
+
+ - `tasks.templ` contains `func groupTasksByEtape(tasks []sqlc.Task, etapes []sqlc.Etape) []EtapeGroup`
+ - `tasks.templ` contains `type EtapeGroup struct`
+ - `tasks.templ` contains `templ EtapeGroupHeader(group EtapeGroup)`
+ - `tasks.templ` contains `class="kanban-column"` (replacing w-72 Tailwind class)
+ - `tasks.templ` contains `class="tasks-section"` inside kanban column
+ - `tasks.templ` contains `class="task-row task-card"` inside TaskCard
+ - `tasks.templ` contains `class="tasks-add-button"` in AddTaskTrigger
+ - `grep -c "EtapeStrip" backend/templates/tasks.templ` returns 0 (OOB calls removed)
+ - `grep "etapes \[\]sqlc.Etape" backend/templates/tasks.templ` shows parameter in both KanbanBoard and KanbanColumn signatures
+ - `just generate` exits 0 (templ compiles cleanly)
+
+ tasks.templ has groupTasksByEtape, EtapeGroupHeader, restyled KanbanColumn/TaskCard/AddTaskTrigger, and no EtapeStrip OOB calls.
+
+
+
+ Task 2: Update KanbanBoard call sites in handlers_tasks.go and verify full test suite
+ backend/internal/web/handlers_tasks.go
+
+ - backend/internal/web/handlers_tasks.go (read lines 580–660 to see the two KanbanBoard call sites and the loadTasksTabData return variables in scope at each call site; confirm etapes is the 2nd return from loadTasksTabData: `tasks, etapes, counts, filter, ok := loadTasksTabData(...)`)
+ - .planning/phases/16-tablo-detail/16-RESEARCH.md (Pattern: KanbanBoard call site count — three call sites must all be updated; tablos.templ was updated in Plan 02; handlers_tasks.go has two)
+
+
+ Edit `backend/internal/web/handlers_tasks.go` at the two `KanbanBoard` call sites:
+
+ Line ~594 (inside TaskReorderHandler or similar): change
+ `templates.KanbanBoard(tablo.ID, csrf.Token(r), tasks, filter)`
+ to
+ `templates.KanbanBoard(tablo.ID, csrf.Token(r), tasks, filter, etapes)`
+
+ Line ~645 (second KanbanBoard call): change the same pattern.
+
+ At each call site, verify that `etapes` is already in scope from the `loadTasksTabData` return values. The function returns `(tasks, etapes, counts, filter, ok)` — use the `etapes` variable directly.
+
+ After editing, run: `just generate && go build ./backend/...` to confirm all three KanbanBoard call sites match the updated signature.
+
+ Then run the full task test suite:
+ `go test ./backend/internal/web/... -run TestTask -count=1`
+
+ And the full handler test suite (no DB required):
+ `go test ./backend/internal/web/... -count=1`
+
+
+ grep -c "KanbanBoard.*etapes" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/internal/web/handlers_tasks.go && cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source && go build ./backend/... && go test ./backend/internal/web/... -run TestTask -count=1
+
+
+ - `grep "KanbanBoard" backend/internal/web/handlers_tasks.go` shows both call sites include `etapes` as 5th argument
+ - `go build ./backend/...` exits 0 (no KanbanBoard argument count mismatch)
+ - `go test ./backend/internal/web/... -run TestTask -count=1` exits 0 (all 19 task tests pass)
+ - `go test ./backend/internal/web/... -count=1` exits 0 (full handler test suite passes with no regressions)
+
+ Both KanbanBoard call sites in handlers_tasks.go pass etapes; all task tests pass; build is clean.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Handler → KanbanBoard template | etapes []sqlc.Etape passed from handler; data comes from authenticated DB query |
+| EtapeGroup rendering | EtapeColor rendered as inline style value (background-color) — same pattern as project-avatar; no script injection via CSS values |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-16-03-01 | Information Disclosure | EtapeGroup server-side grouping | accept | groupTasksByEtape operates on data already fetched via authenticated query; no new data access introduced |
+| T-16-03-02 | Tampering | EtapeColor inline style value | accept | Etape color is stored in DB as user-supplied value but rendered only as CSS background-color; browsers do not execute CSS property values as code |
+| T-16-03-03 | Spoofing | EtapeStrip OOB removal | accept | Removing dead OOB calls has no security impact; HTMX ignores swaps for missing targets |
+| T-16-03-04 | Denial of Service | groupTasksByEtape O(n*m) | accept | n = tasks per column (bounded by UI; typically < 50), m = etapes per tablo (typically < 10); no external input controls loop bounds; not a real DoS surface |
+
+
+
+```bash
+cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source
+go build ./backend/...
+go test ./backend/internal/web/... -count=1
+```
+Build clean; full handler test suite passes (all 19 task tests + all etape tests + all file tests).
+
+
+
+- `groupTasksByEtape` helper groups tasks by etape in etape declaration order with unassigned last
+- `KanbanBoard` and `KanbanColumn` both accept `etapes []sqlc.Etape`
+- Both `handlers_tasks.go` call sites pass `etapes`
+- Kanban column uses `.kanban-column` / `.tasks-section` / `.tasks-section-header` / `.task-list` CSS classes
+- TaskCard uses `.task-row` with `.task-check` + `.task-body` + trash `@ui.IconButton`
+- EtapeStrip OOB calls removed from TaskCardGone and TaskCardOOB
+- `go test ./backend/internal/web/... -count=1` passes (all 19 task tests)
+
+
+
diff --git a/.planning/phases/16-tablo-detail/16-04-PLAN.md b/.planning/phases/16-tablo-detail/16-04-PLAN.md
new file mode 100644
index 0000000..dd04d59
--- /dev/null
+++ b/.planning/phases/16-tablo-detail/16-04-PLAN.md
@@ -0,0 +1,262 @@
+---
+phase: 16-tablo-detail
+plan: 04
+type: execute
+wave: 4
+depends_on:
+ - 16-03
+files_modified:
+ - backend/templates/files.templ
+autonomous: false
+requirements:
+ - DETAIL-04
+
+must_haves:
+ truths:
+ - "Files section uses .overview-section / .overview-section-heading layout with h3 'Files' on left and Upload file button on right"
+ - "File list uses @ui.Table with Filename / Size / Uploaded / Actions columns"
+ - "Each file row renders as
with Download and Delete @ui.IconButton actions"
+ - "FileDeleteConfirmFragment outer element is
(matching element type for outerHTML swap)"
+ - "Empty state uses @ui.EmptyState with Title 'No files yet' and Description 'Upload your first file to get started.'"
+ - "All 9 existing file handler tests pass unchanged"
+ - "Browser walkthrough confirms: files tab shows correct table layout, delete confirm swap works, empty state renders correctly"
+ artifacts:
+ - path: backend/templates/files.templ
+ provides: Restyled FilesTabFragment, fileTableHead, fileTableBody, FileListRow as tr, FileDeleteConfirmFragment as tr
+ contains: "@ui.Table(ui.TableProps{"
+ - path: backend/templates/files_templ.go
+ provides: Generated Go from restyled files.templ
+ exports: []
+ key_links:
+ - from: backend/templates/files.templ FileListRow
+ to: backend/templates/files.templ FileDeleteConfirmFragment
+ via: "hx-target='closest .file-row-zone' hx-swap='outerHTML'"
+ pattern: "class=\"file-row-zone\""
+ - from: backend/templates/files.templ FilesTabFragment
+ to: backend/internal/web/ui/table.templ @ui.Table
+ via: "@ui.Table(ui.TableProps{Head: fileTableHead(), Body: fileTableBody(...)})"
+ pattern: "ui.Table"
+---
+
+
+Restyle the files section (DETAIL-04): replace raw ``/`- ` layout with `@ui.Table`, add `.overview-section-heading` header, introduce `@ui.EmptyState`, and convert `FileListRow` and `FileDeleteConfirmFragment` to use `
` as the outer element so the existing HTMX outerHTML delete swap continues to work. Ends with a browser verification checkpoint.
+
+Purpose: Deliver DETAIL-04 — files section uses the table component with consistent row actions.
+Output: Restyled `files.templ`; all 9 file tests pass; browser checkpoint approves files tab visual result.
+
+
+
+@/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
+
+
+
+@.planning/ROADMAP.md
+@.planning/phases/16-tablo-detail/16-CONTEXT.md
+@.planning/phases/16-tablo-detail/16-RESEARCH.md
+@.planning/phases/16-tablo-detail/16-PATTERNS.md
+@.planning/phases/16-tablo-detail/16-UI-SPEC.md
+@.planning/phases/16-tablo-detail/16-03-SUMMARY.md
+
+
+
+
+From backend/internal/web/ui/table.templ (verified in RESEARCH.md):
+ type TableProps struct {
+ Head templ.Component
+ Body templ.Component
+ }
+ Usage: @ui.Table(ui.TableProps{Head: fileTableHead(), Body: fileTableBody(tabloID, files)})
+
+From backend/internal/web/ui/empty_state.templ (verified):
+ @ui.EmptyState(ui.EmptyStateProps{Title: "No files yet", Description: "Upload your first file to get started."})
+
+From backend/internal/web/ui/variants.go:
+ ui.ButtonVariantDefault, ui.ButtonToneSolid, ui.SizeMD
+ ui.IconButtonVariantNeutral, ui.IconButtonVariantDanger, ui.IconButtonToneGhost
+
+Current files.templ structure (must read file to confirm):
+ FilesTabFragment: renders with FileListRow - items or FileListEmpty()
+ FileListRow:
- — must become
+ FileDeleteConfirmFragment: — must become
+ FileListEmpty: standalone empty state component — no longer called; @ui.EmptyState used instead
+ FileUploadForm: existing form component — triggered from new "Upload file" button via HTMX
+
+HTMX delete flow (Pitfall 2 — CRITICAL):
+ - FileListRow renders
inside @ui.Table's
+ - Trash IconButton: hx-get="/tablos/{tabloID}/files/{fileID}/delete-confirm" hx-target="closest .file-row-zone" hx-swap="outerHTML"
+ - FileDeleteConfirmFragment must render (same element type for outerHTML swap)
+ - Both use colspan="4" in the confirm to span all 4 columns
+
+Files table columns: Filename / Size / Uploaded / Actions (4 columns)
+
+formatBytes helper: check if it already exists in files.templ or files_helpers.go (grep for formatBytes); if not in scope, use a simple inline format (file.SizeBytes/1024 + "KB" or similar)
+
+File upload button triggers existing FileUploadForm via HTMX to a #file-upload-slot:
+ "Upload file" button → hx-get="/tablos/{tabloID}/files/upload-form" hx-target="#file-upload-slot" hx-swap="innerHTML"
+ div id="file-upload-slot" (empty initially, receives the form fragment)
+
+Download icon button: wrap in tag (download is a navigation, not a form POST):
+
+ @ui.IconButton(ui.IconButtonProps{Label: "Download file", Icon: "download", Variant: ui.IconButtonVariantNeutral, Tone: ui.IconButtonToneGhost, Type: "button"})
+
+
+
+
+
+
+
+ Task 1: Restyle FilesTabFragment, FileListRow, and FileDeleteConfirmFragment
+ backend/templates/files.templ
+
+ - backend/templates/files.templ (read the FULL file before editing — check FilesTabFragment, FileListRow, FileDeleteConfirmFragment, FileListEmpty structures; find formatBytes helper location; confirm all HTMX attributes on existing delete confirm flow)
+ - backend/internal/web/ui/table.templ (read to confirm TableProps signature and how Head/Body components are rendered — specifically whether @ui.Table renders a full or just the body; affects how fileTableHead and fileTableBody must be structured)
+ - backend/internal/web/ui/empty_state.templ (read to confirm EmptyStateProps fields)
+ - .planning/phases/16-tablo-detail/16-PATTERNS.md (section "backend/templates/files.templ" — FilesTabFragment new structure, fileTableHead, fileTableBody, FileListRow as tr, FileDeleteConfirmFragment as tr)
+
+
+ Edit `backend/templates/files.templ` to make the following changes. Read the file before starting.
+
+ 1. RESTYLE FilesTabFragment: replace the current body with:
+ - Outer: `div class="overview-section"`
+ - Header: `div class="overview-section-heading"` containing h3 "Files" on the left and `@ui.Button(ui.ButtonProps{Label: "Upload file", Variant: ui.ButtonVariantDefault, Tone: ui.ButtonToneSolid, Size: ui.SizeMD, Type: "button", Attrs: templ.Attributes{"hx-get": "/tablos/{tablo.ID.String()}/files/upload-form", "hx-target": "#file-upload-slot", "hx-swap": "innerHTML"}})` on the right
+ - `div id="file-upload-slot"` (empty; receives FileUploadForm on button click)
+ - Conditional: if len(files) == 0 → `@ui.EmptyState(ui.EmptyStateProps{Title: "No files yet", Description: "Upload your first file to get started."})` else → `@ui.Table(ui.TableProps{Head: fileTableHead(), Body: fileTableBody(tablo.ID, files)})`
+
+ 2. ADD fileTableHead private templ function (lowercase — not exported):
+ `templ fileTableHead()` rendering `| Filename | Size | Uploaded | Actions | `
+
+ 3. ADD fileTableBody private templ function:
+ `templ fileTableBody(tabloID uuid.UUID, files []sqlc.TabloFile)` rendering `for _, f := range files { @FileListRow(tabloID, f) }`
+
+ 4. RESTYLE FileListRow: change the outer element from `- ` to `
`. Inside the ` `:
+ - `| { file.Filename } | `
+ - `{ formatBytes(file.SizeBytes) } | ` (use existing formatBytes helper; if not found, format as strconv.FormatInt(file.SizeBytes/1024, 10) + " KB" inline)
+ - `if file.CreatedAt.Valid { file.CreatedAt.Time.Format("2006-01-02") } | `
+ - `` containing: `@ui.IconButton(ui.IconButtonProps{Label: "Download file", Icon: "download", Variant: ui.IconButtonVariantNeutral, Tone: ui.IconButtonToneGhost, Type: "button"})` followed by `@ui.IconButton(ui.IconButtonProps{Label: "Delete file", Icon: "trash", Variant: ui.IconButtonVariantDanger, Tone: ui.IconButtonToneGhost, Type: "button", Attrs: templ.Attributes{"hx-get": "/tablos/{tabloID}/files/{fileID}/delete-confirm", "hx-target": "closest .file-row-zone", "hx-swap": "outerHTML"}})`
+
+ 5. RESTYLE FileDeleteConfirmFragment: change outer element from `` to ` `. Wrap existing confirm dialog content in a single ``. Preserve all HTMX confirm/cancel button attributes exactly — do not change the confirm POST URL or cancel reload URL.
+
+ 6. PRESERVE FileListEmpty function body unchanged (it is no longer called from FilesTabFragment but may be referenced elsewhere; keep it in the file).
+
+ 7. ADD `"github.com/google/uuid"` to the import block if not already present (needed for fileTableBody parameter type).
+
+ After editing, run: `just generate` to regenerate `files_templ.go`.
+
+
+ grep -c "@ui.Table" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/files.templ && grep -c "@ui.EmptyState" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/files.templ && grep -c "class=\"file-row-zone\"" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/files.templ && grep -c "overview-section-heading" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/files.templ && cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source && go build ./backend/... && go test ./backend/internal/web/... -run TestFilesTab -count=1
+
+
+ - `files.templ` contains `@ui.Table(ui.TableProps{`
+ - `files.templ` contains `@ui.EmptyState(ui.EmptyStateProps{Title: "No files yet"`
+ - `files.templ` contains `class="overview-section-heading"`
+ - `files.templ` contains `class="file-row-zone"` on a `` element (not `- ` or `
`)
+ - `files.templ` FileDeleteConfirmFragment outer element is ` `
+ - `files.templ` contains `Icon: "download"` in FileListRow actions column
+ - `go build ./backend/...` exits 0
+ - `go test ./backend/internal/web/... -run TestFilesTab -count=1` exits 0 (all file tests pass)
+ - `go test ./backend/internal/web/... -count=1` exits 0 (no regressions)
+
+ FilesTabFragment uses @ui.Table and @ui.EmptyState; FileListRow and FileDeleteConfirmFragment use tr; all file tests pass.
+
+
+
+
+ Plans 01–04 together deliver the complete Phase 16 restyling:
+ - Plan 01: download + chat icons in UIIcon; CSS Sections 19–25 in app.css
+ - Plan 02: Tablo detail header (project-card-top), metadata row, tab nav (design token classes), desc in overview tab, EtapeStrip removed from TasksTabFragment
+ - Plan 03: Kanban board with tasks-section layout, etape-grouped task rows, KanbanBoard/Column restyled, handlers updated
+ - Plan 04 (just completed): Files section with @ui.Table, @ui.EmptyState, download + delete IconButtons
+
+
+ Start the dev server: `cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && just dev` (or `air`)
+
+ Navigate to a tablo detail page (e.g. http://localhost:3000/tablos/{some-id}).
+
+ Check these items:
+
+ **Header (DETAIL-01):**
+ 1. Header shows a colored circle avatar with the first letter of the tablo title
+ 2. Title is inline-editable (click to edit, save with Enter or blur)
+ 3. Action controls appear on the right: Discussion link with chat icon, Invite Member button, trash icon button for Delete
+ 4. No hardcoded purple (#804EEC) hex appears as a visible style artifact — brand purple shows only via CSS tokens
+
+ **Metadata row:**
+ 5. Created date appears with a calendar icon and formatted date
+ 6. A purple badge labeled "In progress" appears
+ 7. A progress bar track (gray) appears with a thin colored fill
+
+ **Tab nav:**
+ 8. All 5 tabs (Overview, Tasks, Files, Discussion, Events) appear in a clean horizontal row
+ 9. The active tab has a purple underline and bold text; inactive tabs are muted gray
+ 10. Clicking a tab loads its content via HTMX
+
+ **Overview tab (DETAIL-01):**
+ 11. Description zone appears inside the Overview tab (not above the tab nav)
+ 12. Click description → edit form appears; save returns to display
+
+ **Tasks tab (DETAIL-02 + DETAIL-03):**
+ 13. Three kanban columns (Todo / In Progress / Done) appear side by side
+ 14. Each column has a section header with status label, task count badge, and "Add task" button
+ 15. Tasks within each column appear as row-style cards with a round checkbox + task title + trash icon
+ 16. If etapes are defined: tasks are grouped by etape with a colored dot sub-heading; unassigned tasks appear at the bottom under "No etape"
+ 17. Drag-and-drop reorder still works (drag a task between columns)
+ 18. No EtapeStrip filter pills appear anywhere in the tasks tab
+
+ **Files tab (DETAIL-04):**
+ 19. Files section header shows "Files" on the left and "Upload file" button on the right
+ 20. Clicking "Upload file" reveals the upload form inline
+ 21. If files exist: they appear in a clean table with Filename / Size / Uploaded / Actions columns
+ 22. Each file row has a download icon and a trash icon
+ 23. Clicking trash → delete confirm replaces the row; confirm/cancel work
+ 24. If no files: the empty state with "No files yet" appears
+
+ If any item above is broken or looks wrong, describe the specific issue.
+
+ Type "approved" if all 24 items pass, or describe specific issues to fix.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Handler → FilesTabFragment | files []sqlc.TabloFile from authenticated DB query; tabloID from URL path validated by loadOwnedTablo |
+| HTMX outerHTML swap | delete confirm fragment swaps the .file-row-zone tr; hx-target="closest .file-row-zone" is client-driven but the server validates ownership before returning the confirm fragment |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-16-04-01 | Tampering | FileListRow download href | accept | Download URL /tablos/{id}/files/{fileID}/download is an authenticated route; file ownership validated server-side; no user-controlled URL injection (template uses templ.SafeURL) |
+| T-16-04-02 | Information Disclosure | file.Filename in table | accept | Filename is user-supplied data returned to the same authenticated user who uploaded it; no cross-user exposure possible given ownership model |
+| T-16-04-03 | Tampering | FileDeleteConfirmFragment tr element | accept | HTMX outerHTML swap of tr is client-side presentation only; server validates delete ownership at POST handler; confirm fragment is GET-only (no side effect) |
+
+
+
+```bash
+cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source
+go test ./backend/internal/web/... -count=1
+```
+All existing file tests pass. Browser checkpoint approved.
+
+Final phase gate (run with DB):
+```bash
+TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1
+```
+
+
+
+- `files.templ` FilesTabFragment uses `.overview-section` + `.overview-section-heading` + `@ui.Table` + `@ui.EmptyState`
+- `FileListRow` renders `` with Download + Delete `@ui.IconButton`
+- `FileDeleteConfirmFragment` renders ` ` with `| `
+- `go test ./backend/internal/web/... -count=1` passes (all 9 file tests unchanged)
+- Browser checkpoint approved: all 24 visual/interaction items verified
+- `TEST_DATABASE_URL=... go test ./... -count=1` passes (full suite green)
+
+
+
| | | |