Files
red/.planning/phases/04-pdf-ingest/04-VERIFICATION.md
2026-03-19 22:13:15 -06:00

186 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 04-pdf-ingest
verified: 2026-03-19T00:00:00Z
status: human_needed
score: 13/13 must-haves verified
re_verification: false
human_verification:
- test: "PDF renders in browser — not blank"
expected: "PDF pages are visibly rendered on the document detail page at /portal/documents/{docId}"
why_human: "react-pdf canvas rendering cannot be verified by static code analysis; blank renders are a known runtime failure mode for worker misconfiguration"
- test: "Page navigation Prev/Next works on multi-page PDF"
expected: "Clicking Next advances to page 2; clicking Prev returns to page 1; buttons are disabled at boundaries"
why_human: "State transitions require a running browser; cannot verify via grep"
- test: "Zoom In / Zoom Out changes page size visibly"
expected: "PDF canvas grows/shrinks in browser on click"
why_human: "Visual behavior; cannot verify statically"
- test: "Download button downloads the PDF file"
expected: "Browser downloads the file rather than navigating away"
why_human: "Browser download behavior cannot be verified statically"
- test: "After adding document via modal, document appears in list without page reload"
expected: "router.refresh() causes new document row to appear in DocumentsTable while staying on the client profile page"
why_human: "router.refresh() behavior and UI update require a running browser session"
- test: "Modal search filters the forms list in real time"
expected: "Typing in the search box reduces the visible list to matching templates"
why_human: "Client-side filter state requires browser interaction to confirm"
- test: "Unauthenticated API access returns 401"
expected: "GET /api/forms-library and GET /api/documents/{id}/file return 'Unauthorized' without a session cookie"
why_human: "Auth middleware is wired correctly in code but end-to-end auth behavior requires a running server to confirm"
---
# Phase 4: PDF Ingest Verification Report
**Phase Goal:** Agents can attach PDFs to client records, browse a seeded forms library, upload custom PDFs, and view documents in-browser with a PDF renderer.
**Verified:** 2026-03-19
**Status:** human_needed — all automated checks pass; 7 items require runtime/browser confirmation
**Re-verification:** No — initial verification
---
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | form_templates table exists with id, name, filename, createdAt, updatedAt columns | VERIFIED | schema.ts lines 26-32; migration 0002_wealthy_zzzax.sql CREATE TABLE confirmed |
| 2 | documents table has filePath and formTemplateId columns | VERIFIED | schema.ts lines 43-44; migration ALTER TABLE confirms both columns |
| 3 | npm run seed:forms reads seeds/forms/ and upserts into form_templates | VERIFIED | scripts/seed-forms.ts: readdir → filter .pdf → onConflictDoUpdate; package.json line 13 wires it |
| 4 | seeds/forms/ directory is tracked in git | VERIFIED | seeds/forms/ directory exists; .gitkeep confirmed by SUMMARY |
| 5 | GET /api/forms-library returns authenticated JSON list ordered by name | VERIFIED | forms-library/route.ts: auth() guard + db.select().from(formTemplates).orderBy(asc(name)) |
| 6 | POST /api/documents copies seed PDF or writes custom upload, inserts documents row | VERIFIED | documents/route.ts: full multipart/JSON branching, copyFile + writeFile, db.insert().returning() |
| 7 | GET /api/documents/[id]/file streams PDF bytes with path traversal protection | VERIFIED | documents/[id]/file/route.ts: startsWith(UPLOADS_BASE) guard + readFile + Content-Type: application/pdf |
| 8 | Unauthenticated requests to all three API routes return 401 | VERIFIED (code) | All three routes: if (!session) return new Response('Unauthorized', { status: 401 }) — needs runtime confirm |
| 9 | Client profile page has an 'Add Document' button that opens a modal | VERIFIED | ClientProfileClient.tsx line 77: "+ Add Document" button sets isAddDocOpen; line 92-94: conditional AddDocumentModal render |
| 10 | Modal shows searchable forms library list; agent can filter by typing | VERIFIED | AddDocumentModal.tsx: fetch /api/forms-library on mount; filtered = templates.filter(... query ...) rendered in ul |
| 11 | Modal has 'Browse files' option for custom PDF upload | VERIFIED | AddDocumentModal.tsx lines 100-103: labeled file input accept="application/pdf" |
| 12 | Clicking a document name navigates to the document detail page | VERIFIED | DocumentsTable.tsx lines 48-54: Link href="/portal/documents/{row.id}" wraps each document name |
| 13 | Document detail page renders PDF with page nav and zoom controls | VERIFIED (code) | PdfViewer.tsx: react-pdf Document+Page with Prev/Next buttons and scale state; PdfViewerWrapper dynamic-imports it; page.tsx wires PdfViewerWrapper with docId |
**Score:** 13/13 truths verified (7 require runtime confirmation)
---
## Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `teressa-copeland-homes/src/lib/db/schema.ts` | formTemplates table + extended documents | VERIFIED | formTemplates defined lines 26-32; formTemplateId + filePath on documents lines 43-44; documentsRelations + clientsRelations present |
| `teressa-copeland-homes/scripts/seed-forms.ts` | Seed script for form_templates from seeds/forms/ | VERIFIED | Reads SEEDS_DIR, filters .pdf, upserts via onConflictDoUpdate — substantive, 37 lines |
| `teressa-copeland-homes/seeds/forms/.gitkeep` | Tracked seed directory placeholder | VERIFIED | Directory exists (confirmed) |
| `teressa-copeland-homes/drizzle/0002_wealthy_zzzax.sql` | Applied migration | VERIFIED | CREATE TABLE form_templates + ALTER TABLE documents with both new columns |
| `teressa-copeland-homes/src/app/api/forms-library/route.ts` | Authenticated GET returning form templates | VERIFIED | Exports GET; auth guard + db query + Response.json — substantive |
| `teressa-copeland-homes/src/app/api/documents/route.ts` | Authenticated POST for template copy and custom upload | VERIFIED | Exports POST; content-type branching; path traversal guard; file copy/write; db insert — 77 lines, substantive |
| `teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts` | Authenticated GET PDF streaming | VERIFIED | Exports GET; auth guard; path traversal guard; readFile + Content-Type header — substantive |
| `teressa-copeland-homes/src/app/portal/_components/AddDocumentModal.tsx` | Client component — searchable modal with file picker | VERIFIED | 'use client'; fetch on mount; filter state; submit handler posts to /api/documents; 137 lines, substantive |
| `teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx` | Client component — react-pdf renderer with nav + zoom | VERIFIED | 'use client'; Document+Page; Prev/Next; Zoom In/Out; Download anchor; worker via import.meta.url — substantive |
| `teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx` | Dynamic import wrapper for SSR safety | VERIFIED | dynamic(() => import('./PdfViewer'), { ssr: false }) — correct pattern |
| `teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx` | Document detail server page | VERIFIED | auth + redirect; db.query with { client: true }; PdfViewerWrapper render; back link — substantive |
| `teressa-copeland-homes/next.config.ts` | transpilePackages for react-pdf + pdfjs-dist | VERIFIED | transpilePackages: ['react-pdf', 'pdfjs-dist'] confirmed |
**Note:** The SUMMARY documented PdfViewer.tsx as the only component in the `_components/` directory. The actual implementation added `PdfViewerWrapper.tsx` as a dynamic-import thin wrapper (correct SSR pattern for react-pdf). Both files exist and are wired correctly. This is an enhancement over the plan, not a gap.
---
## Key Link Verification
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `scripts/seed-forms.ts` | `seeds/forms/` | `readdir` | WIRED | Line 10: `const files = await readdir(SEEDS_DIR)` where SEEDS_DIR = `path.join(process.cwd(), 'seeds', 'forms')` |
| `scripts/seed-forms.ts` | `formTemplates` DB table | `onConflictDoUpdate` | WIRED | Lines 26-30: `db.insert(formTemplates).values(...).onConflictDoUpdate({ target: formTemplates.filename, set: {...} })` |
| `src/app/api/documents/route.ts` | `uploads/clients/{clientId}/` | `copyFile + mkdir` | WIRED | Lines 9, 42-44, 51, 64: UPLOADS_DIR + mkdir(destDir) + copyFile/writeFile |
| `src/app/api/documents/[id]/file/route.ts` | `uploads/` | `readFile after startsWith guard` | WIRED | Lines 8, 24-27: UPLOADS_BASE + filePath.startsWith(UPLOADS_BASE) guard + readFile |
| `AddDocumentModal.tsx` | `/api/forms-library` | `fetch on mount` | WIRED | Lines 18-22: `useEffect(() => { fetch('/api/forms-library').then(r => r.json()).then(setTemplates) }, [])` |
| `AddDocumentModal.tsx` | `/api/documents` | `fetch POST on submit` | WIRED | Lines 52-58: both multipart and JSON POST paths call `fetch('/api/documents', { method: 'POST', ... })` |
| `PdfViewer.tsx` | `/api/documents/{docId}/file` | `react-pdf Document file prop` | WIRED | Lines 49, 58: Download href + Document file prop both use `/api/documents/${docId}/file` |
| `ClientProfileClient.tsx` | `AddDocumentModal` | `conditional render + state` | WIRED | Lines 9, 30, 77-79, 92-94: import + isAddDocOpen state + button onClick + conditional render |
| `DocumentsTable.tsx` | `/portal/documents/{id}` | `Link href` | WIRED | Lines 48-54: `<Link href={'/portal/documents/${row.id}'}>{row.name}</Link>` |
| `page.tsx (docId)` | `PdfViewerWrapper` | `import + render` | WIRED | Line 7: import PdfViewerWrapper; line 41: `<PdfViewerWrapper docId={docId} />` |
---
## Requirements Coverage
| Requirement | Source Plans | Description | Status | Evidence |
|-------------|-------------|-------------|--------|----------|
| DOC-01 | 04-01, 04-02, 04-03 | Agent can browse and import PDF forms from vendor API / manual upload | SATISFIED | Forms library API + AddDocumentModal + seeded templates fulfill the manual-upload fallback; DOC-01 open question (vendor API) resolved as manual upload in research |
| DOC-02 | 04-01, 04-02 | Forms library syncs at least monthly | SATISFIED | `npm run seed:forms` provides idempotent monthly sync via `onConflictDoUpdate` on filename; monthly re-run workflow documented |
| DOC-03 | 04-03 | Agent can view an imported PDF document in the browser | SATISFIED (code) | PdfViewer + PdfViewerWrapper + document detail page wired end-to-end; runtime rendering requires human confirmation |
All three phase-4 requirement IDs (DOC-01, DOC-02, DOC-03) are accounted for. No orphaned requirements found. REQUIREMENTS.md traceability table marks all three as Complete at Phase 4.
---
## Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `AddDocumentModal.tsx` | 76, 118 | `placeholder="..."` attributes | Info | HTML input placeholder attributes — not stub indicators; legitimate UX copy |
No blockers or warnings found. The only "placeholder" matches are standard HTML input placeholder text.
---
## Human Verification Required
### 1. PDF Renders in Browser (Not Blank)
**Test:** Navigate to a client profile, add a document (either from library after seeding a PDF, or via file picker). Click the document name. Observe the document detail page.
**Expected:** The PDF pages are visibly rendered as canvas elements. The page counter reads "1 / N" where N is the actual page count.
**Why human:** react-pdf canvas rendering depends on the pdfjs worker being correctly loaded at runtime. A misconfigured worker silently produces a blank canvas. This cannot be detected by static code analysis.
### 2. Page Navigation Controls Work
**Test:** On a multi-page PDF, click the "Next" button repeatedly and then "Prev".
**Expected:** Page counter advances and retreats; "Prev" is disabled on page 1; "Next" is disabled on the last page.
**Why human:** State transitions require a running browser.
### 3. Zoom Controls Work
**Test:** Click "Zoom In" and "Zoom Out" on the document detail page.
**Expected:** The PDF canvas grows and shrinks proportionally. Scale respects the 0.43.0 bounds.
**Why human:** Visual behavior; cannot verify statically.
### 4. Download Button Downloads the File
**Test:** Click the "Download" button on the document detail page.
**Expected:** Browser initiates a file download (PDF lands in Downloads folder) rather than navigating to the file URL.
**Why human:** Browser download behavior (`<a download>`) cannot be verified statically, and depends on correct Content-Type header from the server.
### 5. Document Appears in List After Modal Close
**Test:** Open the "Add Document" modal from a client profile, submit a document, observe the documents section.
**Expected:** The modal closes and the new document row appears in the DocumentsTable without a full page reload.
**Why human:** `router.refresh()` behavior and client-side state update require a running browser session.
### 6. Modal Search Filters in Real Time
**Test:** Open the "Add Document" modal after seeding at least two templates. Type partial text in the search box.
**Expected:** The forms list filters to only matching templates. Clearing the input restores the full list.
**Why human:** Client-side filter state requires browser interaction.
### 7. Unauthenticated API Access Returns 401
**Test:** In an incognito browser window (no session cookie), navigate to `http://localhost:3000/api/forms-library` and `http://localhost:3000/api/documents/fake-id/file`.
**Expected:** Both return "Unauthorized" (not a JSON list or PDF bytes).
**Why human:** Auth middleware behavior requires a running server to confirm end-to-end.
---
## Summary
All 13 observable truths are VERIFIED at the code level. All key links are WIRED. All three requirement IDs (DOC-01, DOC-02, DOC-03) are satisfied. No stub implementations, missing artifacts, or anti-pattern blockers were found.
The phase delivered:
- A complete database data layer (form_templates table, documents extended, migration applied, seed script wired)
- Three authenticated API routes (forms library, document creation, PDF streaming) with path traversal protection
- A full UI flow (AddDocumentModal with library search and file picker, DocumentsTable with document links, PdfViewer with react-pdf page nav and zoom, document detail page with back link and auth)
- react-pdf v9 correctly configured with `transpilePackages` and `import.meta.url` worker (no CDN dependency)
The 7 human verification items are all runtime/visual behaviors that cannot be tested statically. The code is correctly wired for all of them. The main risk is the react-pdf canvas render (item 1) which is the most common failure mode for pdfjs-dist in Next.js App Router setups.
---
_Verified: 2026-03-19_
_Verifier: Claude (gsd-verifier)_