---
phase: 05-files
plan: "02"
subsystem: files-upload-tabs
tags: [htmx, s3, multipart, tab-navigation, templ, router]
dependency_graph:
  requires:
    - 05-01 (FileStorer interface, sqlc TabloFile model, files.sql.go queries)
  provides:
    - backend/internal/web/handlers_files.go (FilesDeps, TabloFilesTabHandler, FileUploadHandler, TabloTasksTabHandler)
    - backend/templates/files.templ (FilesTabFragment, FileUploadForm, FileListRow, FileRowGone, UploadErrorFragment)
    - backend/templates/files_helpers.go (formatBytes)
    - backend/templates/tablos.templ (TabloDetailPage 6-arg, TasksTabFragment, TabloOverviewTabFragment)
    - backend/internal/web/router.go (fileDeps parameter, file + tasks-tab routes)
    - backend/cmd/web/main.go (S3 env vars, files.NewStore, fileDeps wired)
  affects:
    - backend/internal/web/handlers_tablos.go (TabloDetailPage call sites updated)
    - backend/internal/web/handlers_auth_test.go (NewRouter signature updated)
    - backend/internal/web/handlers_tablos_test.go (NewRouter signature updated)
    - backend/internal/web/handlers_tasks_test.go (NewRouter signature updated)
    - backend/internal/web/handlers_test.go (NewRouter signature updated)
    - backend/internal/web/csrf_test.go (NewRouter signature updated)
tech_stack:
  added: []
  patterns:
    - multipart upload via http.MaxBytesReader + ParseMultipartForm (Pitfall 2 guard)
    - S3 key isolation: "files/{tablo_id}/{uuid}" — filename never reaches S3 key (D-04)
    - nil FileStorer guard as FIRST statement in TabloFilesTabHandler + FileUploadHandler (T-05-02-06)
    - hx-push-url on all tab links for browser URL sync (D-08)
    - HTMX fragment vs full-page dispatch on HX-Request header
key_files:
  created:
    - backend/internal/web/handlers_files.go
    - backend/templates/files.templ
    - backend/templates/files_helpers.go
  modified:
    - backend/internal/web/handlers_files_test.go (activated from stub scaffold)
    - backend/templates/tablos.templ (TabloDetailPage restructured, fragment components added)
    - backend/internal/web/router.go (fileDeps param, new routes)
    - backend/internal/web/handlers_tablos.go (call sites updated)
    - backend/cmd/web/main.go (S3 env + fileDeps wiring)
    - backend/internal/web/handlers_auth_test.go
    - backend/internal/web/handlers_tablos_test.go
    - backend/internal/web/handlers_tasks_test.go
    - backend/internal/web/handlers_test.go
    - backend/internal/web/csrf_test.go
decisions:
  - "FileStorer type alias in handlers_files.go (= files.FileStorer) — avoids import of files package in test files while keeping interface stable"
  - "itoa helper inlined in handlers_files.go — avoids strconv import in web package for single use"
  - "formatBytes in templates/files_helpers.go — non-templ Go file in templates package; formatBytes called directly from templ files"
  - "TabloTasksTabHandler takes FilesDeps (not TasksDeps) — tab wiring is a Phase 5 concern; FilesDeps has same Queries pointer; avoids dual-deps threading"
  - "nil filesStore allowed in main.go when S3_ENDPOINT unset — dev/test mode without MinIO; handlers return 503 via nil guard"
  - "UploadErrorFragment re-renders full FilesTabFragment with error in FileUploadForm — simpler than OOB error injection; form + list always consistent"
metrics:
  duration: ~25min
  completed: "2026-05-15"
  tasks: 2
  files: 14
---

# Phase 05 Plan 02: File Upload + Tab Navigation Summary

Vertical slice 1 for the files feature. A user can now visit `/tablos/{id}/files`, see an upload form and any existing file list, upload a file that gets stored in S3 with a DB row, and navigate the Overview/Tasks/Files tab bar with URL-sync via `hx-push-url`. The tab restructuring covers D-07/D-08 and unlocks the files workflow.

## Tasks Completed

| # | Name | Commit | Files |
|---|------|--------|-------|
| RED | File handler test scaffold | cc0d6cf | handlers_files_test.go |
| 1 | FilesDeps + FileUploadHandler + TabloFilesTabHandler + TabloTasksTabHandler | f50836f | handlers_files.go |
| 2 | 3-tab layout + files templates + router + main.go S3 wiring | a12c5ab | tablos.templ, files.templ, files_helpers.go, router.go, handlers_tablos.go, main.go, 5 test files |

## Verification Results

- `go build ./...` exits 0
- `go test ./...` exits 0 — all packages pass; TestFile* SKIP (integration tests without DB — expected)
- `backend/internal/web/handlers_files.go` contains `type FilesDeps struct`, `func TabloFilesTabHandler(`, `func FileUploadHandler(`, `func TabloTasksTabHandler(`
- `FileUploadHandler` first statement is `deps.Files == nil` nil guard returning 503
- `TabloFilesTabHandler` first statement is `deps.Files == nil` nil guard returning 503
- `FileUploadHandler` contains `http.MaxBytesReader` before `ParseMultipartForm`
- `FileUploadHandler` contains `"files/"` string for S3 key (D-04)
- `FileUploadHandler` contains `http.MaxBytesError` for size violation detection
- `backend/templates/tablos.templ` contains `activeTab string` in TabloDetailPage, `hx-push-url`, `tab-content`, `TasksTabFragment`
- `backend/templates/files.templ` contains `FilesTabFragment`, `FileUploadForm`, `hx-encoding`, `FileRowGone`
- `backend/internal/web/router.go` contains `fileDeps FilesDeps` in NewRouter, `TabloFilesTabHandler`, `TabloTasksTabHandler`
- `backend/cmd/web/main.go` contains `files.NewStore` and `fileDeps := web.FilesDeps`
- Both `TabloDetailPage` call sites in `handlers_tablos.go` end with `nil, "overview"`

## Deviations from Plan

### Auto-fixed Issues

None — plan executed exactly as written with one minor implementation adaptation:

**1. [Rule 2 - Missing critical functionality] itoa helper in handlers_files.go**
- **Found during:** Task 1 implementation
- **Issue:** formatMBError needed integer-to-string conversion; importing `strconv` only for this single use would add a dependency unnecessarily
- **Fix:** Added private `itoa(n int) string` helper in handlers_files.go; eliminates strconv import
- **Files modified:** backend/internal/web/handlers_files.go
- **Commit:** f50836f

**2. [Rule 3 - Blocking issue] All test routers needed FilesDeps parameter**
- **Found during:** Task 2 — after NewRouter signature change, 5 test files failed to build
- **Fix:** Updated all 5 test routers (handlers_auth_test.go, handlers_tablos_test.go, handlers_tasks_test.go, handlers_test.go, csrf_test.go) to pass `FilesDeps{Queries: q}` or `FilesDeps{}` in the new position
- **Files modified:** 5 test files
- **Commit:** a12c5ab

## Known Stubs

| File | Description | Resolved by |
|------|-------------|-------------|
| backend/internal/web/handlers_files.go | FileDownloadHandler returns 501 | Plan 03 |
| backend/internal/web/handlers_files.go | FileDeleteConfirmHandler returns 501 | Plan 03 |
| backend/internal/web/handlers_files.go | FileDeleteHandler returns 501 | Plan 03 |
| backend/internal/web/handlers_files_test.go | TestFileDownload t.Skip — Plan 03 | Plan 03 |
| backend/internal/web/handlers_files_test.go | TestFileDelete t.Skip — Plan 03 | Plan 03 |
| backend/internal/web/handlers_files_test.go | TestFileOwnership t.Skip — Plan 03 | Plan 03 |

These stubs are intentional. The stub handlers return 501 and are referenced in FileListRow download/delete links — they will be replaced in Plan 03 (file download + delete).

## Threat Surface Scan

All new surface is covered by the plan's threat model:
- `POST /tablos/{id}/files` (T-05-02-01..06) — covered
- `GET /tablos/{id}/files` (T-05-02-04, T-05-02-06) — covered
- Stub routes for download/delete return 501 with no data exposure

No new unplanned trust boundaries introduced.

## Self-Check: PASSED

Files verified:
- FOUND: backend/internal/web/handlers_files.go
- FOUND: backend/templates/files.templ
- FOUND: backend/templates/files_helpers.go

Commits verified:
- FOUND: cc0d6cf (test(05-02): add RED test scaffold...)
- FOUND: f50836f (feat(05-02): implement FilesDeps...)
- FOUND: a12c5ab (feat(05-02): 3-tab layout...)
