docs(plan): progress snapshot at Task 7 — backend complete, UI next

Tasks 1-7 done in subagent-driven mode (11 commits 5bed62da0ffa1b).
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) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 20:08:28 +02:00
parent a0ffa1baae
commit f286c4ef5f

View File

@@ -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 17 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 912** (4 UI components: sidebar tree, breadcrumb, actions menu, move dialog) — each ~3060 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, ~6090 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.