diff --git a/.planning/phases/05-files/05-02-SUMMARY.md b/.planning/phases/05-files/05-02-SUMMARY.md new file mode 100644 index 0000000..c5b847f --- /dev/null +++ b/.planning/phases/05-files/05-02-SUMMARY.md @@ -0,0 +1,140 @@ +--- +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...)