- Tasks: 2 + RED scaffold committed - Commits:cc0d6cf(RED),f50836f(GREEN handlers),a12c5ab(templates + router + wiring) - All acceptance criteria met; go build + go test ./... pass - Known stubs: FileDownloadHandler/DeleteHandler/DeleteConfirmHandler (Plan 03)
7.8 KiB
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 0go test ./...exits 0 — all packages pass; TestFile* SKIP (integration tests without DB — expected)backend/internal/web/handlers_files.gocontainstype FilesDeps struct,func TabloFilesTabHandler(,func FileUploadHandler(,func TabloTasksTabHandler(FileUploadHandlerfirst statement isdeps.Files == nilnil guard returning 503TabloFilesTabHandlerfirst statement isdeps.Files == nilnil guard returning 503FileUploadHandlercontainshttp.MaxBytesReaderbeforeParseMultipartFormFileUploadHandlercontains"files/"string for S3 key (D-04)FileUploadHandlercontainshttp.MaxBytesErrorfor size violation detectionbackend/templates/tablos.templcontainsactiveTab stringin TabloDetailPage,hx-push-url,tab-content,TasksTabFragmentbackend/templates/files.templcontainsFilesTabFragment,FileUploadForm,hx-encoding,FileRowGonebackend/internal/web/router.gocontainsfileDeps FilesDepsin NewRouter,TabloFilesTabHandler,TabloTasksTabHandlerbackend/cmd/web/main.gocontainsfiles.NewStoreandfileDeps := web.FilesDeps- Both
TabloDetailPagecall sites inhandlers_tablos.goend withnil, "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
strconvonly for this single use would add a dependency unnecessarily - Fix: Added private
itoa(n int) stringhelper 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}orFilesDeps{}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) — coveredGET /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: