docs(05): capture phase context
This commit is contained in:
parent
9e6ab6d5aa
commit
1c02de475e
2 changed files with 266 additions and 0 deletions
139
.planning/phases/05-files/05-CONTEXT.md
Normal file
139
.planning/phases/05-files/05-CONTEXT.md
Normal 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*
|
||||
127
.planning/phases/05-files/05-DISCUSSION-LOG.md
Normal file
127
.planning/phases/05-files/05-DISCUSSION-LOG.md
Normal 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)
|
||||
Loading…
Reference in a new issue