---
phase: 05-files
plan: "03"
subsystem: files-download-delete
tags: [htmx, s3, presign, delete, ownership, templ, tdd]
dependency_graph:
  requires:
    - 05-01 (FileStorer interface with PresignDownload + Delete)
    - 05-02 (FilesDeps, loadOwnedTabloForFile, FileRowGone, FileListRow download/delete links)
  provides:
    - backend/internal/web/handlers_files.go (FileDownloadHandler, FileDeleteConfirmHandler, FileDeleteHandler — replaces 501 stubs)
    - backend/templates/files.templ (FileDeleteConfirmFragment)
  affects: []
tech_stack:
  added: []
  patterns:
    - S3-first delete with always-delete-DB (log failure, never abort — Phase 6 reconciles orphans)
    - nil FileStorer guard as FIRST statement in all three handlers (T-05-03-06)
    - 302 redirect to presigned URL (browser handles redirect directly, no JS needed)
    - Inline confirm fragment via HTMX outerHTML swap on .file-row-zone (mirrors TaskDeleteConfirmFragment)
    - CSRF enforced via @ui.CSRFField in FileDeleteConfirmFragment (T-05-03-05)
key_files:
  created: []
  modified:
    - backend/internal/web/handlers_files.go (three 501 stubs replaced with full implementations)
    - backend/internal/web/handlers_files_test.go (TestFileDownload, TestFileDownload_NonOwner, TestFileDelete, TestFileDelete_S3Failure, TestFileOwnership implemented)
    - backend/templates/files.templ (FileDeleteConfirmFragment added)
decisions:
  - "302 redirect to presigned URL — browser handles redirect natively, no JS or streaming proxy needed (CONTEXT.md discretion item)"
  - "S3 delete failure is logged but does not abort DB delete — orphan S3 objects reconciled by Phase 6 worker (CONTEXT.md deferred items, T-05-03-04)"
  - "files_templ.go is gitignored — templ generate regenerates it from files.templ at build time; not committed"
  - "FileDeleteConfirmFragment cancel button targets #tab-content hx-get /files (reloads tab) — simpler than a show-file route which does not exist yet"
  - "stubbedFileStorer.deleteErr field added — allows TestFileDelete_S3Failure to inject error without a new type"
metrics:
  duration: ~20min
  completed: "2026-05-15"
  tasks: 2
  files: 3
---

# Phase 05 Plan 03: File Download + Delete Summary

Vertical slice 2 for the files feature. A user can now click Download to receive a 302 redirect to a 5-minute presigned S3 URL, click Delete to see an inline confirmation (same HTMX pattern as task delete), and confirm deletion which removes the S3 object (logging failure) then always removes the DB row. All six FILE requirements are now implemented. The complete Tablos workflow — create tablos, manage tasks (kanban), and attach/download/delete files — is operational end-to-end.

## Tasks Completed

| # | Name | Commit | Files |
|---|------|--------|-------|
| RED | File download + delete test scaffold | 98a5a02 | handlers_files_test.go |
| 1 (GREEN) | FileDownloadHandler + FileDeleteConfirmHandler + FileDeleteHandler + FileDeleteConfirmFragment | 9d4dd4f | handlers_files.go, files.templ |

## Verification Results

- `go build ./...` exits 0
- `go test ./...` exits 0 — all packages pass
- `handlers_files.go` contains no "501" (all stubs replaced)
- `FileDownloadHandler`, `FileDeleteConfirmHandler`, `FileDeleteHandler` each have `deps.Files == nil` as first guard returning 503
- `FileDownloadHandler` contains `http.StatusFound` (302 redirect)
- `FileDeleteHandler` contains `deps.Files.Delete` before `deps.Queries.DeleteTabloFile` (S3 first)
- S3 failure log does NOT have an early return — DB delete always runs
- `templates/files.templ` contains `FileDeleteConfirmFragment` with `file-row-zone`, `ButtonVariantDanger`, `templ.SafeURL`, `@ui.CSRFField`
- `FileListRow` download link uses `/download` href; delete button uses `/delete-confirm` in hx-get

## Deviations from Plan

None — plan executed exactly as written.

## Known Stubs

None — all stubs from Plans 01 and 02 are resolved. The full FILE-01..06 requirement set is implemented.

## Threat Surface Scan

All new surface is covered by the plan's threat model:

- `GET /tablos/{id}/files/{file_id}/download` — T-05-03-01 (presigned TTL), T-05-03-02 (IDOR ownership)
- `GET /tablos/{id}/files/{file_id}/delete-confirm` — T-05-03-02 (IDOR ownership), T-05-03-05 (CSRF on confirm form)
- `POST /tablos/{id}/files/{file_id}/delete` — T-05-03-03 (IDOR ownership), T-05-03-04 (S3 partial failure), T-05-03-05 (CSRF), T-05-03-06 (nil FileStorer)

No new unplanned trust boundaries introduced.

## TDD Gate Compliance

- RED gate: commit 98a5a02 `test(05-03): add RED test scaffold for file download + delete handlers`
- GREEN gate: commit 9d4dd4f `feat(05-03): implement FileDownloadHandler, FileDeleteConfirmHandler, FileDeleteHandler`
- REFACTOR gate: not needed — implementation was clean on first pass

## Self-Check: PASSED

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

Commits verified:
- FOUND: 98a5a02 (test(05-03): add RED test scaffold...)
- FOUND: 9d4dd4f (feat(05-03): implement FileDownloadHandler...)
