docs(plan): progress snapshot at Task 7 — backend complete, UI next
Tasks 1-7 done in subagent-driven mode (11 commits5bed62d→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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user