From f286c4ef5f3d59daca61dcdf646ec184d45ef457 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 9 May 2026 20:08:28 +0200 Subject: [PATCH] =?UTF-8?q?docs(plan):=20progress=20snapshot=20at=20Task?= =?UTF-8?q?=207=20=E2=80=94=20backend=20complete,=20UI=20next?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tasks 1-7 done in subagent-driven mode (11 commits 5bed62d → a0ffa1b). The entire DB + service + API layer for folders is shipped: schema, manage_folders perm, listTree/createFolder/renameFolder/moveFolder/ deleteFolderSoftRescue, validators, all 4 folder routes, the per-doc move endpoint, and the listDocuments folder filter (with descendant expansion). Reps can already manage folders end-to-end via direct API calls. Records the design decisions made mid-execution: hybrid storage strategy (UUID-flat + path-style download URLs), permission split, soft-rescue delete semantics, cycle prevention with port-scoped ancestor walk, PATCH-body exclusivity via .strict(), and the updatedAt bump rule (per-doc move yes, bulk soft-rescue no). Tests at pause: 1213/1213 vitest, tsc clean. Resume prompt + task ordering for Task 8 onwards included so a fresh session can pick up without context. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plans/2026-05-09-documents-folders.md | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/docs/superpowers/plans/2026-05-09-documents-folders.md b/docs/superpowers/plans/2026-05-09-documents-folders.md index a37cef65..50e47b32 100644 --- a/docs/superpowers/plans/2026-05-09-documents-folders.md +++ b/docs/superpowers/plans/2026-05-09-documents-folders.md @@ -2,6 +2,72 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +--- + +## Progress snapshot — 2026-05-09 (mid-execution pause) + +Working on branch `feat/documents-folders` (off `main`). Subagent-driven execution: every task gets implementer → spec reviewer → code-quality reviewer → fix loop if needed. + +| Task | Topic | Status | Commit(s) | +|------|-------|--------|-----------| +| 1 | Schema + migration (`document_folders` + `folder_id` on documents) | ✅ Done | `5bed62d` + `4a50bab` (fix: Drizzle `.references()` + relations) | +| 2 | `documents.manage_folders` permission | ✅ Done | `e6cf50f` | +| 3 | Service: `listTree` + `createFolder` (TDD) | ✅ Done | `4b31f01` + `5c5ab49` (fix: port-scope test cleanup + tighten message) | +| 4 | Service: rename + move (cycle prevention) + soft-rescue delete | ✅ Done | `e9251a3` + `4ec0004` (fix: audit-log out of tx + portId on ancestor walk + drop misleading updatedAt + userId for rename/move audit) | +| 5 | Zod validators | ✅ Done | `830ac39` | +| 6 | Folder API routes (GET tree / POST / PATCH rename-or-move / DELETE) | ✅ Done | `1082b80` + `e9d5df6` (fix: `.strict()` on union members so `{name, parentId}` together is a 400 not silent drop) | +| 7 | listDocuments folder filter + per-doc move route | ✅ Done | `a0ffa1b` | +| 8 | `useDocumentFolders` hook | 🔴 Not started | — | +| 9 | `FolderTreeSidebar` component | 🔴 Not started | — | +| 10 | `FolderBreadcrumb` component | 🔴 Not started | — | +| 11 | `FolderActionsMenu` (create/rename/delete dialogs) | 🔴 Not started | — | +| 12 | `MoveToFolderDialog` (per-doc picker) | 🔴 Not started | — | +| 13 | Wire `DocumentsHub`: sidebar + breadcrumb, drop signature pill, In-progress tab | 🔴 Not started | — | +| 14 | Dynamic type-filter chips + per-row Move action | 🔴 Not started | — | +| 15 | Admin-configurable Expired tab | 🔴 Not started | — | +| 16 | Playwright smoke test | 🔴 Not started | — | +| 17 | CLAUDE.md update + final verification | 🔴 Not started | — | +| 18 | **NEW** — path-style download URLs (hybrid storage decision) | 🔴 Not started | — | +| 19 | **NEW** — importer from organized S3/filesystem bucket | 🔴 Not started | — | + +**Test posture at pause:** `pnpm exec tsc --noEmit` clean; full vitest suite **1213/1213 passing** (108 test files). 11 commits on the branch ahead of `main`. + +**Backend complete; UI + storage-strategy work remains.** Tasks 1–7 ship the entire DB + service + API layer for folders. Reps can already create / rename / move / delete folders and move documents between them via direct API calls — only the UI and the path-style URL polish are missing. + +### Decision log so far (recorded mid-execution, locking the design) + +- **Storage strategy:** Hybrid — UUID-flat storage paths preserved for parity with the established pattern across brochures, berth PDFs, invoices, reports, templates, expense receipts; the `migrate-storage` byte-verbatim copy keeps working. Documents will gain a `downloadUrl` field whose URL embeds the folder path + filename for browser-tab / shared-link readability, validated for truth on the server (Task 18). The legacy-bucket importer (Task 19) is the migration tool for any organised MinIO tree the team brings over. +- **Permission split:** `documents.manage_folders` is the new perm; `documents.edit` no longer covers folder reorganisation. Admin + sales_manager + director get the new perm by default; sales_agent / viewer / residential_partner do not. +- **Soft-rescue delete:** `deleteFolderSoftRescue` re-parents subfolders + documents to the deleted folder's parent (or root) inside a transaction; never CASCADE. Audit-logged with `metadata.rescuedTo`. +- **Cycle prevention:** `moveFolder` walks the destination's ancestor chain in JS before writing, with both `seen`-set defense and a `portId` filter on the walk so a corrupted parentId pointing at another port can't be silently traversed. +- **PATCH body exclusivity:** Folder PATCH refuses bodies that carry both `name` and `parentId` via `.strict()` on each union member, so a rename request can't silently swallow a move attempt. +- **`updatedAt` semantics on bulk vs per-doc moves:** Bulk soft-rescue does NOT bump per-document `updatedAt` (admin storage op shouldn't surface every doc as "recently modified"). Per-doc move via the `[id]/folder` PATCH DOES bump `updatedAt` (deliberate user action on that doc). + +### What's next when execution resumes + +1. **Task 8** (useDocumentFolders hook) — small TanStack wrapper. ~30 min. +2. **Tasks 9–12** (4 UI components: sidebar tree, breadcrumb, actions menu, move dialog) — each ~30–60 min. Independent of each other. +3. **Task 13** (DocumentsHub wiring) — the integration point. Drops `signatureOnly` pill, adds In-progress tab, threads `folderId` through queries. ~60 min. +4. **Task 14** (dynamic type chips + per-row Move) — ~45 min. +5. **Task 15** (admin-configurable Expired tab) — ~30 min. +6. **Task 16** (Playwright smoke) — ~30 min. +7. **Task 18** (path-style download URLs) — ~60 min, can land independently of UI tasks. +8. **Task 19** (organized-bucket importer) — script-only, ~60–90 min, deferrable. +9. **Task 17** (CLAUDE.md + final verification) — last. + +To resume from a fresh session, paste: + +``` +I'm resuming the documents-folders plan execution. We're on branch +feat/documents-folders. Tasks 1-7 are complete (commits 5bed62d → a0ffa1b). +Use the superpowers:subagent-driven-development skill to continue with +Task 8 (useDocumentFolders hook). Plan: +docs/superpowers/plans/2026-05-09-documents-folders.md. +Tests at last checkpoint: 1213/1213. Branch off main. +``` + +--- + **Goal:** Add a port-wide nestable folder tree to documents, plus quality-of-life polish on the documents hub (drop the confusing "Signature-based only" pill, add an "In progress" tab, surface dynamic type-filter chips, gate the "Expired" tab on a per-port setting). **Architecture:** New `document_folders` table with a self-referencing `parent_id` (unlimited nesting via recursive CTE for path resolution). Add a nullable `folder_id` column to `documents`; null = root. Folder UI is a collapsed-by-default left sidebar tree plus a breadcrumb header on the documents hub. Folder delete moves children to the parent (soft rescue); audit-logged. Folder ops gated on a new `documents.manage_folders` permission, mirroring the existing `files.manage_folders`. All API routes follow the established `withAuth(withPermission(...))` + `parseBody` + `errorResponse` envelope. @@ -2805,6 +2871,7 @@ If you want to push the branch, do so (the user will say if not). The trade-off — that an admin browsing MinIO directly sees UUID gibberish instead of meaningful folder names — is mitigated for the rep-facing UX by serving documents via a **path-style** URL whose user-visible path mirrors the folder tree. The actual file lookup is keyed on the document's `id`; the path segments are decorative + validated for truth. **Files:** + - Create: `src/app/api/v1/documents/[id]/download/[...slug]/route.ts` - Modify: `src/lib/services/documents.service.ts` — add `buildDocumentDownloadUrl(doc, folderTree)` helper that resolves the doc's folder path + filename and emits the URL string. - Modify: `src/lib/services/documents.service.ts` — `listDocuments` and the single-doc detail returns now include a `downloadUrl` field. @@ -2953,6 +3020,7 @@ function buildExpectedSlug( - [ ] **Step 4: Test the truth-check + happy path** Create `tests/integration/document-path-style-download.test.ts` with cases: + - Happy path: download a document via correct URL works (returns the file body). - Wrong-folder-path slug: returns 404. - Wrong-filename slug: returns 404. @@ -2991,6 +3059,7 @@ a downloadUrl field so UI consumers don't reconstruct paths. **Context:** When the team migrates from a legacy MinIO bucket whose folder structure represents real organisation (`s3://old-data/Deals 2026/Q1/contract.pdf`), the importer walks that tree, builds matching `document_folders` rows in the CRM, and inserts `documents` rows pointing at the existing storage keys without rewriting them. One-shot script, idempotent. **Files:** + - Create: `scripts/import-organized-documents.ts` - Optional: `tests/unit/import-organized-documents.test.ts` (testing the path-parser pure function). @@ -3015,6 +3084,7 @@ pnpm tsx scripts/import-organized-documents.ts \ ``` Idempotent: re-runs are no-ops via: + - `document_folders` sibling-uniqueness index (re-create attempts hit ConflictError → caught + skipped). - `documents` rows checked by `(portId, fileStoragePath)` before insert.