Files
red/.planning/phases/11.1-agent-and-client-initials/11.1-01-PLAN.md
Chandler Copeland ae17e017d3 docs(11.1): create phase plan — agent and client initials
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>
2026-03-21 14:55:00 -06:00

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
teressa-copeland-homes/src/lib/db/schema.ts
teressa-copeland-homes/drizzle/0009_agent_initials.sql
teressa-copeland-homes/src/app/api/agent/initials/route.ts
teressa-copeland-homes/src/app/portal/(protected)/profile/page.tsx
teressa-copeland-homes/src/app/portal/_components/AgentInitialsPanel.tsx
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx
true
INIT-01
INIT-02
INIT-03
INIT-04
truths artifacts key_links
Agent can navigate to /portal/profile and see both the Agent Signature section and a new Agent Initials section below it
Agent can draw initials on the canvas in the Agent Initials section and save them — a thumbnail appears after saving
Agent can click 'Update Initials' to redraw and replace their saved initials
FieldPlacer palette shows an orange 'Agent Initials' token (7th token, distinct from the purple 'Initials' client token)
Placing an agent-initials field saves it in signatureFields with type 'agent-initials'
The existing purple 'Initials' token still appears in FieldPlacer and continues to work — no regression
'agent-initials' is NOT returned to the client signing page (isClientVisibleField returns false for it)
path provides contains
teressa-copeland-homes/src/lib/db/schema.ts agentInitialsData TEXT column on users table; 'agent-initials' added to SignatureFieldType union; isClientVisibleField() guards both agent-signature and agent-initials agentInitialsData
path provides exports
teressa-copeland-homes/src/app/api/agent/initials/route.ts GET/PUT endpoints for reading and writing agent initials
GET
PUT
path provides
teressa-copeland-homes/src/app/portal/_components/AgentInitialsPanel.tsx Client component with signature_pad canvas (80px height), save/update/thumbnail flow
path provides
teressa-copeland-homes/src/app/portal/(protected)/profile/page.tsx Server component fetching both agentSignatureData and agentInitialsData; renders AgentSignaturePanel + AgentInitialsPanel
from to via pattern
teressa-copeland-homes/src/app/portal/(protected)/profile/page.tsx teressa-copeland-homes/src/app/portal/_components/AgentInitialsPanel.tsx initialData prop (string | null) initialData.*agentInitialsData
from to via pattern
teressa-copeland-homes/src/app/portal/_components/AgentInitialsPanel.tsx /api/agent/initials fetch PUT with { dataURL } fetch.*api/agent/initials
from to via pattern
teressa-copeland-homes/src/lib/db/schema.ts isClientVisibleField returns false for both agent-signature and agent-initials agent-initials.*isClientVisibleField
Establish the agent initials storage and UI layer: DB migration adds `agentInitialsData TEXT` to users, GET/PUT API routes read/write the base64 PNG dataURL, a new `AgentInitialsPanel` component (clone of `AgentSignaturePanel` with 80px canvas) is added to the existing `/portal/profile` page below the signature section, and the FieldPlacer palette gets an orange "Agent Initials" token. The `SignatureFieldType` union gains `'agent-initials'` and `isClientVisibleField()` is updated to exclude it from client-visible fields.

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.md

From 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 });
Task 1: DB migration, API routes, schema type updates for agent initials storage teressa-copeland-homes/src/lib/db/schema.ts teressa-copeland-homes/drizzle/0009_[auto-named].sql teressa-copeland-homes/src/app/api/agent/initials/route.ts Four changes in sequence:

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 });
}
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 TypeScript compiles clean; migration SQL file exists at drizzle/0009_*.sql; schema.ts has agentInitialsData column, 'agent-initials' in SignatureFieldType, and updated isClientVisibleField(); GET /api/agent/initials returns 401 without session and { agentInitialsData: null } with valid session. Task 2: AgentInitialsPanel component, profile page section update, FieldPlacer token teressa-copeland-homes/src/app/portal/_components/AgentInitialsPanel.tsx teressa-copeland-homes/src/app/portal/(protected)/profile/page.tsx teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx Three files to create/modify:

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 (vs max-h-20 for 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 &quot;Agent Initials&quot; 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.

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_initials_data';" returns one row
  4. GET /api/agent/initials returns 401 without session; returns { agentInitialsData: null } with valid session
  5. /portal/profile renders both sections — Agent Signature (existing, unchanged) and Agent Initials (new) — with their respective canvases
  6. Agent can draw on the initials canvas and click "Save Initials" — PUT /api/agent/initials returns 200, thumbnail appears
  7. Agent can click "Update Initials" — canvas reappears for redraw
  8. FieldPlacer shows "Agent Initials" as a draggable orange token (7th entry); purple "Initials" client token still present
  9. Placing an "Agent Initials" field on a document page saves it with type: 'agent-initials' in signatureFields
  10. 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>
After completion, create `.planning/phases/11.1-agent-and-client-initials/11.1-01-SUMMARY.md`