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