docs(16): create phase 16 tablo detail plan — 4 plans, 4 waves

Phase 16 delivers DETAIL-01/02/03/04: header restyling, kanban
tasks-section layout with server-side etape grouping, and files
table component. Ends with a browser verify checkpoint.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-05-16 23:20:49 +02:00
parent 36011fc8a3
commit 965ec5e5ce
No known key found for this signature in database
5 changed files with 988 additions and 1 deletions

View file

@ -111,6 +111,7 @@ Plans:
**Mode:** mvp **Mode:** mvp
**Status:** Pending **Status:** Pending
**Requirements:** DETAIL-01, DETAIL-02, DETAIL-03, DETAIL-04 **Requirements:** DETAIL-01, DETAIL-02, DETAIL-03, DETAIL-04
**Plans:** 4 plans
**Success Criteria:** **Success Criteria:**
1. Tablo detail header uses project-card-top layout with title, avatar, and action controls 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 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 4. Files section uses the table component with consistent row actions
5. All existing task, etape, and file handler tests pass unchanged 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 1925 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 ### 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. **Goal:** Restyle the discussion view and planning page using design system components to make them visually consistent with the rest of the app.

View file

@ -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 (1925)
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 1315.
<objective>
Add the two missing icon SVG cases (`download`, `chat`) to `UIIcon` in `icon_button.templ`, and append all Phase 16 CSS sections (1925) to `app.css`. This is the shared substrate that plans 0204 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 1925 appended.
</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>
@.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
<interfaces>
<!-- Key patterns the executor needs. No exploration required. -->
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 `<span>{kind}</span>` (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))`
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add `download` and `chat` icon cases to UIIcon switch</name>
<files>backend/internal/web/ui/icon_button.templ</files>
<read_first>
- backend/internal/web/ui/icon_button.templ (read entire file — ~80 lines — before editing; understand the full switch structure and the `default:` position)
</read_first>
<action>
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`.
</action>
<verify>
<automated>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/...</automated>
</verify>
<acceptance_criteria>
- `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
</acceptance_criteria>
<done>UIIcon switch contains "download" and "chat" cases; project compiles cleanly.</done>
</task>
<task type="auto">
<name>Task 2: Append CSS Sections 1925 to app.css</name>
<files>backend/internal/web/ui/app.css</files>
<read_first>
- backend/internal/web/ui/app.css (read lines 340450 to confirm the end of the file and the existing shared `.tasks-section-header h3` rule at lines 355361; 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)
</read_first>
<action>
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.
</action>
<verify>
<automated>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</automated>
</verify>
<acceptance_criteria>
- `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)
</acceptance_criteria>
<done>app.css has all Phase 16 CSS sections appended; no hardcoded hex values; project compiles cleanly.</done>
</task>
</tasks>
<threat_model>
## 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.
</threat_model>
<verification>
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).
</verification>
<success_criteria>
- `icon_button.templ` has `case "download":` and `case "chat":` before `default:`
- `app.css` has 7 new sections (1925) appended with no hardcoded hex values
- `go test ./backend/internal/web/... -count=1` passes with no new failures
- `go build ./backend/...` exits 0
</success_criteria>
<output>
After completion, create `.planning/phases/16-tablo-detail/16-01-SUMMARY.md` using the summary template.
</output>

View file

@ -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"
---
<objective>
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.
</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>
@.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
<interfaces>
<!-- Critical contracts and current patterns the executor must know. -->
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 ~103112) — 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 ~306325) 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 <a> 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
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Restyle TabloDetailPage header, metadata row, and tab nav</name>
<files>backend/templates/tablos.templ</files>
<read_first>
- 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)
</read_first>
<action>
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`.
</action>
<verify>
<automated>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</automated>
</verify>
<acceptance_criteria>
- `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)
</acceptance_criteria>
<done>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.</done>
</task>
<task type="auto">
<name>Task 2: Remove EtapeStrip call from TasksTabFragment and update KanbanBoard call site</name>
<files>backend/templates/tablos.templ</files>
<read_first>
- backend/templates/tablos.templ (already read in Task 1 — find TasksTabFragment, line ~407420; 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)
</read_first>
<action>
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`.
</action>
<verify>
<automated>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</automated>
</verify>
<acceptance_criteria>
- `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)
</acceptance_criteria>
<done>EtapeStrip is not called from TasksTabFragment; KanbanBoard call is updated to pass etapes.</done>
</task>
</tasks>
<threat_model>
## 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 |
</threat_model>
<verification>
```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.
</verification>
<success_criteria>
- `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
</success_criteria>
<output>
After completion, create `.planning/phases/16-tablo-detail/16-02-SUMMARY.md` using the summary template.
</output>

View file

@ -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"
---
<objective>
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.
</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>
@.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
<interfaces>
<!-- Key contracts the executor needs. -->
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; ... }
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add groupTasksByEtape helper, EtapeGroup type, EtapeGroupHeader component; update KanbanBoard/Column signatures and restyled column/card/trigger</name>
<files>backend/templates/tasks.templ</files>
<read_first>
- 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)
</read_first>
<action>
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`.
</action>
<verify>
<automated>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</automated>
</verify>
<acceptance_criteria>
- `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)
</acceptance_criteria>
<done>tasks.templ has groupTasksByEtape, EtapeGroupHeader, restyled KanbanColumn/TaskCard/AddTaskTrigger, and no EtapeStrip OOB calls.</done>
</task>
<task type="auto">
<name>Task 2: Update KanbanBoard call sites in handlers_tasks.go and verify full test suite</name>
<files>backend/internal/web/handlers_tasks.go</files>
<read_first>
- backend/internal/web/handlers_tasks.go (read lines 580660 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)
</read_first>
<action>
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`
</action>
<verify>
<automated>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</automated>
</verify>
<acceptance_criteria>
- `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)
</acceptance_criteria>
<done>Both KanbanBoard call sites in handlers_tasks.go pass etapes; all task tests pass; build is clean.</done>
</task>
</tasks>
<threat_model>
## 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 |
</threat_model>
<verification>
```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).
</verification>
<success_criteria>
- `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)
</success_criteria>
<output>
After completion, create `.planning/phases/16-tablo-detail/16-03-SUMMARY.md` using the summary template.
</output>

View file

@ -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 <tr class='file-row-zone'> with Download and Delete @ui.IconButton actions"
- "FileDeleteConfirmFragment outer element is <tr class='file-row-zone'> (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"
---
<objective>
Restyle the files section (DETAIL-04): replace raw `<ul>`/`<li>` layout with `@ui.Table`, add `.overview-section-heading` header, introduce `@ui.EmptyState`, and convert `FileListRow` and `FileDeleteConfirmFragment` to use `<tr>` 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.
</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>
@.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
<interfaces>
<!-- Key contracts the executor needs. -->
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 <ul> with FileListRow <li> items or FileListEmpty()
FileListRow: <li class="file-row-zone" id="file-{id}"> — must become <tr>
FileDeleteConfirmFragment: <div class="file-row-zone" id="file-{id}"> — must become <tr>
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 <tr class="file-row-zone" id="file-{id}"> inside @ui.Table's <tbody>
- Trash IconButton: hx-get="/tablos/{tabloID}/files/{fileID}/delete-confirm" hx-target="closest .file-row-zone" hx-swap="outerHTML"
- FileDeleteConfirmFragment must render <tr class="file-row-zone" id="file-{id}"> (same element type for outerHTML swap)
- Both use colspan="4" in the confirm <td> 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 <a> tag (download is a navigation, not a form POST):
<a href="/tablos/{tabloID}/files/{fileID}/download" aria-label="Download {file.Filename}">
@ui.IconButton(ui.IconButtonProps{Label: "Download file", Icon: "download", Variant: ui.IconButtonVariantNeutral, Tone: ui.IconButtonToneGhost, Type: "button"})
</a>
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Restyle FilesTabFragment, FileListRow, and FileDeleteConfirmFragment</name>
<files>backend/templates/files.templ</files>
<read_first>
- 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 <table><thead><tbody> 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)
</read_first>
<action>
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 `<tr><th>Filename</th><th>Size</th><th>Uploaded</th><th>Actions</th></tr>`
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 `<li class="file-row-zone" id="file-{id}">` to `<tr class="file-row-zone" id="file-{id}">`. Inside the `<tr>`:
- `<td>{ file.Filename }</td>`
- `<td>{ formatBytes(file.SizeBytes) }</td>` (use existing formatBytes helper; if not found, format as strconv.FormatInt(file.SizeBytes/1024, 10) + " KB" inline)
- `<td>if file.CreatedAt.Valid { file.CreatedAt.Time.Format("2006-01-02") }</td>`
- `<td>` containing: `<a href="/tablos/{tabloID}/files/{fileID}/download" aria-label="Download {file.Filename}">@ui.IconButton(ui.IconButtonProps{Label: "Download file", Icon: "download", Variant: ui.IconButtonVariantNeutral, Tone: ui.IconButtonToneGhost, Type: "button"})</a>` 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 `<div class="file-row-zone" id="file-{id}">` to `<tr class="file-row-zone" id="file-{id}">`. Wrap existing confirm dialog content in a single `<td colspan="4">`. 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`.
</action>
<verify>
<automated>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</automated>
</verify>
<acceptance_criteria>
- `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 `<tr>` element (not `<li>` or `<div>`)
- `files.templ` FileDeleteConfirmFragment outer element is `<tr class="file-row-zone"` with `<td colspan="4">`
- `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)
</acceptance_criteria>
<done>FilesTabFragment uses @ui.Table and @ui.EmptyState; FileListRow and FileDeleteConfirmFragment use tr; all file tests pass.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
Plans 0104 together deliver the complete Phase 16 restyling:
- Plan 01: download + chat icons in UIIcon; CSS Sections 1925 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
</what-built>
<how-to-verify>
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.
</how-to-verify>
<resume-signal>Type "approved" if all 24 items pass, or describe specific issues to fix.</resume-signal>
</task>
</tasks>
<threat_model>
## 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) |
</threat_model>
<verification>
```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
```
</verification>
<success_criteria>
- `files.templ` FilesTabFragment uses `.overview-section` + `.overview-section-heading` + `@ui.Table` + `@ui.EmptyState`
- `FileListRow` renders `<tr class="file-row-zone">` with Download + Delete `@ui.IconButton`
- `FileDeleteConfirmFragment` renders `<tr class="file-row-zone">` with `<td colspan="4">`
- `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)
</success_criteria>
<output>
After completion, create `.planning/phases/16-tablo-detail/16-04-SUMMARY.md` using the summary template.
</output>