From 1c02de475ed0e4389cf2e116aa2139532ee9d44f Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Fri, 15 May 2026 10:58:19 +0200 Subject: [PATCH] docs(05): capture phase context --- .planning/phases/05-files/05-CONTEXT.md | 139 ++++++++++++++++++ .../phases/05-files/05-DISCUSSION-LOG.md | 127 ++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 .planning/phases/05-files/05-CONTEXT.md create mode 100644 .planning/phases/05-files/05-DISCUSSION-LOG.md diff --git a/.planning/phases/05-files/05-CONTEXT.md b/.planning/phases/05-files/05-CONTEXT.md new file mode 100644 index 0000000..4c7841c --- /dev/null +++ b/.planning/phases/05-files/05-CONTEXT.md @@ -0,0 +1,139 @@ +# Phase 5: Files - Context + +**Gathered:** 2026-05-15 +**Status:** Ready for planning + + +## 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. + + + + +## 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"`). + + + + +## 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 + + + + +## 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/.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. + + + + +## 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. + + + + +## 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. + + + +--- + +*Phase: 5-Files* +*Context gathered: 2026-05-15* diff --git a/.planning/phases/05-files/05-DISCUSSION-LOG.md b/.planning/phases/05-files/05-DISCUSSION-LOG.md new file mode 100644 index 0000000..5aa0083 --- /dev/null +++ b/.planning/phases/05-files/05-DISCUSSION-LOG.md @@ -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)