Added INIT-01 through INIT-04 requirements to REQUIREMENTS.md. Updated ROADMAP.md with Phase 11.1 goal, success criteria, and plan list. Created three plan files mirroring Phase 11 structure: 11.1-01 (DB migration, API routes, AgentInitialsPanel, FieldPlacer token), 11.1-02 (preparePdf agentInitialsData param + prepare route guard), 11.1-03 (human E2E verification checkpoint). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
21 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 11.1-agent-and-client-initials | 01 | execute | 1 |
|
true |
|
|
Purpose: Once the agent has saved their initials and the palette token exists, Plan 02 can wire up preparePdf() embedding without any further schema or UI work. The existing 'initials' type (client-initials) requires zero changes — it is already fully wired end-to-end from Phase 10.
Output: Profile page with working draw/save/update initials flow; agent-initials token in field palette; security boundary in isClientVisibleField(); zero new npm packages.
<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.md @.planning/phases/11-agent-saved-signature-and-signing-workflow/11-01-SUMMARY.mdFrom src/lib/db/schema.ts (current state — Phase 11 complete, agentInitialsData NOT YET present):
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(),
agentSignatureData: text("agent_signature_data"), // Existing Phase 11 column
// Phase 11.1 adds: agentInitialsData: text("agent_initials_data"),
});
From src/lib/db/schema.ts (current SignatureFieldType — Phase 11 complete):
export type SignatureFieldType =
| 'client-signature'
| 'initials' // This IS client-initials — fully wired, DO NOT RENAME
| 'text'
| 'checkbox'
| 'date'
| 'agent-signature';
// Phase 11.1 adds: | 'agent-initials'
From src/lib/db/schema.ts (current isClientVisibleField — Phase 11 complete):
// CURRENT — only guards agent-signature:
export function isClientVisibleField(field: SignatureFieldData): boolean {
return getFieldType(field) !== 'agent-signature';
}
// AFTER Phase 11.1 modification — must guard both:
export function isClientVisibleField(field: SignatureFieldData): boolean {
const t = getFieldType(field);
return t !== 'agent-signature' && t !== 'agent-initials';
}
From FieldPlacer.tsx (PALETTE_TOKENS — Phase 11 complete, agent-initials NOT YET present):
// Current PALETTE_TOKENS (6 entries after Phase 11 — Phase 11.1 adds the 7th):
const PALETTE_TOKENS: Array<{ id: SignatureFieldType; label: string; color: string }> = [
{ id: 'client-signature', label: 'Signature', color: '#2563eb' }, // blue
{ id: 'initials', label: 'Initials', color: '#7c3aed' }, // purple (CLIENT initials — leave as-is)
{ id: 'checkbox', label: 'Checkbox', color: '#059669' }, // green
{ id: 'date', label: 'Date', color: '#d97706' }, // amber
{ id: 'text', label: 'Text', color: '#64748b' }, // slate
{ id: 'agent-signature', label: 'Agent Signature', color: '#dc2626' }, // red
// ADD: { id: 'agent-initials', label: 'Agent Initials', color: '#ea580c' }, // orange
];
// validTypes set — currently 6 entries:
const validTypes = new Set<string>(['client-signature', 'initials', 'text', 'checkbox', 'date', 'agent-signature']);
// Phase 11.1 adds: 'agent-initials' to this set
From src/app/portal/_components/AgentSignaturePanel.tsx (template to clone — Phase 11 complete):
// AgentInitialsPanel is a clone of AgentSignaturePanel with these differences:
// 1. API endpoint: '/api/agent/initials' (not '/api/agent/signature')
// 2. Canvas height: 80px (not 140px) — initials are compact
// 3. Thumbnail max-height: max-h-16 (not max-h-20) — proportionally shorter
// 4. Labels: "initials" instead of "signature" throughout
// 5. Prop name: initialData (same — matches AgentSignaturePanel)
// 6. Error message: 'Please draw your initials first'
// 7. Save button text: 'Save Initials' / 'Save Updated Initials'
// 8. Update button text: 'Update Initials'
// 9. Alt text: 'Saved agent initials'
// DPR canvas init, signature_pad, save/error/loading state — identical pattern
From src/app/portal/(protected)/profile/page.tsx (Phase 11 state — needs second section):
// Current query fetches only agentSignatureData:
const user = await db.query.users.findFirst({
where: eq(users.id, session.user.id),
columns: { agentSignatureData: true }, // Phase 11.1 adds: agentInitialsData: true
});
// Phase 11.1 adds AgentInitialsPanel import and a second <section> below the existing one
Migration pattern from drizzle/0008 (accept whatever name drizzle-kit generates for 0009):
-- The generated SQL will be:
ALTER TABLE "users" ADD COLUMN "agent_initials_data" text;
-- File will be: drizzle/0009_[auto-generated-words].sql
Auth pattern (identical to all protected routes):
import { auth } from '@/lib/auth';
const session = await auth();
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 });
1. schema.ts — three targeted modifications:
a) Add agentInitialsData column to users pgTable (alongside existing agentSignatureData):
agentSignatureData: text("agent_signature_data"), // existing
agentInitialsData: text("agent_initials_data"), // ADD — nullable, no default
b) Extend SignatureFieldType union — add 'agent-initials' as the last member:
export type SignatureFieldType =
| 'client-signature'
| 'initials'
| 'text'
| 'checkbox'
| 'date'
| 'agent-signature'
| 'agent-initials'; // ADD
c) Update isClientVisibleField() to exclude 'agent-initials' (CRITICAL — prevents agent-initials coordinates from surfacing in the client signing session):
export function isClientVisibleField(field: SignatureFieldData): boolean {
const t = getFieldType(field);
return t !== 'agent-signature' && t !== 'agent-initials';
}
2. Run migration generation and apply:
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run db:generate && npm run db:migrate
Accept whatever filename drizzle-kit generates (will be 0009_[random_words].sql). The SQL content will be:
ALTER TABLE "users" ADD COLUMN "agent_initials_data" text;
3. Create GET/PUT route at src/app/api/agent/initials/route.ts:
Create the directory first: mkdir -p src/app/api/agent/initials
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: { agentInitialsData: true },
});
return Response.json({ agentInitialsData: user?.agentInitialsData ?? 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 initials data' }, { status: 422 });
}
if (dataURL.length > 50_000) {
return Response.json({ error: 'Initials data too large' }, { status: 422 });
}
await db.update(users)
.set({ agentInitialsData: dataURL })
.where(eq(users.id, session.user.id));
return Response.json({ ok: true });
}
1. Create AgentInitialsPanel.tsx at src/app/portal/_components/AgentInitialsPanel.tsx:
This is a clone of AgentSignaturePanel.tsx with the following differences applied (do NOT change anything else):
- API endpoint:
/api/agent/initials - Canvas height:
80px(initials are compact; signature uses 140px) - Thumbnail:
max-h-16(vsmax-h-20for signature) - All user-visible strings use "initials" instead of "signature"
- Error message:
'Please draw your initials first' - Alt text:
'Saved agent initials'
'use client';
import { useEffect, useRef, useState } from 'react';
import SignaturePad from 'signature_pad';
interface AgentInitialsPanelProps {
initialData: string | null;
}
export function AgentInitialsPanel({ initialData }: AgentInitialsPanelProps) {
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 initials first');
return;
}
const dataURL = sigPadRef.current.toDataURL('image/png');
setSaving(true);
setError(null);
try {
const res = await fetch('/api/agent/initials', {
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 initials:</p>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={savedData}
alt="Saved agent initials"
className="max-h-16 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 Initials
</button>
</div>
);
}
return (
<div className="space-y-3">
<canvas
ref={canvasRef}
className="w-full border border-gray-300 rounded bg-white"
style={{ height: '80px', 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 Initials' : 'Save Initials')}
</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. Modify profile/page.tsx — two targeted changes:
a) Update the DB query to fetch agentInitialsData alongside agentSignatureData (single query, one round-trip):
// BEFORE:
columns: { agentSignatureData: true },
// AFTER:
columns: { agentSignatureData: true, agentInitialsData: true },
b) Add import for AgentInitialsPanel and a second <section> below the existing Agent Signature section:
import { AgentInitialsPanel } from '../../_components/AgentInitialsPanel';
Add this section after the closing </section> of the Agent Signature block:
<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 Initials</h2>
<p className="text-sm text-gray-500 mt-1">
Draw your initials once. They will be embedded in any "Agent Initials" fields when you prepare a document.
</p>
</div>
<AgentInitialsPanel initialData={user?.agentInitialsData ?? null} />
</section>
3. Modify FieldPlacer.tsx — two targeted changes:
a) Add 'agent-initials' token to PALETTE_TOKENS (7th entry, after agent-signature):
{ id: 'agent-initials', label: 'Agent Initials', color: '#ea580c' },
Orange (#ea580c) is unused by any existing token. It visually groups with red (agent-signature) as "agent-owned" while remaining distinct from client-visible tokens.
b) Add 'agent-initials' to the validTypes Set:
// Add 'agent-initials' to the existing set — do not change any other entries
const validTypes = new Set<string>(['client-signature', 'initials', 'text', 'checkbox', 'date', 'agent-signature', 'agent-initials']);
Do NOT change any other FieldPlacer logic — drag behavior, overlay rendering, and coordinate conversion all apply to agent-initials the same way they apply to 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; /portal/profile shows both the Agent Signature section (unchanged) and the new Agent Initials section below it; FieldPlacer palette shows an orange "Agent Initials" token as the 7th entry; the existing purple "Initials" client token is still present and unchanged.
npx tsc --noEmitpasses with zero errorsnpm run devstarts without errors- Migration applied:
psql -d [db] -c "SELECT column_name FROM information_schema.columns WHERE table_name='users' AND column_name='agent_initials_data';"returns one row - GET /api/agent/initials returns 401 without session; returns
{ agentInitialsData: null }with valid session - /portal/profile renders both sections — Agent Signature (existing, unchanged) and Agent Initials (new) — with their respective canvases
- Agent can draw on the initials canvas and click "Save Initials" — PUT /api/agent/initials returns 200, thumbnail appears
- Agent can click "Update Initials" — canvas reappears for redraw
- FieldPlacer shows "Agent Initials" as a draggable orange token (7th entry); purple "Initials" client token still present
- Placing an "Agent Initials" field on a document page saves it with
type: 'agent-initials'in signatureFields - GET /api/sign/[token] does NOT return agent-initials fields (isClientVisibleField returns false)
<success_criteria>
- INIT-01: Agent can draw and save initials from /portal/profile; thumbnail shows after save
- INIT-02: "Update Initials" button replaces saved initials with a new one (same PUT route)
- INIT-03 (partial): "Agent Initials" token appears in FieldPlacer palette; placed fields have type 'agent-initials'; isClientVisibleField() excludes them from client signing session (Plan 02 completes the embedding)
- INIT-04 (confirmed): Existing purple "Initials" token still present in palette; no changes to SigningPageClient.tsx or any client-initials pipeline
- TypeScript build clean; zero new npm packages </success_criteria>