---
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.4–3.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)_
