docs(11-agent-saved-signature-and-signing-workflow): create phase plan

This commit is contained in:
Chandler Copeland
2026-03-21 13:55:38 -06:00
parent e89c2b3f1b
commit 7783543158
4 changed files with 823 additions and 3 deletions

View File

@@ -0,0 +1,438 @@
---
phase: 11-agent-saved-signature-and-signing-workflow
plan: "01"
type: execute
wave: 1
depends_on: []
files_modified:
- teressa-copeland-homes/src/lib/db/schema.ts
- teressa-copeland-homes/drizzle/0008_agent_signature.sql
- teressa-copeland-homes/src/app/api/agent/signature/route.ts
- teressa-copeland-homes/src/app/portal/(protected)/profile/page.tsx
- teressa-copeland-homes/src/app/portal/_components/AgentSignaturePanel.tsx
- teressa-copeland-homes/src/app/portal/_components/PortalNav.tsx
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx
autonomous: true
requirements:
- AGENT-01
- AGENT-02
- AGENT-03
must_haves:
truths:
- "Agent can navigate to /portal/profile via the nav"
- "Agent can draw a signature on the canvas and save it"
- "A thumbnail of the saved signature appears after saving"
- "Agent can click 'Update Signature' to redraw and replace their saved signature"
- "Agent signature palette token appears in the FieldPlacer (red 'Agent Signature' token)"
- "Placing an agent-signature field saves it in signatureFields with type 'agent-signature'"
artifacts:
- path: "teressa-copeland-homes/src/lib/db/schema.ts"
provides: "agentSignatureData TEXT column on users table"
contains: "agentSignatureData"
- path: "teressa-copeland-homes/src/app/api/agent/signature/route.ts"
provides: "GET/PUT endpoints for reading and writing agent signature"
exports: ["GET", "PUT"]
- path: "teressa-copeland-homes/src/app/portal/_components/AgentSignaturePanel.tsx"
provides: "Client component with signature_pad canvas, save/update/thumbnail flow"
- path: "teressa-copeland-homes/src/app/portal/(protected)/profile/page.tsx"
provides: "Server component fetching agentSignatureData and rendering AgentSignaturePanel"
key_links:
- from: "teressa-copeland-homes/src/app/portal/(protected)/profile/page.tsx"
to: "teressa-copeland-homes/src/app/portal/_components/AgentSignaturePanel.tsx"
via: "initialData prop (string | null)"
pattern: "initialData.*agentSignatureData"
- from: "teressa-copeland-homes/src/app/portal/_components/AgentSignaturePanel.tsx"
to: "/api/agent/signature"
via: "fetch PUT with { dataURL }"
pattern: "fetch.*api/agent/signature"
- from: "teressa-copeland-homes/src/app/portal/_components/PortalNav.tsx"
to: "/portal/profile"
via: "navLinks entry"
pattern: "portal/profile"
---
<objective>
Establish the agent signature storage and UI layer: DB migration adds `agentSignatureData TEXT` to users, GET/PUT API routes read/write the base64 PNG dataURL, a new `/portal/profile` page hosts the `AgentSignaturePanel` draw-and-save canvas component, the portal nav gains a Profile link, and the FieldPlacer palette gets the red "Agent Signature" token.
Purpose: Once the agent has saved their signature and the palette token exists, Plan 02 can wire up the preparePdf() embedding without any further schema or UI work.
Output: Profile page with working draw/save/update signature flow; agent-signature token in field palette; zero new npm packages.
</objective>
<execution_context>
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/10-expanded-field-types-end-to-end/10-03-SUMMARY.md
<interfaces>
<!-- Key types and confirmed patterns extracted from codebase (2026-03-21 inspection). -->
<!-- Executor uses these directly — no codebase exploration needed. -->
From src/lib/db/schema.ts (current users table — agentSignatureData NOT YET present):
```typescript
export const users = pgTable("users", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
email: text("email").notNull().unique(),
passwordHash: text("password_hash").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
// Phase 11 adds: agentSignatureData: text("agent_signature_data"),
});
```
From src/lib/db/schema.ts (SignatureFieldType — agent-signature already valid):
```typescript
export type SignatureFieldType =
| 'client-signature'
| 'initials'
| 'text'
| 'checkbox'
| 'date'
| 'agent-signature';
```
From FieldPlacer.tsx (PALETTE_TOKENS — agent-signature NOT YET present, validTypes already includes it):
```typescript
// Current PALETTE_TOKENS (5 entries — Phase 11 adds the 6th):
const PALETTE_TOKENS: Array<{ id: SignatureFieldType; label: string; color: string }> = [
{ id: 'client-signature', label: 'Signature', color: '#2563eb' },
{ id: 'initials', label: 'Initials', color: '#7c3aed' },
{ id: 'checkbox', label: 'Checkbox', color: '#059669' },
{ id: 'date', label: 'Date', color: '#d97706' },
{ id: 'text', label: 'Text', color: '#64748b' },
// ADD: { id: 'agent-signature', label: 'Agent Signature', color: '#dc2626' },
];
// validTypes already has 'agent-signature' at line 258:
const validTypes = new Set<string>(['client-signature','initials','text','checkbox','date','agent-signature']);
```
From src/app/sign/[token]/_components/SignatureModal.tsx (DPR canvas init — confirmed working):
```typescript
useEffect(() => {
if (!isOpen || tab !== 'draw' || !canvasRef.current) return;
const canvas = canvasRef.current;
const ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext('2d')?.scale(ratio, ratio);
sigPadRef.current = new SignaturePad(canvas, {
backgroundColor: 'rgba(0,0,0,0)',
penColor: '#1B2B4B',
});
return () => sigPadRef.current?.off();
}, [isOpen, tab]);
```
From src/lib/auth.config.ts (session.user.id is set):
```typescript
// callbacks.jwt: if (user) token.id = user.id;
// callbacks.session: session.user.id = token.id as string;
// Usage in any protected API route:
const session = await auth();
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 });
```
Migration pattern from drizzle/0007_equal_nekra.sql:
```sql
-- Pattern for nullable TEXT column:
ALTER TABLE "clients" ADD COLUMN "property_address" text;
-- Phase 11 equivalent:
ALTER TABLE "users" ADD COLUMN "agent_signature_data" text;
```
From src/app/portal/_components/PortalNav.tsx (navLinks pattern):
```typescript
// Current navLinks:
const navLinks = [
{ href: '/portal/dashboard', label: 'Dashboard' },
{ href: '/portal/clients', label: 'Clients' },
// Phase 11 adds: { href: '/portal/profile', label: 'Profile' }
];
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: DB migration and API routes for agent signature storage</name>
<files>
teressa-copeland-homes/src/lib/db/schema.ts
teressa-copeland-homes/drizzle/0008_agent_signature.sql
teressa-copeland-homes/src/app/api/agent/signature/route.ts
</files>
<action>
Three changes in sequence:
**1. schema.ts — add column to users table:**
Add `agentSignatureData: text("agent_signature_data")` as a nullable column (no `.notNull()`, no default) to the `users` pgTable definition. This is the only change to schema.ts.
**2. Run migration generation and apply:**
```bash
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run db:generate && npm run db:migrate
```
The generated SQL file will be at `drizzle/0008_agent_signature.sql` and will contain:
```sql
ALTER TABLE "users" ADD COLUMN "agent_signature_data" text;
```
**3. Create GET/PUT route at src/app/api/agent/signature/route.ts:**
```typescript
import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
export async function GET() {
const session = await auth();
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 });
const user = await db.query.users.findFirst({
where: eq(users.id, session.user.id),
columns: { agentSignatureData: true },
});
return Response.json({ agentSignatureData: user?.agentSignatureData ?? null });
}
export async function PUT(req: Request) {
const session = await auth();
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 });
const { dataURL } = await req.json() as { dataURL: string };
if (!dataURL || !dataURL.startsWith('data:image/png;base64,')) {
return Response.json({ error: 'Invalid signature data' }, { status: 422 });
}
if (dataURL.length > 50_000) {
return Response.json({ error: 'Signature data too large' }, { status: 422 });
}
await db.update(users)
.set({ agentSignatureData: dataURL })
.where(eq(users.id, session.user.id));
return Response.json({ ok: true });
}
```
The directory `src/app/api/agent/signature/` must be created (mkdir -p).
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20</automated>
</verify>
<done>TypeScript compiles clean; migration SQL file exists at drizzle/0008_agent_signature.sql; GET /api/agent/signature returns { agentSignatureData: null } when called with a valid session.</done>
</task>
<task type="auto">
<name>Task 2: AgentSignaturePanel component, profile page, PortalNav link, FieldPlacer token</name>
<files>
teressa-copeland-homes/src/app/portal/_components/AgentSignaturePanel.tsx
teressa-copeland-homes/src/app/portal/(protected)/profile/page.tsx
teressa-copeland-homes/src/app/portal/_components/PortalNav.tsx
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx
</files>
<action>
Four files to create/modify:
**1. Create AgentSignaturePanel.tsx** at `src/app/portal/_components/AgentSignaturePanel.tsx`:
```typescript
'use client';
import { useEffect, useRef, useState } from 'react';
import SignaturePad from 'signature_pad';
interface AgentSignaturePanelProps {
initialData: string | null;
}
export function AgentSignaturePanel({ initialData }: AgentSignaturePanelProps) {
const [savedData, setSavedData] = useState<string | null>(initialData);
const [isDrawing, setIsDrawing] = useState(!initialData);
const canvasRef = useRef<HTMLCanvasElement>(null);
const sigPadRef = useRef<SignaturePad | null>(null);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!isDrawing || !canvasRef.current) return;
const canvas = canvasRef.current;
const ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext('2d')?.scale(ratio, ratio);
sigPadRef.current = new SignaturePad(canvas, {
backgroundColor: 'rgba(0,0,0,0)',
penColor: '#1B2B4B',
});
return () => sigPadRef.current?.off();
}, [isDrawing]);
async function handleSave() {
if (!sigPadRef.current || sigPadRef.current.isEmpty()) {
setError('Please draw your signature first');
return;
}
const dataURL = sigPadRef.current.toDataURL('image/png');
setSaving(true);
setError(null);
try {
const res = await fetch('/api/agent/signature', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dataURL }),
});
if (res.ok) {
setSavedData(dataURL);
setIsDrawing(false);
} else {
const data = await res.json().catch(() => ({ error: 'Save failed' }));
setError(data.error ?? 'Save failed');
}
} catch {
setError('Network error — please try again');
} finally {
setSaving(false);
}
}
if (!isDrawing && savedData) {
return (
<div className="space-y-4">
<p className="text-sm text-gray-600">Your saved signature:</p>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={savedData}
alt="Saved agent signature"
className="max-h-20 border border-gray-200 rounded bg-white p-2"
/>
<button
onClick={() => { setIsDrawing(true); setError(null); }}
className="px-4 py-2 text-sm bg-white border border-gray-300 rounded hover:bg-gray-50"
>
Update Signature
</button>
</div>
);
}
return (
<div className="space-y-3">
<canvas
ref={canvasRef}
className="w-full border border-gray-300 rounded bg-white"
style={{ height: '140px', touchAction: 'none', display: 'block' }}
/>
<div className="flex gap-2">
<button
onClick={() => sigPadRef.current?.clear()}
className="px-3 py-2 text-sm bg-white border border-gray-300 rounded hover:bg-gray-50"
>
Clear
</button>
<button
onClick={handleSave}
disabled={saving}
className="px-4 py-2 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{saving ? 'Saving...' : (savedData ? 'Save Updated Signature' : 'Save Signature')}
</button>
{savedData && (
<button
onClick={() => { setIsDrawing(false); setError(null); }}
className="px-3 py-2 text-sm bg-white border border-gray-300 rounded hover:bg-gray-50"
>
Cancel
</button>
)}
</div>
{error && <p className="text-sm text-red-600">{error}</p>}
</div>
);
}
```
**2. Create profile/page.tsx** at `src/app/portal/(protected)/profile/page.tsx`:
```typescript
import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { AgentSignaturePanel } from '../../_components/AgentSignaturePanel';
export default async function ProfilePage() {
const session = await auth();
if (!session?.user?.id) redirect('/agent/login');
const user = await db.query.users.findFirst({
where: eq(users.id, session.user.id),
columns: { agentSignatureData: true },
});
return (
<div className="max-w-2xl mx-auto py-8 px-4 space-y-8">
<h1 className="text-2xl font-semibold text-gray-900">Profile</h1>
<section className="bg-white border border-gray-200 rounded-lg p-6 space-y-4">
<div>
<h2 className="text-lg font-medium text-gray-900">Agent Signature</h2>
<p className="text-sm text-gray-500 mt-1">
Draw your signature once. It will be embedded in any "Agent Signature" fields when you prepare a document.
</p>
</div>
<AgentSignaturePanel initialData={user?.agentSignatureData ?? null} />
</section>
</div>
);
}
```
**3. Modify PortalNav.tsx** — add Profile to navLinks:
Find the `navLinks` array (currently has Dashboard and Clients entries). Add a third entry: `{ href: '/portal/profile', label: 'Profile' }`.
**4. Modify FieldPlacer.tsx** — add agent-signature palette token:
Find the `PALETTE_TOKENS` array (currently has 5 entries). Add the agent-signature token as the 6th entry:
```typescript
{ id: 'agent-signature', label: 'Agent Signature', color: '#dc2626' },
```
This is the only change needed to FieldPlacer.tsx — the `validTypes` set already includes `'agent-signature'`.
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20</automated>
</verify>
<done>TypeScript compiles clean; `npm run dev` starts without error; visiting /portal/profile shows the AgentSignaturePanel; PortalNav shows "Profile" link; FieldPlacer palette shows a red "Agent Signature" token.</done>
</task>
</tasks>
<verification>
After both tasks complete, verify the full Plan 01 state:
1. `npx tsc --noEmit` passes with zero errors
2. `npm run dev` starts without errors
3. Migration applied: `psql -d [db] -c "SELECT column_name FROM information_schema.columns WHERE table_name='users' AND column_name='agent_signature_data';"` returns one row
4. GET /api/agent/signature returns 401 without session; returns `{ agentSignatureData: null }` with valid session
5. /portal/profile renders the AgentSignaturePanel canvas
6. Agent can draw on canvas and click "Save Signature" — PUT returns 200, thumbnail appears
7. Agent can click "Update Signature" — canvas reappears for redraw
8. FieldPlacer on a document shows "Agent Signature" as a draggable red token
</verification>
<success_criteria>
- AGENT-01: Agent can draw and save a signature from /portal/profile; thumbnail shows after save
- AGENT-02: "Update Signature" button replaces saved signature with a new one (same PUT route)
- AGENT-03: "Agent Signature" token appears in FieldPlacer palette; placed fields have type 'agent-signature'
- TypeScript build clean, zero new npm packages
</success_criteria>
<output>
After completion, create `.planning/phases/11-agent-saved-signature-and-signing-workflow/11-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,258 @@
---
phase: 11-agent-saved-signature-and-signing-workflow
plan: "02"
type: execute
wave: 2
depends_on:
- "11-01"
files_modified:
- teressa-copeland-homes/src/lib/pdf/prepare-document.ts
- teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts
autonomous: true
requirements:
- AGENT-04
must_haves:
truths:
- "When agent prepares a document with agent-signature fields and a saved signature, the prepared PDF contains the signature image at each agent-sig field coordinate"
- "The agent signature is invisible to the client — it is never returned by GET /api/sign/[token]"
- "When agent prepares a document with agent-signature fields but no saved signature, the prepare route returns 422 with { error: 'agent-signature-missing' } — no silent failure"
- "When agent prepares a document with no agent-signature fields, the prepare route succeeds normally regardless of whether a signature is saved"
artifacts:
- path: "teressa-copeland-homes/src/lib/pdf/prepare-document.ts"
provides: "preparePdf() with agentSignatureData param; embedPng + drawImage at agent-sig field coordinates"
contains: "agentSignatureData"
- path: "teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts"
provides: "Fetches agentSignatureData from DB; 422 guard when agent-sig fields present but no sig saved; passes to preparePdf()"
contains: "agent-signature-missing"
key_links:
- from: "teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts"
to: "teressa-copeland-homes/src/lib/pdf/prepare-document.ts"
via: "preparePdf(srcPath, destPath, textFields, sigFields, agentSignatureData)"
pattern: "preparePdf.*agentSignatureData"
- from: "teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts"
to: "teressa-copeland-homes/src/lib/db/schema.ts"
via: "db.query.users.findFirst({ columns: { agentSignatureData: true } })"
pattern: "agentSignatureData.*findFirst"
---
<objective>
Wire the agent signature into the prepare pipeline: `preparePdf()` gains an `agentSignatureData` parameter and embeds the PNG at each agent-signature field coordinate using the same `pdfDoc.embedPng()` + `page.drawImage()` pattern already used for client signatures. The prepare route fetches the agent's saved signature from the DB and passes it to `preparePdf()`, with a 422 guard if agent-signature fields are present but no signature has been saved.
Purpose: Fulfills AGENT-04 — agent's saved signature is baked into the prepared PDF before it reaches the client.
Output: Prepared PDFs with embedded agent signatures; 422 error when signature is missing; client signing session unaffected (isClientVisibleField filter already excludes agent-signature).
</objective>
<execution_context>
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/STATE.md
@.planning/phases/11-agent-saved-signature-and-signing-workflow/11-01-SUMMARY.md
<interfaces>
<!-- Key types and confirmed patterns from codebase (2026-03-21 inspection). -->
From src/lib/pdf/prepare-document.ts (current function signature — BEFORE Phase 11):
```typescript
export async function preparePdf(
srcPath: string,
destPath: string,
textFields: Record<string, string>,
sigFields: SignatureFieldData[],
// Phase 11 adds: agentSignatureData: string | null = null
): Promise<void>
```
Current agent-signature stub at line ~138 (to be replaced):
```typescript
} else if (fieldType === 'agent-signature') {
// Skip — agent signature handled by Phase 11; no placeholder drawn here
}
```
From src/lib/signing/embed-signature.ts (confirmed working embedPng pattern):
```typescript
// embedPng accepts base64 DataURL directly (no Buffer conversion needed)
const pngImage = await pdfDoc.embedPng(sig.dataURL); // 'data:image/png;base64,...'
page.drawImage(pngImage, {
x: sig.x,
y: sig.y,
width: sig.width,
height: sig.height,
});
```
From src/app/api/documents/[id]/prepare/route.ts (current POST handler structure):
```typescript
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 });
// ... body parsing, doc lookup, path resolution ...
await preparePdf(srcPath, destPath, textFields, sigFields); // 4 args currently
// ... DB update, audit log, return ...
}
```
From src/lib/db/schema.ts (getFieldType — confirmed working pattern):
```typescript
export function getFieldType(field: SignatureFieldData): SignatureFieldType {
return field.type ?? 'client-signature';
}
```
CRITICAL: embedPng is called ONCE per document (not once per field). The returned PDFImage
object is reused across multiple drawImage calls. Call pdfDoc.embedPng(agentSignatureData)
before the field loop, store as agentSigImage, then reference agentSigImage inside the loop.
COORDINATE SYSTEM: Use field.x, field.y, field.width, field.height directly. These are already
stored in PDF user-space (bottom-left origin) by FieldPlacer's screenToPdfCoords() conversion.
Do NOT invert or adjust Y — this is identical to how embedSignatureInPdf() works for client sigs.
IMPORT NOTE: PDFImage type from @cantoo/pdf-lib is needed for the agentSigImage variable type.
Check prepare-document.ts existing imports — PDFDocument, rgb, StandardFonts, etc. are already
imported from @cantoo/pdf-lib. Add PDFImage to that import if not already present.
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: preparePdf() — add agentSignatureData param and embed at agent-sig field coordinates</name>
<files>
teressa-copeland-homes/src/lib/pdf/prepare-document.ts
</files>
<action>
Two targeted changes to `preparePdf()`:
**1. Add `agentSignatureData` parameter** with a default of `null` so existing call sites compile without change until the prepare route is updated in Task 2:
```typescript
export async function preparePdf(
srcPath: string,
destPath: string,
textFields: Record<string, string>,
sigFields: SignatureFieldData[],
agentSignatureData: string | null = null, // ADD THIS
): Promise<void>
```
**2. Embed the agent signature PNG once before the field loop:**
After `const pages = pdfDoc.getPages();` and before the field loop, add:
```typescript
// Embed agent signature image once — reused across all agent-sig fields
let agentSigImage: import('@cantoo/pdf-lib').PDFImage | null = null;
if (agentSignatureData) {
agentSigImage = await pdfDoc.embedPng(agentSignatureData);
}
```
If PDFImage is already imported from @cantoo/pdf-lib at the top of the file (check the existing imports), use the named import directly instead of the inline import type. Only add `PDFImage` to the destructured import if it is not already there — do not change any other imports.
**3. Replace the agent-signature stub** in the field loop with real embedding:
Find the existing `else if (fieldType === 'agent-signature') { // Skip — ... }` block and replace it with:
```typescript
} else if (fieldType === 'agent-signature') {
if (agentSigImage) {
page.drawImage(agentSigImage, {
x: field.x,
y: field.y,
width: field.width,
height: field.height,
});
}
// If no signature saved: the prepare route guards against this with 422 before calling preparePdf
}
```
No other changes to prepare-document.ts. The function body, other field type branches, and the atomic write logic remain untouched.
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20</automated>
</verify>
<done>TypeScript compiles clean; preparePdf() function signature now has 5 parameters (last is optional); agent-signature stub replaced with drawImage call; no regressions in other field type branches.</done>
</task>
<task type="auto">
<name>Task 2: prepare route — fetch agentSignatureData, 422 guard, pass to preparePdf</name>
<files>
teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts
</files>
<action>
Three additions to the POST handler in `src/app/api/documents/[id]/prepare/route.ts`:
**1. Import `users` from schema** (if not already imported — check existing imports at top of file):
```typescript
import { users } from '@/lib/db/schema';
```
Also ensure `getFieldType` is imported from `@/lib/db/schema` (needed for the guard check). If already present, do not duplicate.
**2. Fetch agentSignatureData from DB** — add this block AFTER the existing doc lookup and sigFields parsing, BEFORE the `preparePdf()` call:
```typescript
// Fetch agent's saved signature for embedding at agent-signature field coordinates
const agentUser = await db.query.users.findFirst({
where: eq(users.id, session.user.id),
columns: { agentSignatureData: true },
});
const agentSignatureData = agentUser?.agentSignatureData ?? null;
// Guard: if document has agent-signature fields but no signature saved, block prepare
const hasAgentSigFields = sigFields.some(f => getFieldType(f) === 'agent-signature');
if (hasAgentSigFields && !agentSignatureData) {
return Response.json(
{ error: 'agent-signature-missing', message: 'No agent signature saved. Go to Profile to save your signature first.' },
{ status: 422 }
);
}
```
**3. Pass `agentSignatureData` to `preparePdf()`** — update the existing call:
```typescript
// BEFORE (4 args):
await preparePdf(srcPath, destPath, textFields, sigFields);
// AFTER (5 args):
await preparePdf(srcPath, destPath, textFields, sigFields, agentSignatureData);
```
Do not change any other logic in the route handler — the DB update, audit logging, and response all remain unchanged.
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20</automated>
</verify>
<done>TypeScript compiles clean; prepare route fetches agentSignatureData; guard returns 422 with { error: 'agent-signature-missing' } when agent-sig fields exist but no signature saved; preparePdf called with 5 args including agentSignatureData.</done>
</task>
</tasks>
<verification>
After both tasks complete, verify the full Plan 02 state:
1. `npx tsc --noEmit` passes with zero errors
2. `npm run build` succeeds (or `npm run dev` starts clean)
3. Functional test — draw save place prepare round-trip:
a. Visit /portal/profile, draw and save a signature
b. Open a document in the portal, place an "Agent Signature" field token on any page
c. Fill text fields and click Prepare
d. Prepare succeeds (200 response)
e. Download the prepared PDF and verify the agent signature PNG is embedded at the correct position
4. Guard test — prepare with no saved signature:
a. Use a fresh test account (or temporarily clear agentSignatureData in DB)
b. Place an agent-signature field, attempt to prepare
c. Prepare route returns 422 with `{ error: 'agent-signature-missing' }`
5. Client signing page test — open the signing link for the prepared document:
a. Signing page does NOT show an agent-signature field overlay (isClientVisibleField already filters it)
b. Client can sign normally — only client-signature and initials overlays appear
</verification>
<success_criteria>
- AGENT-04: Agent's saved PNG is embedded at each agent-signature field coordinate in the prepared PDF
- No agent-signature content is exposed to the client via GET /api/sign/[token]
- Prepare fails with actionable 422 when agent-sig fields exist but no signature saved
- TypeScript build clean; zero new npm packages; no regressions on other field types
</success_criteria>
<output>
After completion, create `.planning/phases/11-agent-saved-signature-and-signing-workflow/11-02-SUMMARY.md`
</output>

View File

@@ -0,0 +1,124 @@
---
phase: 11-agent-saved-signature-and-signing-workflow
plan: "03"
type: execute
wave: 3
depends_on:
- "11-01"
- "11-02"
files_modified: []
autonomous: false
requirements:
- AGENT-01
- AGENT-02
- AGENT-03
- AGENT-04
must_haves:
truths:
- "Agent can draw, save, and see a thumbnail of their signature from /portal/profile"
- "Agent can update (replace) their saved signature"
- "Agent can place an Agent Signature token on a PDF document in the FieldPlacer"
- "Preparing a document with agent-signature fields embeds the saved PNG in the correct position in the prepared PDF"
- "Opening the client signing link for the prepared document does NOT show any agent-signature overlay"
artifacts:
- path: "teressa-copeland-homes/src/app/portal/(protected)/profile/page.tsx"
provides: "Accessible profile page with draw/save/thumbnail signature flow"
- path: "teressa-copeland-homes/src/lib/pdf/prepare-document.ts"
provides: "preparePdf with agent PNG embedding at agent-sig field coordinates"
key_links:
- from: "Agent saves signature at /portal/profile"
to: "Prepared PDF"
via: "PUT /api/agent/signature → users.agentSignatureData → preparePdf() → pdfDoc.embedPng + drawImage"
pattern: "full pipeline"
---
<objective>
Human verification of the complete Phase 11 agent signature workflow: draw once at profile, place fields on a document, prepare the document, verify the signature is embedded in the correct position in the PDF, and confirm the client signing session is unaffected.
Purpose: All four AGENT requirements are verified end-to-end by a human using the portal.
Output: Phase 11 marked complete; AGENT-01 through AGENT-04 confirmed working.
</objective>
<execution_context>
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/STATE.md
@.planning/phases/11-agent-saved-signature-and-signing-workflow/11-01-SUMMARY.md
@.planning/phases/11-agent-saved-signature-and-signing-workflow/11-02-SUMMARY.md
</context>
<tasks>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 1: Human verification — full Phase 11 agent signature end-to-end</name>
<action>Run the dev server and execute the 5-step verification below. There are no code changes in this plan — all code was delivered in Plans 01 and 02.</action>
<what-built>
Complete Phase 11 agent signature workflow:
- /portal/profile page with signature draw canvas and thumbnail
- "Agent Signature" token in FieldPlacer palette (red, draggable)
- preparePdf() embeds saved PNG at agent-signature field coordinates
- Prepare route fetches agentSignatureData and passes 422 guard
- Client signing session filters out agent-signature fields (unchanged from Phase 8)
</what-built>
<how-to-verify>
Start the dev server: `cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run dev`
**Step 1 — Save your signature (AGENT-01):**
- Visit http://localhost:3000/portal/profile
- Confirm "Profile" appears in the portal nav
- Draw a distinctive signature on the canvas
- Click "Save Signature"
- Expected: Canvas is replaced by a thumbnail image of your signature
**Step 2 — Update signature (AGENT-02):**
- Click "Update Signature" on the profile page
- Draw a slightly different signature
- Click "Save Updated Signature"
- Expected: New thumbnail replaces the old one
**Step 3 — Place agent-signature field (AGENT-03):**
- Open any document in the portal and go to the prepare tab
- Confirm the FieldPlacer palette shows a red "Agent Signature" token
- Drag the "Agent Signature" token and drop it onto the document (any page, any position)
- Expected: A red field box appears on the PDF where you dropped it
**Step 4 — Prepare with embedded signature (AGENT-04):**
- Assign the document to a client, fill any required text fields
- Click Prepare
- Expected: Prepare succeeds (no error, document status changes)
- Download the prepared PDF from the portal
- Open the downloaded PDF and navigate to the page where you placed the agent-signature field
- Expected: Your saved signature image appears at that position in the PDF
**Step 5 — Client signing session is unaffected:**
- Send the prepared document to a client (or use a test signing link)
- Open the signing link in the browser
- Expected: The client signing page does NOT show any overlay or prompt for the agent-signature position
- Expected: Client-signature and initials overlays (if placed) still appear normally for the client
</how-to-verify>
<resume-signal>Type "approved" if all 5 steps pass. Describe any issues found if not.</resume-signal>
<verify>All 5 steps pass and human types "approved"</verify>
<done>Full end-to-end round-trip confirmed: agent draws, saves, places field, prepares, and embedded signature appears in prepared PDF; client signing session is unaffected</done>
</task>
</tasks>
<verification>
Human verification approved — all four AGENT requirements confirmed by live testing:
- AGENT-01: Agent drew and saved signature; thumbnail visible at /portal/profile
- AGENT-02: "Update Signature" replaced saved signature with a new one
- AGENT-03: Red "Agent Signature" token visible and usable in FieldPlacer palette
- AGENT-04: Prepared PDF contains agent signature PNG at the placed field coordinates; client signing session shows no agent-signature overlay
</verification>
<success_criteria>
Human approves all 5 verification steps. Phase 11 is complete and all AGENT requirements are satisfied.
</success_criteria>
<output>
After completion, create `.planning/phases/11-agent-saved-signature-and-signing-workflow/11-03-SUMMARY.md`
</output>