Files
red/.planning/phases/11.1-agent-and-client-initials/11.1-RESEARCH.md

727 lines
40 KiB
Markdown
Raw Normal View History

# Phase 11.1: Agent and Client Initials - Research
**Researched:** 2026-03-21
**Domain:** Drizzle ORM schema migration, Next.js App Router API routes, signature_pad canvas, @cantoo/pdf-lib PNG embedding, signing page client interactivity
**Confidence:** HIGH
<phase_requirements>
## Phase Requirements
Phase 11.1 has no IDs yet in REQUIREMENTS.md — it was inserted urgently and REQUIREMENTS.md has not been updated. The planner MUST assign IDs when creating plans. Recommended IDs and descriptions:
| Recommended ID | Description | Research Support |
|----------------|-------------|-----------------|
| INIT-01 | Agent can draw and save initials to their account profile (drawn once, reused) | Same pattern as AGENT-01: `agentInitialsData TEXT` column on `users` table; GET/PUT `/api/agent/initials`; `AgentInitialsPanel` component (clone of `AgentSignaturePanel`) |
| INIT-02 | Agent can update their saved initials at any time | Same as AGENT-02: PUT `/api/agent/initials` is upsert; "Update Initials" button when saved initials already exist |
| INIT-03 | Agent can place agent-initials fields on a PDF; saved initials PNG embedded at prepare time (invisible to client) | Same as AGENT-03/04: add `'agent-initials'` to `PALETTE_TOKENS`, `validTypes`, `isClientVisibleField()`, and `preparePdf()` |
| INIT-04 | Agent can place client-initials fields on a PDF; client must initial each one during the signing session | Mirrors `'client-signature'` in every pipeline layer: `isClientVisibleField()` returns true, signing page shows interactive overlay, POST route embeds via `embedSignatureInPdf()` |
**Note to planner:** Add these four IDs to `REQUIREMENTS.md` under a new `### Initials` section in `## v1.1 Requirements` and update the Traceability table before creating plan files.
</phase_requirements>
---
## Summary
Phase 11.1 introduces two parallel initials features that each mirror an existing pattern exactly. Agent initials mirrors the agent signature pattern from Phase 11 (draw once on profile, embed at prepare time, invisible to client). Client-initials mirrors the client-signature pattern from Phase 6/10 (client draws during signing session, embedded by the POST route). The codebase is already scaffolded for both: the existing `'initials'` field type in `SignatureFieldType` is already treated as client-visible and client-interactive in every layer of the pipeline.
The key question going in was: does the existing `'initials'` type cover "client-initials" or is it a generic type? **Answer from codebase inspection:** `'initials'` is already fully wired as a client-interactive type — it appears in `isClientVisibleField()` (returns true), `SigningPageClient.tsx` (renders interactive overlay with purple pulse animation, triggers modal with "Add Initials" title), and the POST route's `signableFields` filter. So `'initials'` = client-initials. **Do NOT rename or replace it.** The only new field types needed are `'agent-initials'` for the agent side and a new `PALETTE_TOKENS` distinction that separates "Initials (client)" from "Agent Initials".
The critical constraint is that `'agent-initials'` must be treated like `'agent-signature'` in the security layer: `isClientVisibleField()` must return false for it, the prepare route must fetch `agentInitialsData`, and the signing POST route must NOT include it in `signableFields`. The existing `'initials'` type already works end-to-end for client-initials with no changes needed — no new field type for client-initials.
**Primary recommendation:** Implement in two sequential plans — Plan A: DB migration (`agentInitialsData TEXT` on `users`) + GET/PUT `/api/agent/initials` routes + `AgentInitialsPanel` component + profile page section + `isClientVisibleField()` update + `PALETTE_TOKENS` addition + `validTypes` addition; Plan B: `preparePdf()` agent-initials embedding + prepare route guard. The existing `'initials'` type handles client-initials with zero code changes — confirm this and document it, but do not change it.
---
## Standard Stack
### Core (All Existing — No New Dependencies)
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| drizzle-orm | ^0.45.1 | ADD `agentInitialsData TEXT` column to `users` table via schema + migration | Same pattern as Phase 11's `agentSignatureData`; Phase 9's `propertyAddress`; already proven |
| @cantoo/pdf-lib | ^2.6.3 | `pdfDoc.embedPng(agentInitialsData)` + `page.drawImage()` at agent-initials field coordinates | Exact same mechanism used for agent-signature in Phase 11; `embedPng` accepts base64 DataURL directly |
| signature_pad | ^5.1.3 | Canvas drawing in `AgentInitialsPanel` (profile page) | Already used in `AgentSignaturePanel` and `SignatureModal`; identical API |
| next-auth | 5.0.0-beta.30 | `auth()` in API routes for `session.user.id` | Already in all protected routes; unchanged |
| drizzle-kit | ^0.31.10 | `npm run db:generate && npm run db:migrate` | Standard migration workflow; all 8 prior migrations use this |
| next | 16.2.0 | New API route `/api/agent/initials`; profile page section | Already the app framework |
### No New Dependencies
Phase 11.1 adds zero new npm packages. Every primitive needed (canvas drawing, PNG embedding, session auth, DB updates) already exists and is tested.
**Installation:**
```bash
# No new packages needed
```
---
## Architecture Patterns
### Recommended File Creation/Modification Map
```
src/lib/db/schema.ts
# Modified: add agentInitialsData TEXT column to users table
# Modified: isClientVisibleField() — add 'agent-initials' to the false-return branch
drizzle/0009_agent_initials.sql
# New: ALTER TABLE "users" ADD COLUMN "agent_initials_data" text;
# Generated by: npm run db:generate
src/app/api/agent/initials/route.ts
# New: GET returns { agentInitialsData: string|null }
# PUT body: { dataURL: string } — validates + stores in DB
src/app/portal/(protected)/profile/page.tsx
# Modified: add AgentInitialsPanel section below the existing AgentSignaturePanel section
src/app/portal/_components/AgentInitialsPanel.tsx
# New: clone of AgentSignaturePanel — identical structure, different labels and API endpoint
src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx
# Modified: add 'agent-initials' token to PALETTE_TOKENS
# Modified: add 'agent-initials' to validTypes set
src/lib/pdf/prepare-document.ts
# Modified: add agentInitialsData param; embedPng + drawImage at agent-initials coordinates
# NOTE: 'initials' field type (client-initials) already draws a purple placeholder — NO CHANGE
src/app/api/documents/[id]/prepare/route.ts
# Modified: fetch agentInitialsData from DB; 422 guard; pass to preparePdf()
```
**CRITICAL INSIGHT — What Does NOT Change:**
- `'initials'` field type in `SignatureFieldType` — keep as-is (it IS client-initials already)
- `isClientVisibleField()` for `'initials'` — already returns true — keep as-is
- `SigningPageClient.tsx` — already handles `'initials'` as interactive with purple overlay — NO CHANGE
- `SignatureModal.tsx` — already shows "Add Initials" title when `activeFieldType === 'initials'` — NO CHANGE
- POST `/api/sign/[token]` `signableFields` filter — already includes `'initials'` — NO CHANGE
- `preparePdf()` `'initials'` branch — already draws purple "Initials" placeholder — NO CHANGE
### Pattern 1: DB Migration — Add agentInitialsData TEXT Column
**What:** One `ALTER TABLE` adds `agent_initials_data TEXT` (nullable, no default) to the `users` table. In `schema.ts`, add `agentInitialsData: text("agent_initials_data")` to the `users` pgTable definition alongside the existing `agentSignatureData`.
**Example:**
```typescript
// src/lib/db/schema.ts — modified users table
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
agentInitialsData: text("agent_initials_data"), // NEW Phase 11.1 column
});
```
```sql
-- drizzle/0009_agent_initials.sql (generated by npm run db:generate)
ALTER TABLE "users" ADD COLUMN "agent_initials_data" text;
```
**Migration naming:** drizzle-kit auto-generates the filename. The previous migration was `0008_windy_cloak.sql`. The next will be `0009_[random_words].sql`. Accept whatever name drizzle-kit produces.
### Pattern 2: isClientVisibleField() Update — Add agent-initials to False Branch
**What:** `isClientVisibleField()` currently returns false only for `'agent-signature'`. It must also return false for `'agent-initials'`. The signing GET route filters fields using this function — `'agent-initials'` must never surface to the client.
**Current code (to be modified):**
```typescript
export function isClientVisibleField(field: SignatureFieldData): boolean {
return getFieldType(field) !== 'agent-signature';
}
```
**Modified code:**
```typescript
export function isClientVisibleField(field: SignatureFieldData): boolean {
const t = getFieldType(field);
return t !== 'agent-signature' && t !== 'agent-initials';
}
```
**Also:** Add `'agent-initials'` to the `SignatureFieldType` union (one line addition):
```typescript
export type SignatureFieldType =
| 'client-signature'
| 'initials' // client-initials — already fully wired throughout the pipeline
| 'text'
| 'checkbox'
| 'date'
| 'agent-signature'
| 'agent-initials'; // NEW: agent draws once on profile, embedded at prepare time
```
### Pattern 3: GET/PUT /api/agent/initials Route
**What:** Exact parallel of `/api/agent/signature/route.ts` from Phase 11. Same structure: GET reads agentInitialsData, PUT validates and writes it.
**Example:**
```typescript
// src/app/api/agent/initials/route.ts
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 });
}
```
### Pattern 4: AgentInitialsPanel Component
**What:** Clone of `AgentSignaturePanel.tsx` with three changes: (1) prop is `initialData` for initials, (2) API endpoint is `/api/agent/initials`, (3) labels say "Initials" not "Signature". Canvas size should be smaller since initials are narrower — suggest 200px wide canvas or same 100% width but shorter height (80px vs 140px).
**Example (key differences from AgentSignaturePanel):**
```typescript
// src/app/portal/_components/AgentInitialsPanel.tsx
'use client';
import { useEffect, useRef, useState } from 'react';
import SignaturePad from 'signature_pad';
interface AgentInitialsPanelProps {
initialData: string | null; // agentInitialsData from DB
}
export function AgentInitialsPanel({ initialData }: AgentInitialsPanelProps) {
// Same state shape as AgentSignaturePanel
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);
// Same DPR canvas init (identical to AgentSignaturePanel and SignatureModal)
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', { // DIFFERENT endpoint
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);
}
}
// Thumbnail view when saved (same structure as AgentSignaturePanel)
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" // max-h-16 (shorter than signature)
/>
<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>
);
}
// Drawing view — same structure, shorter canvas (80px vs 140px for signature)
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' }} // SHORTER: initials are compact
/>
<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>
);
}
```
### Pattern 5: Profile Page — Add AgentInitialsPanel Section
**What:** The existing `/portal/profile/page.tsx` has one `<section>` for Agent Signature. Add a second `<section>` below it for Agent Initials.
**Example:**
```typescript
// src/app/portal/(protected)/profile/page.tsx — modified
import { AgentSignaturePanel } from '../../_components/AgentSignaturePanel';
import { AgentInitialsPanel } from '../../_components/AgentInitialsPanel'; // NEW import
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, agentInitialsData: true }, // ADD agentInitialsData
});
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>
{/* EXISTING: Agent Signature section */}
<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 &quot;Agent Signature&quot; fields when you prepare a document.
</p>
</div>
<AgentSignaturePanel initialData={user?.agentSignatureData ?? null} />
</section>
{/* NEW: Agent Initials section */}
<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 &quot;Agent Initials&quot; fields when you prepare a document.
</p>
</div>
<AgentInitialsPanel initialData={user?.agentInitialsData ?? null} />
</section>
</div>
);
}
```
### Pattern 6: FieldPlacer — Add agent-initials Token and validTypes Entry
**What:** `FieldPlacer.tsx` has `PALETTE_TOKENS` (currently 6 entries after Phase 11) and `validTypes` set. Add `'agent-initials'` to both. Choose a distinct color — orange is not yet used.
**Example:**
```typescript
// PALETTE_TOKENS — add agent-initials token (orange, distinct from existing colors)
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)
{ 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
{ id: 'agent-initials', label: 'Agent Initials', color: '#ea580c' }, // orange — NEW
];
// validTypes set — add 'agent-initials'
const validTypes = new Set<string>(['client-signature', 'initials', 'text', 'checkbox', 'date', 'agent-signature', 'agent-initials']);
```
**Color rationale:** Orange (`#ea580c`) is visually adjacent to red (agent-signature) but distinct. It signals "agent-owned" like the red token but different asset type. Purple is already used for client initials, creating clear visual separation between agent-initials (orange) and client-initials (purple).
### Pattern 7: preparePdf() — Add agentInitialsData Param and Embed
**What:** `preparePdf()` currently has 5 parameters (including `agentSignatureData`). Add a 6th: `agentInitialsData: string | null = null`. Add embed-once-draw-many for `'agent-initials'` fields, exactly parallel to `'agent-signature'`. The `'initials'` branch (client-initials purple placeholder) is untouched.
**Example:**
```typescript
// src/lib/pdf/prepare-document.ts — updated signature
export async function preparePdf(
srcPath: string,
destPath: string,
textFields: Record<string, string>,
sigFields: SignatureFieldData[],
agentSignatureData: string | null = null,
agentInitialsData: string | null = null, // NEW PARAM
): Promise<void> {
// ...existing code...
// Embed agent signature image once (existing)
let agentSigImage: PDFImage | null = null;
if (agentSignatureData) {
agentSigImage = await pdfDoc.embedPng(agentSignatureData);
}
// Embed agent initials image once — NEW
let agentInitialsImage: PDFImage | null = null;
if (agentInitialsData) {
agentInitialsImage = await pdfDoc.embedPng(agentInitialsData);
}
for (const field of sigFields) {
const page = pages[field.page - 1];
if (!page) continue;
const fieldType = getFieldType(field);
// ... existing branches for client-signature, initials, checkbox, date, text, agent-signature ...
} else if (fieldType === 'agent-initials') { // NEW BRANCH
if (agentInitialsImage) {
page.drawImage(agentInitialsImage, {
x: field.x,
y: field.y,
width: field.width,
height: field.height,
});
}
// If no initials saved: prepare route guards with 422 before reaching here
}
}
// ...rest unchanged...
}
```
### Pattern 8: Prepare Route — Fetch agentInitialsData, Guard, Pass to preparePdf
**What:** Three additions to the POST handler: (1) fetch `agentInitialsData` from users alongside `agentSignatureData`, (2) add a 422 guard if agent-initials fields exist but no initials saved, (3) pass `agentInitialsData` as 6th argument to `preparePdf()`.
**Example:**
```typescript
// src/app/api/documents/[id]/prepare/route.ts — additions
// Fetch BOTH agent signature and agent initials in a single DB query
const agentUser = await db.query.users.findFirst({
where: eq(users.id, session.user.id),
columns: { agentSignatureData: true, agentInitialsData: true }, // ADD agentInitialsData
});
const agentSignatureData = agentUser?.agentSignatureData ?? null;
const agentInitialsData = agentUser?.agentInitialsData ?? null; // NEW
// Guard: agent-signature fields (existing)
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 }
);
}
// Guard: agent-initials fields (NEW)
const hasAgentInitialsFields = sigFields.some(f => getFieldType(f) === 'agent-initials');
if (hasAgentInitialsFields && !agentInitialsData) {
return Response.json(
{ error: 'agent-initials-missing', message: 'No agent initials saved. Go to Profile to save your initials first.' },
{ status: 422 }
);
}
// Pass both to preparePdf (6 args now)
await preparePdf(srcPath, destPath, textFields, sigFields, agentSignatureData, agentInitialsData);
```
### Anti-Patterns to Avoid
- **Adding a new `'client-initials'` field type.** The existing `'initials'` type IS client-initials. It is already wired throughout every layer of the pipeline. Adding a new type would require duplicating all the handling without any benefit. Use `'initials'` for client-facing initials fields.
- **Changing SigningPageClient.tsx or SignatureModal.tsx.** The client initials flow (`'initials'`) is already working end-to-end from Phase 10. The modal already shows "Add Initials" when `activeFieldType === 'initials'`. Do not touch these files.
- **Calling `embedPng()` inside the field loop.** Call `pdfDoc.embedPng(agentInitialsData)` once before the loop; reuse the `PDFImage` reference in the loop via `page.drawImage()`.
- **Forgetting to add `'agent-initials'` to `isClientVisibleField()`.** If omitted, the signing GET route will return `'agent-initials'` field coordinates to the client, the client's signing page will show an interactive overlay for what should be an agent-embedded field, and the POST route's `signaturesWithCoords` will throw because the client never submitted a signature for that field.
- **Fetching agentSignatureData and agentInitialsData in two separate DB queries.** Combine into a single `db.query.users.findFirst()` call that fetches both columns. One round-trip is always better than two.
- **Adding `agentInitialsData` to the session JWT.** Like agent signature, initials data should NOT be in the session — it's 2-8KB image data. Always query the `users` table separately in the prepare route using `session.user.id` as the WHERE key.
---
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| PNG embedding in PDF | Custom base64 decode + raw stream injection | `pdfDoc.embedPng(dataURL)` from @cantoo/pdf-lib | Already used for client signatures and agent signature; handles transparency, ICC profiles correctly |
| Canvas initials drawing | New custom canvas event handler | `signature_pad` (already in bundle) | DPR scaling, pressure sensitivity, mobile compatibility already handled |
| New client-initials field type | `'client-initials'` type with duplicate pipeline wiring | `'initials'` (already fully wired end-to-end) | Adding a new type would replicate all existing handling for no benefit |
| Separate DB queries for signature + initials | Two `findFirst()` calls in prepare route | Single `findFirst()` with both columns | Reduces round-trips |
| Image size validation | Custom file-size check | `dataURL.length > 50_000` string length check | Same guard used for agent signature; sufficient for typical initials PNG |
**Key insight:** Phase 11.1 adds zero new libraries and makes exactly two new things (agent-initials storage/embed + the `'agent-initials'` field type). The entire client-initials pipeline already works via `'initials'`. The planner should resist adding complexity.
---
## Common Pitfalls
### Pitfall 1: Adding client-initials as a New Field Type When 'initials' Already Works
**What goes wrong:** Planner or executor introduces a new `'client-initials'` type string, adds it to `SignatureFieldType`, and wires it through `isClientVisibleField()`, `SigningPageClient.tsx`, `preparePdf()`, and the POST signing route. This duplicates all the existing `'initials'` handling without benefit.
**Why it happens:** The phase description says "client-initials fields" and executor assumes a new field type is needed.
**How to avoid:** The existing `'initials'` field type is the client-initials type. Confirm this by reading the codebase: `SigningPageClient.tsx` at line 56 has `useState<'client-signature' | 'initials'>` for the active field type; lines 92-96 fire the modal for `ft === 'client-signature' || ft === 'initials'`; the modal title is "Add Initials" when `activeFieldType === 'initials'`. It is already fully wired.
**Warning signs:** Any new file or code block that handles `'client-initials'` as a string — that string should never appear in Phase 11.1 code.
### Pitfall 2: Forgetting isClientVisibleField() for agent-initials
**What goes wrong:** `'agent-initials'` is added to `SignatureFieldType` and `PALETTE_TOKENS` but `isClientVisibleField()` is not updated. The GET route at `/api/sign/[token]` returns `'agent-initials'` field coordinates to the client. The signing page renders an interactive purple-ish overlay for those coordinates. The client tries to click it, the modal fires. The client submits initials for that field. The POST route's `signableFields` filter only accepts `'client-signature'` and `'initials'` — so the agent-initials field is excluded from `signaturesWithCoords`. The `agentInitialsImage` (embedded at prepare time) gets a second drawing attempt at signing time, causing corruption or confusion.
**Why it happens:** `isClientVisibleField()` update is missed in Plan A implementation.
**How to avoid:** The `isClientVisibleField()` update MUST be in Plan A — before any document with `'agent-initials'` fields can reach the signing stage. Include it as part of the schema.ts modification task alongside the column addition and `SignatureFieldType` union update.
**Warning signs:** Signing page shows an interactive overlay at agent-initials field coordinates; POST route receives unexpected fieldId in the signatures array.
### Pitfall 3: preparePdf() Now Has 6 Parameters — Caller Must Be Updated Simultaneously
**What goes wrong:** `preparePdf()` gains a 6th parameter `agentInitialsData`. The prepare route still calls it with 5 args. TypeScript build error: "Expected 6 arguments, but got 5."
**Why it happens:** Same pitfall as Phase 11 Plan B — the function and its caller must be updated together.
**How to avoid:** Make `agentInitialsData` optional with a default of `null` (`agentInitialsData: string | null = null`) so the existing 5-arg call site still compiles. Then update the caller in the same plan task or the task immediately following. The default-to-null approach was used successfully for Phase 11.
**Warning signs:** TypeScript build error on `preparePdf` call site.
### Pitfall 4: PALETTE_TOKENS Color Conflict
**What goes wrong:** Agent-initials token is assigned a color already used by another field type, making the UI confusing.
**Why it happens:** Current palette: blue (client-signature), purple (initials/client-initials), green (checkbox), amber (date), slate (text), red (agent-signature). No orange is used.
**How to avoid:** Use orange (`#ea580c`) for agent-initials. It visually groups with red (agent-signature) to signal "agent-owned" while being clearly distinct. If orange is undesirable, the next option is teal (`#0d9488`).
**Warning signs:** Two tokens with identical or nearly identical colors in the FieldPlacer palette.
### Pitfall 5: agent-initials Guard and agentInitialsData Fetch Order in Prepare Route
**What goes wrong:** The prepare route fetches agentSignatureData and agentInitialsData in separate queries, or the 422 guard for agent-initials-missing fires after `preparePdf()` is called.
**Why it happens:** Incremental modifications to the route without reading the full context.
**How to avoid:** Combine both columns in a single `db.query.users.findFirst()` call. Place both guards before the `preparePdf()` call:
```
fetch agentUser (both columns in one query)
→ guard: hasAgentSigFields && !agentSignatureData → 422
→ guard: hasAgentInitialsFields && !agentInitialsData → 422
→ preparePdf(...)
```
**Warning signs:** Two separate DB round-trips; 422 error returned after PDF work has already been done.
---
## Code Examples
Verified patterns from codebase inspection (2026-03-21):
### Current isClientVisibleField() — Phase 11 State (To Be Modified)
```typescript
// Source: src/lib/db/schema.ts (Phase 11 complete, 2026-03-21)
// Currently only returns false for 'agent-signature'
// Phase 11.1 must add 'agent-initials'
export function isClientVisibleField(field: SignatureFieldData): boolean {
return getFieldType(field) !== 'agent-signature';
}
// After Phase 11.1 modification:
export function isClientVisibleField(field: SignatureFieldData): boolean {
const t = getFieldType(field);
return t !== 'agent-signature' && t !== 'agent-initials';
}
```
### Current SignatureFieldType Union — Phase 11 State (To Be Extended)
```typescript
// Source: src/lib/db/schema.ts (Phase 11 complete, 2026-03-21)
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'
```
### Current preparePdf() signature — Phase 11 State (To Be Extended)
```typescript
// Source: src/lib/pdf/prepare-document.ts (Phase 11 complete, 2026-03-21)
export async function preparePdf(
srcPath: string,
destPath: string,
textFields: Record<string, string>,
sigFields: SignatureFieldData[],
agentSignatureData: string | null = null, // Added Phase 11
// Phase 11.1 adds: agentInitialsData: string | null = null,
): Promise<void>
```
### Current preparePdf() 'initials' branch — No Changes Needed
```typescript
// Source: src/lib/pdf/prepare-document.ts lines 115-124 (Phase 11 complete, 2026-03-21)
// This is the CLIENT initials placeholder — leave completely untouched
} else if (fieldType === 'initials') {
// Purple "Initials" placeholder — transparent background, border + label only
page.drawRectangle({
x: field.x, y: field.y, width: field.width, height: field.height,
borderColor: rgb(0.49, 0.23, 0.93), borderWidth: 1.5,
});
page.drawText('Initials', {
x: field.x + 4, y: field.y + 4, size: 8, font: helvetica,
color: rgb(0.49, 0.23, 0.93),
});
}
```
### Current prepare route — DB fetch to be extended
```typescript
// Source: src/app/api/documents/[id]/prepare/route.ts (Phase 11 complete, 2026-03-21)
// Currently fetches only agentSignatureData — Phase 11.1 adds agentInitialsData
const agentUser = await db.query.users.findFirst({
where: eq(users.id, session.user.id),
columns: { agentSignatureData: true }, // Add: agentInitialsData: true
});
```
### SigningPageClient — client-initials already wired, ZERO changes needed
```typescript
// Source: src/app/sign/[token]/_components/SigningPageClient.tsx (Phase 10/11 complete, 2026-03-21)
// Line 56: 'initials' is already the active field type for client initials
const [activeFieldType, setActiveFieldType] = useState<'client-signature' | 'initials'>('client-signature');
// Lines 92-96: modal fires for ft === 'initials' — no changes needed
const ft = getFieldType(field);
if (ft !== 'client-signature' && ft !== 'initials') return;
setActiveFieldId(fieldId);
setActiveFieldType(ft as 'client-signature' | 'initials');
setModalOpen(true);
// Line 370: modal title is "Add Initials" — no changes needed
title={activeFieldType === 'initials' ? 'Add Initials' : 'Add Signature'}
```
---
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| 'initials' type unsupported in signing page | 'initials' fully wired — client can initial fields during signing session | Phase 10 | Client-initials already works; Phase 11.1 needs ZERO signing page changes |
| No agent initials support | agentInitialsData column on users; GET/PUT /api/agent/initials; AgentInitialsPanel on profile page | Phase 11.1 | Agent can draw/save initials once, reused for all documents |
| 'agent-initials' not in SignatureFieldType | 'agent-initials' in SignatureFieldType union | Phase 11.1 | FieldPlacer can place agent-initials fields; preparePdf embeds them |
| isClientVisibleField() only guards 'agent-signature' | isClientVisibleField() guards both 'agent-signature' and 'agent-initials' | Phase 11.1 | Signing page never exposes agent-initials field coordinates to client |
| preparePdf() has 5 params | preparePdf() has 6 params (agentSignatureData + agentInitialsData) | Phase 11.1 | Agent initials PNG embedded at correct field coordinates at prepare time |
**After Phase 11.1, the following remain unchanged from Phase 10/11:**
- `'initials'` handling in every file — it is client-initials and works end-to-end
- `SignatureModal.tsx` — already handles "Add Initials" title
- `SigningPageClient.tsx` — already has purple animation for initials fields
- The POST `/api/sign/[token]` route's `signableFields` filter — already includes `'initials'`
---
## Open Questions
1. **REQUIREMENTS.md traceability — phase 11.1 has no IDs**
- What we know: Phase 11.1 was inserted urgently; REQUIREMENTS.md does not have any INIT-XX IDs; STATE.md confirms "Phase 11.1 inserted after Phase 11: Agent and Client Initials (URGENT)"; Traceability table has no entry for Phase 11.1
- What's unclear: Whether user wants to formally add INIT-01 through INIT-04 to REQUIREMENTS.md or just plan without IDs
- Recommendation: Planner should add INIT-01 through INIT-04 as a Wave 0 task before writing plan files. The recommended IDs and descriptions are in the `<phase_requirements>` section at the top of this research.
2. **Should agent-initials and agent-signature be combined into a single DB query on the profile page?**
- What we know: The profile page currently queries `{ agentSignatureData: true }`. Phase 11.1 adds a second column. Drizzle's `columns` projection can fetch both in one query.
- Recommendation: Yes — fetch both in one query: `columns: { agentSignatureData: true, agentInitialsData: true }`. One DB round-trip is always better than two.
3. **Is the canvas height 80px or 140px for AgentInitialsPanel?**
- What we know: `AgentSignaturePanel` uses 140px height. Initials are shorter (2-3 letters) and don't need as much vertical space.
- Recommendation: 80px is appropriate for initials. This is Claude's discretion — the planner can adjust if desired.
4. **Should the prepare route's 422 guards (agent-sig-missing and agent-initials-missing) be combined into a single multi-check or remain separate?**
- Recommendation: Keep them separate for clearer error messages. The client-side prepare panel can display different messages depending on which error code is returned. This also makes it easy to add future agent-owned field types.
---
## Sources
### Primary (HIGH confidence)
- `/Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/db/schema.ts` (codebase inspection 2026-03-21) — `SignatureFieldType` union confirmed; `isClientVisibleField()` confirmed to return false only for `'agent-signature'`; `users` table confirmed to have `agentSignatureData` column (Phase 11 complete); `agentInitialsData` NOT yet present
- `/Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/pdf/prepare-document.ts` (codebase inspection 2026-03-21) — 5-param signature confirmed; `'initials'` purple placeholder branch confirmed at lines 115-124; `'agent-signature'` embedding branch confirmed; `agentInitialsData` NOT yet present; `PDFImage` already imported
- `/Users/ccopeland/temp/red/teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx` (codebase inspection 2026-03-21) — `'initials'` fully wired as client-interactive field type; purple animation at lines 207-215; modal title "Add Initials" at line 370; `signableFields` filter at lines 210-213 includes `'initials'`; ZERO changes needed for Phase 11.1
- `/Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/sign/[token]/route.ts` (codebase inspection 2026-03-21) — `isClientVisibleField` filter confirmed at line 90; POST handler's `signableFields` filter confirmed at lines 210-213 (includes `'initials'`); zero changes needed for client-initials
- `/Users/ccopeland/temp/red/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx` (codebase inspection 2026-03-21) — `PALETTE_TOKENS` confirmed to have 6 entries (including `agent-signature` from Phase 11); `validTypes` set confirmed; `'agent-initials'` not yet present
- `/Users/ccopeland/temp/red/teressa-copeland-homes/src/app/portal/(protected)/profile/page.tsx` (codebase inspection 2026-03-21) — Phase 11 complete profile page confirmed; queries only `agentSignatureData`; `AgentSignaturePanel` rendered; needs second section for `AgentInitialsPanel`
- `/Users/ccopeland/temp/red/teressa-copeland-homes/src/app/portal/_components/AgentSignaturePanel.tsx` (codebase inspection 2026-03-21) — confirmed working; exact template to clone for `AgentInitialsPanel`
- `/Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts` (codebase inspection 2026-03-21) — confirmed Phase 11 state: fetches `agentSignatureData`, has 422 guard, calls `preparePdf()` with 5 args; Phase 11.1 adds `agentInitialsData` column to the same query + second 422 guard + 6th arg
- `/Users/ccopeland/temp/red/teressa-copeland-homes/drizzle/` (codebase inspection 2026-03-21) — 9 migrations exist (00000008); next migration will be 0009
- `.planning/STATE.md` — Decision locked: `signableFields filter limits POST to client-signature and initials only` (Phase 10); `isClientVisibleField() returns false only for 'agent-signature'` (Phase 8-01); `agentSignatureData` column exists (Phase 11-01)
- `.planning/phases/11-agent-saved-signature-and-signing-workflow/11-RESEARCH.md` — Phase 11 patterns confirmed; `preparePdf()` parameter structure documented
### Secondary (MEDIUM confidence)
- Phase 10 STATE.md decisions — `signableFields` filter behavior for `'initials'` confirmed consistent with live codebase inspection
---
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — all libraries confirmed via package.json and source inspection; zero new dependencies; all patterns are direct copies of Phase 11 patterns
- Architecture: HIGH — specific file paths and line numbers confirmed via codebase inspection; the `'initials'` type is confirmed fully wired end-to-end; the agent-initials pattern is a direct clone of agent-signature
- Pitfalls: HIGH — all pitfalls derived from reading actual implementation code; the "don't add client-initials type" pitfall is confirmed critical by reading SigningPageClient.tsx directly
**Research date:** 2026-03-21
**Valid until:** 2026-04-20 (stable stack; all patterns established in prior phases)