docs(05): capture phase context

This commit is contained in:
Arthur Belleville 2026-05-15 10:58:19 +02:00
parent 9e6ab6d5aa
commit 1c02de475e
No known key found for this signature in database
2 changed files with 266 additions and 0 deletions

View file

@ -0,0 +1,139 @@
# Phase 5: Files - Context
**Gathered:** 2026-05-15
**Status:** Ready for planning
<domain>
## Phase Boundary
A tablo owner can attach files to a tablo — upload via server-proxied multipart POST, list with filename/size/date, download via signed time-limited URLs, and delete. File bytes stored in S3-compatible object storage (Cloudflare R2 in production, MinIO for local dev). Metadata stored in Postgres. All routes scoped to tablo owner.
Delivers FILE-01..06. **Not in scope:** file previews/thumbnails, file versioning, file attachments on tasks (tasks only have title/description in v1), background job for orphan-file cleanup (Phase 6), per-user storage quotas.
This phase also introduces **tab navigation** on the tablo detail page: Overview / Tasks / Files — previously deferred from Phase 4.
</domain>
<decisions>
## Implementation Decisions
### Upload Method
- **D-01:** Server-proxied upload — client submits a multipart form POST to the Go server; Go streams bytes to S3-compatible storage. No presigned PUT URLs for v1. Simpler auth story, easier server-side enforcement of size limits and content-type detection.
- **D-02:** Storage backend is fully configurable via env vars (`S3_ENDPOINT`, `S3_BUCKET`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`). Same Go code works with Cloudflare R2 (production) or self-hosted MinIO (local dev) — just a config change.
- **D-03:** Add a **MinIO service** to `compose.yaml` for local dev. Include an `mc` (MinIO client) init container that creates the bucket on first start. Developers can run the full file upload flow locally without any external credentials.
### Storage Schema
- **D-04:** S3 object keys use `files/{tablo_id}/{uuid}` format (tablo-scoped with UUID). Tablo ownership is visible in the key; per-tablo listing and bulk-delete are trivial. No key collisions possible.
- **D-05:** Content-type is **server-detected** from the first 512 bytes using `net/http.DetectContentType`. The browser-reported MIME type is ignored — prevents spoofing. Detected content-type stored in `tablo_files.content_type` and set as the S3 object's `ContentType` metadata.
- **D-06:** **Allow duplicates** — no dedup logic for v1. Each upload gets its own UUID, DB row, and S3 object. User can upload the same file twice; both appear in the list with their own delete buttons.
### Page Layout — Tab Navigation
- **D-07:** The tablo detail page introduces **3 tabs: Overview / Tasks / Files**. Overview contains the tablo title and description (inline editable, as in Phase 3). Tasks contains the kanban board (Phase 4). Files contains the file list + upload form (this phase).
- **D-08:** Tab switching uses **HTMX + `hx-push-url`**. Each tab is a server-rendered sub-route:
- `GET /tablos/{id}` → redirects to or renders Overview tab
- `GET /tablos/{id}/tasks` → Tasks tab fragment
- `GET /tablos/{id}/files` → Files tab fragment
- Tab clicks fire `hx-get` + `hx-push-url` so the URL updates and browser back-button navigates between tabs. No JS tab state.
### File Restrictions
- **D-09:** Default max upload size is **25 MB**, configurable via `MAX_UPLOAD_SIZE_MB` env var. Enforced server-side via `http.MaxBytesReader` wrapping the request body before streaming begins, and verified again before writing to S3. Friendly error message surfaced above the upload form on violation (ROADMAP success criterion #6).
- **D-10:** **All file types accepted** — no allowlist for v1. Content-type is server-detected and stored for future use but not used to gate uploads. Users can attach any file type.
### Claude's Discretion
- Exact Tailwind styling for the file list rows (compact vs. card layout, icon per content-type, hover states).
- Tab active/inactive visual treatment (underline vs. filled tab, border style).
- Which tab is the default landing when visiting `/tablos/{id}` for the first time (Overview is the natural default).
- Whether the upload form is always visible at the top of the Files tab or collapsed behind an "Attach file" button.
- HTTP verb for file upload: `POST /tablos/{id}/files` (form-compatible).
- Whether file delete uses inline confirmation (same D-07 pattern from Phase 3) or a modal.
- Signed URL TTL for downloads (ROADMAP suggests "e.g. 5 minutes" — planner's call within that range).
- Whether downloading redirects to the signed URL (302) or opens in a new tab (link with `target="_blank"`).
</decisions>
<canonical_refs>
## Canonical References
**Downstream agents MUST read these before planning or implementing.**
### Requirements
- `.planning/REQUIREMENTS.md` §Files (FILE-01..06) — The 6 file requirements this phase delivers
- `.planning/REQUIREMENTS.md` §Tasks (TASK-01..07) — Task requirements; Phase 5 must not regress kanban behavior when adding tabs
- `.planning/PROJECT.md` — Core value statement and constraints (HTMX-first, no JS framework, single binary, S3-compatible storage)
- `.planning/ROADMAP.md` §Phase 5 — Success criteria and user-in-loop decisions
### Prior Phase Context (locked decisions that constrain this phase)
- `.planning/phases/04-tasks-kanban/04-CONTEXT.md` — D-10 (kanban is embedded in tablo detail, tabs deferred to Phase 5), D-08 (inline edit pattern), D-03 (hard-delete only)
- `.planning/phases/03-tablos-crud/03-CONTEXT.md` — D-05 (inline create pattern), D-06 (inline edit pattern), D-07 (inline delete confirmation) — reused in Files UI
- `.planning/phases/02-authentication/02-CONTEXT.md` — D-23/D-24 (chi route groups, middleware order), RequireAuth middleware
- `.planning/phases/01-foundation/01-CONTEXT.md` — chi router + templ + sqlc + goose migration conventions
### Codebase Patterns (Go backend)
- `backend/internal/web/router.go` — Router structure, protected group, route ordering convention (static before parametric)
- `backend/internal/web/handlers_tablos.go` — HTMX-aware handler pattern (HX-Request detection, fragment vs full-page response) — reuse verbatim for file handlers
- `backend/internal/web/ui/` — Existing Card, Button, Badge, CSRFField components — reuse in Files tab UI
- `backend/templates/` — Base layout, templ component patterns
- `backend/internal/db/queries/tablos.sql` — sqlc query style reference
- `backend/migrations/0004_tasks.sql` — Migration style reference (most recent); Phase 5 adds `0005_files.sql`
- `backend/internal/files/doc.go` — Placeholder package; Phase 5 implementation lands here
- `backend/cmd/worker/main.go` — Exists but Phase 6 wires it; Phase 5 does not touch worker
### Infrastructure
- `compose.yaml` — Add MinIO service here; follow existing postgres service pattern for naming/health checks
</canonical_refs>
<code_context>
## Existing Code Insights
### Reusable Assets
- `ui.Card`, `ui.Button`, `ui.Badge`, `ui.CSRFField` — available in `backend/internal/web/ui/`. Files tab rows and upload form should use these.
- `auth.RequireAuth` middleware — already wraps the tablo route group; file routes nest inside the same group under `/tablos/{id}/files*`.
- `auth.Authed(ctx)` — extracts `*auth.User` from context; file handlers verify `tablo.user_id == authed_user.id` (FILE-06).
- HTMX fragment pattern from Phase 2/3/4 (`HX-Request` header detection, 200+fragment vs 303+redirect) — reuse verbatim for all file mutations.
- Inline delete confirmation pattern (Phase 3 D-07) — reuse for file delete.
- `templates.Layout` — base layout; Files tab fragment extends tablo detail page.
### Established Patterns
- sqlc queries in `backend/internal/db/queries/<domain>.sql`, generated output in `backend/internal/db/sqlc/`. Phase 5 adds `files.sql`.
- Handler constructors return `http.HandlerFunc` with a `FilesDeps` struct (mirrors `TasksDeps` from Phase 4).
- Middleware order locked by D-24: `RequestID → RealIP → SlogLogger → Recoverer → ResolveSession → csrf.Protect → [route groups]`. File routes add to the existing `RequireAuth` group.
- goose migrations numbered sequentially: `0005_files.sql` is next.
- `templ generate` must run after any `.templ` file change.
- Static route segments declared BEFORE parametric in chi (e.g., `/tablos/{id}/files/new` before `/tablos/{id}/files/{file_id}`).
### Integration Points
- `backend/internal/web/router.go` — Add tab sub-routes (`/tablos/{id}`, `/tablos/{id}/tasks`, `/tablos/{id}/files`) and file CRUD routes inside the existing `RequireAuth` group.
- `backend/cmd/web/main.go` — Pass `FilesDeps` (db pool + S3 client) to `NewRouter` alongside existing deps. S3 client constructed from env vars at startup.
- `backend/templates/tablos/detail.templ` — Restructure into tab layout (tab bar + content area). Tasks tab content = existing kanban; Overview tab = existing title/description; Files tab = new.
- `compose.yaml` — Add MinIO service + mc init container for local dev bucket creation.
</code_context>
<specifics>
## Specific Ideas
- S3 key format: `files/{tablo_id}/{uuid}` — e.g., `files/a1b2c3d4-…/f5e6c7b8-….pdf`
- The original filename (from the multipart header) is stored in `tablo_files.filename` for display; it is NOT used as the S3 key (prevents path traversal, collision, and special-character issues).
- MinIO in `compose.yaml` should expose port 9000 (S3 API) and 9001 (console UI) — the mc init container creates the bucket (`xtablo-dev` or similar) and sets it to private.
- `MAX_UPLOAD_SIZE_MB` env var default: 25. `http.MaxBytesReader` wraps the body before any reads begin.
- Signed download URLs: short TTL (5 minutes per ROADMAP suggestion) generated via `s3:GetObject` presigned URL from the Go server on each download request. Client is redirected to the signed URL (302) — no proxying of download bytes.
- Tab sub-routes pattern: `GET /tablos/{id}` renders the full page with Overview tab active; `GET /tablos/{id}/tasks` and `GET /tablos/{id}/files` serve the full page OR a fragment (depending on HX-Request header), consistent with the existing HTMX handler pattern.
</specifics>
<deferred>
## Deferred Ideas
- **Self-hosted MinIO in production** — user asked about this during discussion. For v1, production can point at R2 or any S3-compatible endpoint via env. Hosting MinIO in production on the same VPS as the Go binary is viable (add MinIO to the production Dockerfile or compose stack) but deferred — worth revisiting in the Deploy phase (Phase 7) or v2.
- **File type restriction / allowlist** — deferred to a later phase if security requirements change. Content-type is stored in DB, so adding enforcement later requires only a middleware/validation check, not a schema change.
- **Per-user storage quota** — out of v1 scope per PROJECT.md.
- **File preview / thumbnail generation** — image/PDF previews deferred to v2. Content-type is stored so preview rendering can be added later.
- **Orphan file cleanup job** — files whose tablo is deleted leave orphan S3 objects. Cleanup deferred to Phase 6 (background worker), which will implement a job that reconciles DB rows with S3 objects.
</deferred>
---
*Phase: 5-Files*
*Context gathered: 2026-05-15*

View file

@ -0,0 +1,127 @@
# Phase 5: Files - Discussion Log
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
**Date:** 2026-05-15
**Phase:** 5-files
**Areas discussed:** Upload method, Storage schema, Page layout, File restrictions
---
## Upload method
| Option | Description | Selected |
|--------|-------------|----------|
| Server-proxied | Client POSTs multipart form to Go server; Go streams to S3. Simpler auth, easier server-side enforcement. | ✓ |
| Presigned PUT | Server generates signed URL; client PUTs directly to R2/MinIO. Offloads bandwidth but requires CORS config and two-step flow. | |
**User's choice:** Server-proxied
| Option | Description | Selected |
|--------|-------------|----------|
| Cloudflare R2 | Use R2 as target for v1, same as JS stack. | |
| Self-hosted MinIO | Run MinIO in compose stack. Fully self-contained. | |
| Configurable via env | S3_ENDPOINT/S3_BUCKET/etc as env vars. R2 in prod, MinIO in dev. Zero code difference. | ✓ |
**User's choice:** Configurable via env (support both R2 and MinIO)
**Notes:** User asked about self-hosting S3 storage before selecting — clarified that MinIO is a drop-in S3-compatible option, same Go SDK, just different endpoint URL.
| Option | Description | Selected |
|--------|-------------|----------|
| Yes — add MinIO to compose.yaml | Real S3-compatible bucket locally; no external credentials needed. mc init container creates the bucket. | ✓ |
| No — skip MinIO in compose.yaml | Developers point at R2 or external bucket. Simpler compose.yaml. | |
**User's choice:** Yes — add MinIO to compose.yaml
---
## Storage schema
| Option | Description | Selected |
|--------|-------------|----------|
| Tablo-scoped with UUID: files/{tablo_id}/{uuid} | Tablo ownership visible in key; per-tablo listing trivial; no collisions. | ✓ |
| Flat UUID only: files/{uuid} | Simpler key; tablo grouping only in DB. | |
| Content hash (dedup): files/{sha256} | Dedup via hash; complicates delete if shared across tablos. | |
**User's choice:** Tablo-scoped with UUID
| Option | Description | Selected |
|--------|-------------|----------|
| Server-detect from file bytes | net/http.DetectContentType on first 512 bytes. Ignores browser MIME, prevents spoofing. | ✓ |
| Trust client MIME + extension fallback | Use browser Content-Type, fall back to mime.TypeByExtension. Simpler but spoofable. | |
**User's choice:** Server-detect from file bytes
| Option | Description | Selected |
|--------|-------------|----------|
| Allow duplicates | Each upload gets own UUID, row, and S3 object. No dedup logic. | ✓ |
| Reject exact duplicates by filename | Error if same filename already exists in tablo. | |
| Reject by content hash | Store SHA-256; reject if hash + tablo_id already exists. | |
**User's choice:** Allow duplicates (no dedup for v1)
---
## Page layout
| Option | Description | Selected |
|--------|-------------|----------|
| Single scroll — files section below kanban | Append files section below kanban on same page. No tabs. HTMX-friendly. | |
| Tabs — Overview / Tasks / Files | Introduce tab navigation. Server-rendered, HTMX tab switching. | ✓ |
**User's choice:** Tabs
| Option | Description | Selected |
|--------|-------------|----------|
| Tasks / Files (2 tabs) | Minimal. Tablo title/description as header above tabs. | |
| Overview / Tasks / Files (3 tabs) | Overview = tablo metadata (editable). Tasks = kanban. Files = files. | ✓ |
**User's choice:** Overview / Tasks / Files (3 tabs)
| Option | Description | Selected |
|--------|-------------|----------|
| HTMX + URL push | Each tab is a server sub-route; hx-push-url; back-button works; shareable URLs. | ✓ |
| HTMX swap only (no URL change) | Simpler; no sub-routes; back-button doesn't navigate between tabs. | |
**User's choice:** HTMX + URL push
---
## File restrictions
| Option | Description | Selected |
|--------|-------------|----------|
| 10 MB default | Covers most docs/images. Configurable via MAX_UPLOAD_SIZE_MB. | |
| 25 MB default | Covers larger exports and presentations. Reasonable for single-VPS proxy. | ✓ |
| 50 MB default | Generous; for design files or videos. | |
**User's choice:** 25 MB
| Option | Description | Selected |
|--------|-------------|----------|
| Allow all types | No allowlist. Content-type stored but not used to gate. | ✓ |
| Restrict to safe allowlist | Only images, documents, archives. Reject executables. | |
**User's choice:** Allow all types for v1
---
## Claude's Discretion
- Exact Tailwind styling for file list rows and upload form
- Tab active/inactive visual treatment (underline vs. filled)
- Default tab when visiting `/tablos/{id}` (Overview is natural default)
- Whether upload form is always visible or collapsed behind an "Attach file" button
- HTTP verb for file upload: `POST /tablos/{id}/files`
- File delete UX: inline confirmation (Phase 3 D-07 pattern) vs. modal
- Signed URL TTL for downloads (ROADMAP suggests ~5 minutes)
- Download behavior: 302 redirect to signed URL vs. `target="_blank"` link
## Deferred Ideas
- **Self-hosted MinIO in production** — viable on a single VPS but deferred to Phase 7 (Deploy) or v2
- **File type allowlist / restriction** — content-type stored in DB so adding enforcement later is a validation check, not a schema change
- **Per-user storage quota** — out of v1 scope
- **File previews / thumbnails** — image/PDF preview deferred to v2
- **Orphan file cleanup job** — deferred to Phase 6 (background worker)