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