Two-plan wave structure for PREV-01: preview API route + modal (Plan 01, Wave 1) then PreparePanel/FieldPlacer wiring + human verification (Plan 02, Wave 2). Send button gated on previewToken staleness detection. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
11 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 12-filled-document-preview | 01 | execute | 1 |
|
true |
|
|
Purpose: PREV-01 requires the agent see a live filled preview (text, signatures, stamps embedded) before the Send button becomes available. Plan 01 builds the two new artifacts. Plan 02 wires them into PreparePanel and gates the Send button.
Output:
- POST /api/documents/[id]/preview — auth-guarded route that calls preparePdf() to a versioned temp path, streams bytes back, then deletes the temp file
- PreviewModal component — react-pdf Document/Page modal with prev/next page navigation
<execution_context> @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.mdFrom teressa-copeland-homes/src/lib/pdf/prepare-document.ts:
export async function preparePdf(
srcPath: string, // Absolute path to source PDF
destPath: string, // Absolute path to write prepared PDF
textFields: Record<string, string>,
sigFields: SignatureFieldData[],
agentSignatureData: string | null = null,
agentInitialsData: string | null = null,
): Promise<void>
From teressa-copeland-homes/src/lib/db/schema.ts (existing pattern):
// getFieldType coalesces missing type field to 'client-signature'
export function getFieldType(f: SignatureFieldData): SignatureFieldType
export type SignatureFieldData = { ... }
// users table columns (relevant subset):
// agentSignatureData: text()
// agentInitialsData: text()
From react-pdf (installed v10.4.1):
// node_modules/react-pdf/dist/shared/types.d.ts
export type File = string | ArrayBuffer | Blob | Source | null;
// Document accepts ArrayBuffer directly as the file prop
From existing prepare route pattern (src/app/api/documents/[id]/prepare/route.ts):
// Auth guard pattern
const session = await auth();
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 });
// Path construction pattern
const UPLOADS_DIR = path.join(process.cwd(), 'uploads');
const srcPath = path.join(UPLOADS_DIR, doc.filePath);
// Path traversal guard
if (!previewPath.startsWith(UPLOADS_DIR)) return new Response('Forbidden', { status: 403 });
// 422 guard pattern (mirror exactly in preview route)
const hasAgentSigFields = sigFields.some(f => getFieldType(f) === 'agent-signature');
if (hasAgentSigFields && !agentSignatureData) {
return Response.json({ error: 'agent-signature-missing', message: 'No agent signature saved.' }, { status: 422 });
}
const hasAgentInitialsFields = sigFields.some(f => getFieldType(f) === 'agent-initials');
if (hasAgentInitialsFields && !agentInitialsData) {
return Response.json({ error: 'agent-initials-missing', message: 'No agent initials saved.' }, { status: 422 });
}
From existing PdfViewer worker configuration (pattern to replicate in PreviewModal):
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url,
).toString();
The route must:
1. Auth guard: `const session = await auth(); if (!session?.user?.id) return new Response('Unauthorized', { status: 401 });`
2. Params: `const { id } = await params;` — params is `Promise<{ id: string }>` (Next.js 15 async params)
3. Parse body: `const body = await req.json() as { textFillData?: Record<string, string> };`
4. Fetch document from DB: `db.query.documents.findFirst({ where: eq(documents.id, id) })` — return 404 if not found, 422 if no filePath
5. Build versioned preview path: `doc.filePath.replace(/\.pdf$/, \`_preview_${Date.now()}.pdf\`)` joined with UPLOADS_DIR — NOT `_prepared.pdf`
6. Path traversal guard: `if (!previewPath.startsWith(UPLOADS_DIR)) return new Response('Forbidden', { status: 403 });`
7. Fetch agent data: single `db.query.users.findFirst({ where: eq(users.id, session.user.id), columns: { agentSignatureData: true, agentInitialsData: true } })`
8. Apply 422 guards (mirror prepare route exactly):
- `hasAgentSigFields && !agentSignatureData` → 422 with `{ error: 'agent-signature-missing' }`
- `hasAgentInitialsFields && !agentInitialsData` → 422 with `{ error: 'agent-initials-missing' }`
9. Wrap in try/finally: `try { await preparePdf(srcPath, previewPath, textFields, sigFields, agentSigData, agentInitialsData); const pdfBytes = await readFile(previewPath); return new Response(pdfBytes, { headers: { 'Content-Type': 'application/pdf' } }); } finally { unlink(previewPath).catch(() => {}); }`
10. The try/finally ensures the temp file is always deleted even if readFile throws.
Imports: `import { auth } from '@/lib/auth'`, `import { db } from '@/lib/db'`, `import { documents, users } from '@/lib/db/schema'`, `import { getFieldType } from '@/lib/db/schema'`, `import type { SignatureFieldData } from '@/lib/db/schema'`, `import { eq } from 'drizzle-orm'`, `import { preparePdf } from '@/lib/pdf/prepare-document'`, `import path from 'node:path'`, `import { readFile, unlink } from 'node:fs/promises'`
Do NOT import `rename` or `writeFile` — preparePdf handles its own write internally.
Do NOT update any document DB columns (status, preparedFilePath) — this is preview only.
```bash
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | grep "preview/route"
```
No TypeScript errors in preview/route.ts.
File exists at src/app/api/documents/[id]/preview/route.ts; exports POST; TypeScript compiles without errors in that file; does not import rename/writeFile; uses try/finally for cleanup.
Task 2: PreviewModal component
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreviewModal.tsx
Create `PreviewModal.tsx` as a `'use client'` component.
Props interface:
```typescript
interface PreviewModalProps {
pdfBytes: ArrayBuffer;
onClose: () => void;
}
```
Implementation requirements:
1. Configure pdfjs worker at module scope (required — PreviewModal is a new module independent of PdfViewer):
```typescript
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url,
).toString();
```
2. Import CSS layers that PdfViewer uses: `import 'react-pdf/dist/Page/AnnotationLayer.css'` and `import 'react-pdf/dist/Page/TextLayer.css'`
3. State: `const [numPages, setNumPages] = useState(0)` and `const [pageNumber, setPageNumber] = useState(1)`
4. Pass `pdfBytes` (the ArrayBuffer state variable from parent) directly to `<Document file={pdfBytes}>` — NEVER wrap in `new Uint8Array()` or rebuild on each render; the parent stores it in useState so it is a stable reference
5. `onLoadSuccess`: `({ numPages }) => setNumPages(numPages)`
6. Use fixed overlay: `position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 50; display: flex; align-items: center; justify-content: center` — apply as Tailwind classes or inline style (inline style matches existing PreparePanel pattern)
7. Modal inner container: white background, max-width 900px, 90vw width, max-height 90vh, overflow-y auto, padding 16px
8. Navigation: Prev button (disabled when pageNumber <= 1), page counter `{pageNumber} / {numPages || '?'}`, Next button (disabled when pageNumber >= numPages), Close button
9. Render: `<Document file={pdfBytes} onLoadSuccess={...}><Page pageNumber={pageNumber} /></Document>`
Import: `import { Document, Page, pdfjs } from 'react-pdf'`
This component does NOT need to call any API. It receives bytes from the parent.
```bash
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | grep "PreviewModal"
```
No TypeScript errors in PreviewModal.tsx.
File exists at src/app/portal/(protected)/documents/[docId]/_components/PreviewModal.tsx; exports PreviewModal with correct props; configures pdfjs worker; renders Document with ArrayBuffer file prop; TypeScript compiles cleanly.
```bash
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30
```
Zero TypeScript errors project-wide.
ls /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/documents/\[id\]/preview/route.ts && ls /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/portal/\(protected\)/documents/\[docId\]/_components/PreviewModal.tsx
Both files exist.
<success_criteria>
- POST /api/documents/[id]/preview route exists, auth-guarded, uses versioned path, mirrors 422 guards from prepare route, cleans up temp file in try/finally
- PreviewModal exists, accepts ArrayBuffer prop, configures pdfjs worker independently, renders react-pdf Document/Page with prev/next navigation
- Zero TypeScript compilation errors </success_criteria>