--- 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" --- 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. @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/10-expanded-field-types-end-to-end/10-03-SUMMARY.md 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(['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' } ]; ``` Task 1: DB migration and API routes for agent signature storage 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 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). cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 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. Task 2: AgentSignaturePanel component, profile page, PortalNav link, FieldPlacer token 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 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(initialData); const [isDrawing, setIsDrawing] = useState(!initialData); const canvasRef = useRef(null); const sigPadRef = useRef(null); const [saving, setSaving] = useState(false); const [error, setError] = useState(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 (

Your saved signature:

{/* eslint-disable-next-line @next/next/no-img-element */} Saved agent signature
); } return (
{savedData && ( )}
{error &&

{error}

}
); } ``` **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 (

Profile

Agent Signature

Draw your signature once. It will be embedded in any "Agent Signature" fields when you prepare a document.

); } ``` **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'`.
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 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.
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 - 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 After completion, create `.planning/phases/11-agent-saved-signature-and-signing-workflow/11-01-SUMMARY.md`