Commit Graph

165 Commits

Author SHA1 Message Date
Chandler Copeland
12a74fcf4a feat(18-02): PATCH and DELETE handlers at /api/templates/[id]
- PATCH accepts name and signatureFields for rename and field save per D-10
- PATCH sets updatedAt: new Date() explicitly per D-05
- PATCH returns 404 for archived or missing templates
- DELETE soft-deletes by setting archivedAt: new Date() per D-07 (no hard delete)
- DELETE sets updatedAt: new Date() per D-05, returns 204 No Content
- Both handlers auth-gated with auth() per D-11
2026-04-06 12:17:39 -06:00
Chandler Copeland
28c7773b40 feat(18-02): GET and POST handlers at /api/templates
- GET lists active templates (archivedAt IS NULL) with LEFT JOIN for formName
- GET computes fieldCount server-side from signatureFields.length per D-12
- POST validates name + formTemplateId, verifies FK exists, returns 201
- Both handlers auth-gated with auth() per D-11
2026-04-06 12:17:18 -06:00
Chandler Copeland
c33c4ec075 chore(18-01): generate migration 0012 for document_templates table
- CREATE TABLE document_templates with 7 columns
- FK constraint form_template_id -> form_templates(id) ON DELETE no action
- No alterations to existing tables
2026-04-06 12:15:12 -06:00
Chandler Copeland
9e677f9505 feat(18-01): add documentTemplates table and relation to schema.ts
- Add document_templates pgTable with 7 columns: id, name, formTemplateId, signatureFields, archivedAt, createdAt, updatedAt
- formTemplateId FK references formTemplates.id with no onDelete cascade
- signatureFields typed as SignatureFieldData[] via jsonb, nullable (template starts empty)
- archivedAt nullable timestamp for soft-delete (NULL = active)
- Add documentTemplatesRelations joining to formTemplates
2026-04-06 12:15:01 -06:00
Chandler Copeland
f83dba5e69 docs: complete project research 2026-04-06 11:54:40 -06:00
Chandler Copeland
6013dfe89f feat: client-text and client-checkbox field types — signer fills text/checks boxes on signing page 2026-04-06 11:35:30 -06:00
Chandler Copeland
116fa2bdfb chore: add start.sh and seed-forms.sh deployment scripts 2026-04-03 18:36:51 -06:00
Chandler Copeland
e2bda51d91 feat: per-signer status panel with Resend button on sent documents, fix sign page field filter 2026-04-03 18:21:07 -06:00
Chandler Copeland
bc0495dea9 fix(signing): filter fields by signer on sign page — was passing all fields unfiltered 2026-04-03 18:14:46 -06:00
Chandler Copeland
4fe7913d7e fix(email): switch to SMTP port 587 STARTTLS — port 465 SSL blocked in Docker 2026-04-03 18:06:33 -06:00
Chandler Copeland
2d2a43a3c9 fix(docker): polyfill DOMMatrix/ImageData/Path2D for pdfjs-dist in linux/amd64 container via NODE_OPTIONS --require 2026-04-03 18:02:39 -06:00
Chandler Copeland
f15e538f5c fix(ui): uniform card heights, tinted bottom section on client cards 2026-04-03 17:51:44 -06:00
Chandler Copeland
ac42fa1fc7 fix: show additional contacts on client profile page 2026-04-03 17:49:13 -06:00
Chandler Copeland
47bc0f4cfa fix: remove redundant recipients field — signers system handles all email dispatch 2026-04-03 17:46:30 -06:00
Chandler Copeland
a00d52216e feat(clients): show contacts on client cards, auto-persist seeded signers to DB 2026-04-03 17:41:51 -06:00
Chandler Copeland
81ce0b9ab0 feat(clients): multi-contact support — co-buyers, auto-seed document signers from client contacts 2026-04-03 17:37:39 -06:00
Chandler Copeland
4f25a8c124 fix(signers): show invalid email error, persist signers immediately via PATCH, add PATCH /api/documents/[id] 2026-04-03 17:29:27 -06:00
Chandler Copeland
07cfaf0511 fix(docker): add credentials fields to auth provider, postgres service, expose port 5433, AUTH_URL/AUTH_TRUST_HOST in env example 2026-04-03 17:21:01 -06:00
Chandler Copeland
72c23f8bba feat(17-02): DEPLOYMENT.md with env setup, migration from host, docker compose up, health check
- Step-by-step guide: configure .env.production, run drizzle-kit migrate from host
- docker compose up -d --build to build and start
- curl /api/health verification step
- Update workflow: git pull, optional migration, compose up --build
2026-04-03 16:56:43 -06:00
Chandler Copeland
a107970269 feat(17-02): docker-compose.yml with env_file secrets, DNS fix, named volume; update .gitignore
- env_file: .env.production — secrets injected at runtime, not baked into image
- dns array [8.8.8.8, 1.1.1.1] + NODE_OPTIONS=--dns-result-order=ipv4first for SMTP EAI_AGAIN fix
- Named volume uploads:/app/uploads persists PDFs across container restarts
- restart: unless-stopped, port 3000:3000
- .gitignore: added /uploads/ entry for production Docker volume path
2026-04-03 16:56:25 -06:00
Chandler Copeland
e83ced580d feat(17-02): Dockerfile three-stage build, .dockerignore, .env.production.example
- Three-stage node:20-slim Dockerfile with --platform=linux/amd64 on all 3 FROM lines
- Non-root nextjs:nodejs user, seeds/ copied for form library, uploads/ dir pre-created
- HEALTHCHECK via wget pointing to /api/health, CMD node server.js
- .dockerignore excludes node_modules, .next, .git, .env*, uploads/, *.md
- .env.production.example with exactly 11 required vars (template, no real secrets, force-added past .env* glob)
2026-04-03 16:56:09 -06:00
Chandler Copeland
57326d77e7 feat(17-01): add GET /api/health endpoint with DB connectivity check
- Runs SELECT 1 via Drizzle db client
- Returns { ok: true, db: 'connected' } with status 200 on success
- Returns { ok: false, error: message } with status 503 on DB failure
- No auth check — intentionally public for Docker HEALTHCHECK
2026-04-03 16:53:42 -06:00
Chandler Copeland
fa7d6a9636 feat(17-01): enable standalone output, limit DB pool to 5, remove @vercel/blob
- Add output: 'standalone' to next.config.ts for Docker three-stage build
- Change postgres(url) to postgres(url, { max: 5 }) to avoid Neon free tier exhaustion
- Remove dead @vercel/blob dependency (imported nowhere in codebase)
2026-04-03 16:53:18 -06:00
Chandler Copeland
29557f06e0 feat(16-04): render N/M signed badge in DocumentsTable Status column
- Extend DocumentRow type with optional signedCount, totalSigners, hasMultipleSigners
- Render badge after StatusBadge only for multi-signer Sent documents (D-11, D-12, D-13)
- Badge: inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700 ml-1.5
- Shows '{N}/{M} signed' text from server-computed token counts
- No badge for single-signer or fully-signed documents
2026-04-03 16:31:04 -06:00
Chandler Copeland
ad4e27af42 feat(16-04): enrich dashboard rows with signedCount, totalSigners, hasMultipleSigners
- Import signingTokens and sql from drizzle-orm
- Add documents.signers to main select
- Fetch token counts per document in single grouped query (avoids N+1)
- Build tokenMap for O(1) lookup per row
- Produce enrichedRows with signedCount, totalSigners, hasMultipleSigners fields
- Pass enrichedRows to DocumentsTable instead of filteredRows
2026-04-03 16:29:17 -06:00
Chandler Copeland
1c8551c30d feat(16-02): PreparePanel signer list UI, send-block validation, persist signers to DB 2026-04-03 16:26:53 -06:00
Chandler Copeland
d768fc6aae feat(16-03): active signer selector, per-signer field coloring, unassigned field red highlight 2026-04-03 16:25:39 -06:00
Chandler Copeland
9da2cc67fd feat(16-01): thread signers and unassignedFieldIds through PdfViewer chain to FieldPlacer
- PdfViewerWrapper accepts and passes signers/unassignedFieldIds to PdfViewer
- PdfViewer accepts and passes both props to FieldPlacer
- FieldPlacer adds signers/unassignedFieldIds to FieldPlacerProps (optional, defaulted to []/ new Set())
- No rendering changes — prop tunnel only for Wave 2 consumers

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 16:21:20 -06:00
Chandler Copeland
ac1f1d6cec feat(16-01): thread signers state through DocumentPageClient
- Server page passes doc.signers as initialSigners to DocumentPageClient
- DocumentPageClient adds signers + unassignedFieldIds state (initialized from server)
- Props threaded to PdfViewerWrapper (signers, unassignedFieldIds) and PreparePanel (signers, onSignersChange, unassignedFieldIds, onUnassignedFieldIdsChange)
- PreparePanel interface extended to accept new optional multi-signer props

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 16:21:13 -06:00
Chandler Copeland
1749e10e9c feat(15-03): signer-aware POST handler with accumulate PDF and atomic completion
- Step 3.5: fetch signerEmail from tokenRow after atomic claim
- Step 7: accumulate pattern — read from signedFilePath or preparedFilePath as working PDF
- Step 7: write to JTI-keyed _partial_ path to prevent concurrent signer collisions
- Step 8a: date stamps scoped to this signer's date fields (D-09)
- Step 8b: signable fields scoped to this signer's fields (Pitfall 4)
- Step 9.5: JTI-keyed datestamped temp file to prevent collision
- Step 10.5: update signedFilePath to this signer's partial after each signing
- Step 11: remaining-token count check before completion attempt
- Step 11: completionTriggeredAt atomic guard (UPDATE WHERE IS NULL RETURNING)
- Step 12: status='Signed' only in completion winner block (fixes first-signer-wins bug)
- Step 13: agent notification + signer completion emails only at completion
- Legacy null-signerEmail tokens fall through all signer filters unchanged
2026-04-03 15:47:37 -06:00
Chandler Copeland
7a04a4f617 feat(15-02): rewrite send route with multi-signer token loop and legacy fallback
- Loop over doc.signers when populated: one createSigningToken per signer with signerEmail
- Dispatch all signing emails in parallel via Promise.all
- Preserve legacy single-signer path unchanged when signers is null/empty
- Replace NEXT_PUBLIC_BASE_URL with APP_BASE_URL for signing URLs
- Add audit event with metadata.signerEmail for each signer in multi-signer path
- Import DocumentSigner from schema for type casting
2026-04-03 15:46:44 -06:00
Chandler Copeland
0f97c4233f feat(15-03): signer-aware GET field filter and updated imports
- Filter signatureFields by tokenRow.signerEmail for multi-signer tokens (D-04)
- Legacy null-signerEmail tokens return all isClientVisibleField fields (D-05)
- Added imports: createSignerDownloadToken, sendSignerCompletionEmail, DocumentSigner, sql
2026-04-03 15:45:59 -06:00
Chandler Copeland
14efa1dce4 feat(15-01): create public signer download route GET /api/sign/download/[token]
- Public route — no auth session required
- Validates signer-download JWT via verifySignerDownloadToken
- Guards: expired/invalid token (401), incomplete doc (404), path traversal (403)
- Serves signed PDF with Content-Disposition attachment header
2026-04-03 15:43:40 -06:00
Chandler Copeland
e1cdfe9b7b feat(15-01): add sendSignerCompletionEmail to signing-mailer
- Sends plain-text email with signed document download link to signer
- Follows established createTransporter() + sendMail() pattern
- Subject: 'Signed copy ready: {documentName}', 72h expiry in body text
2026-04-03 15:43:17 -06:00
Chandler Copeland
70c48cc377 feat(15-01): extend createSigningToken with signerEmail, add signer-download token pair
- createSigningToken now accepts optional signerEmail param and persists to DB
- Added createSignerDownloadToken (72h TTL, purpose: signer-download)
- Added verifySignerDownloadToken with purpose claim validation
2026-04-03 15:43:00 -06:00
Chandler Copeland
363949124c feat(14-01): generate and apply Drizzle migration 0010 for multi-signer columns
- Migration 0010_sharp_archangel.sql adds signer_email to signing_tokens
- Migration adds signers JSONB column to documents
- Migration adds completion_triggered_at TIMESTAMP to documents
- Additive-only: no DROP columns, no ALTER TYPE, no backfills
- Applied successfully to local Neon/Postgres instance
2026-04-03 15:16:15 -06:00
Chandler Copeland
c658f13ea0 feat(14-01): add multi-signer types and columns to schema.ts
- Add signerEmail?: string to SignatureFieldData interface
- Add getSignerEmail() helper function with fallback pattern
- Add DocumentSigner interface { email, color }
- Add documents.signers JSONB column typed as DocumentSigner[]
- Add documents.completionTriggeredAt nullable TIMESTAMP column
- Add signingTokens.signerEmail nullable TEXT column
2026-04-03 15:15:32 -06:00
Chandler Copeland
6265a64a50 chore(13-04): remove debug console.log from classifyFieldsWithAI
- Remove 3 console.log statements that printed blank count, all blank descriptions, and AI classifications
- These were development debug statements; not appropriate for production code
- Tests pass (prepare-document.test.ts: 10/10), TypeScript clean
2026-04-03 14:31:15 -06:00
Chandler Copeland
c80133ea58 fix(13): stamp page numbers on rendered images, fix signature block pattern in prompt
- Red PAGE N label stamped on each image so GPT-4o correctly attributes fields to pages
- Prompt: add 'blank line above label' signature block pattern (common in real estate docs)
- Prompt: explicit rule — place field on blank underline, not on the (Label) text below it

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 17:52:31 -06:00
Chandler Copeland
8ac5acb486 fix(13): reduce field heights, nudge y-offset, tighten height prompt guidance
- Max heights reduced: text/date 16pt, initials 18pt, signature 26pt
- 0.5% y nudge pushes fields onto the underline instead of floating above it
- Prompt specifies 1.2% height for text/date, 1.8% for signatures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 17:49:11 -06:00
Chandler Copeland
48788dea23 fix(13): use AI-estimated field sizes with type bounds, stricter no-inline-text rule
- Replace fixed 144x36 with AI widthPct/heightPct clamped to per-type min/max
  (signatures 100-250x20-40pt, initials 36-80x16-28pt, date 60-130x14-24pt, text 60-280x14-24pt)
- Prompt: explicit 'no inline body text' rule — if text is part of a sentence, skip it
- Prompt: widthPct should match visual underline width, heightPct kept thin (~2-2.5%)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 17:46:04 -06:00
Chandler Copeland
461abb0dc4 fix(13): mark @napi-rs/canvas as serverExternalPackages to prevent Turbopack bundling native .node binding
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 17:43:16 -06:00
Chandler Copeland
e7bf5abb9f fix(13): switch to GPT-4o vision — render PDF pages as images for accurate field placement
- extractPdfText now renders each page to JPEG via @napi-rs/canvas + pdfjs-dist (108dpi)
- field-placement.ts sends rendered page images to GPT-4o with vision (detail: high)
- AI can now visually identify underlines, signature blocks, date fields, initials boxes
- System prompt focuses on visual cues (blank lines, boxes) not text pattern matching
- Handles multi-field lines: separate fields for signature blank and date blank on same line

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 17:40:47 -06:00
Chandler Copeland
b5216a8542 fix(13): extract text with line positions for accurate AI field placement
- extractPdfText now returns TextLine[] with yPct/xPct per line instead of flat text blob
- AI can now see spatial layout (where blank lines/underscores actually are vs body text)
- Rewrote system prompt: explicit rules about blank lines/underscores/signature blocks,
  place ALL blanks even without prefill value, match field type to label pattern

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 17:35:02 -06:00
Chandler Copeland
c67d56dc48 fix(13-01): upgrade to gpt-4o, remove checkboxes, clamp AI coords to page bounds
- gpt-4o-mini replaced with gpt-4o for better placement accuracy
- checkbox removed from schema enum and filtered in loop (positions are input-dependent)
- y coordinate clamped to [0, pageHeight - fieldHeight] to prevent fields rendering
  outside the PDF canvas when AI returns yPct near 100%

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 17:29:49 -06:00
Chandler Copeland
72f7d20bac fix(13-03): dynamic import PreviewModal to prevent SSR DOMMatrix crash
react-pdf calls new DOMMatrix() at module level — static import causes
500 on document page during SSR. Same pattern as PdfViewer/PdfViewerWrapper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 17:25:14 -06:00
Chandler Copeland
3f4ca5a8e5 fix(13-01): use file:// worker path for pdfjs-dist 5.x fake-worker in Node.js
Empty string workerSrc is falsy — PDFWorker.workerSrc getter throws before
_setupFakeWorkerGlobal can dynamically import the worker file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 17:19:29 -06:00
Chandler Copeland
bfdaee14ed feat(13-03): add AI Auto-place button to PreparePanel and wire DocumentPageClient handler
- Add onAiAutoPlace prop and handleAiAutoPlaceClick with aiLoading state to PreparePanel
- Add violet AI Auto-place button above Preview button (Draft documents only)
- Add aiPlacementKey state and handleAiAutoPlace callback in DocumentPageClient
- handleAiAutoPlace: POST /api/documents/[id]/ai-prepare, merges textFillData, increments aiPlacementKey, resets previewToken
- Pass aiPlacementKey to PdfViewerWrapper and onAiAutoPlace to PreparePanel
2026-03-21 17:08:10 -06:00
Chandler Copeland
3e11eef1c4 feat(13-03): add aiPlacementKey prop to FieldPlacer and thread through PdfViewerWrapper/PdfViewer
- Add aiPlacementKey?: number to FieldPlacerProps interface
- Add aiPlacementKey to loadFields useEffect dependency array for re-fetch on AI placement
- Thread aiPlacementKey through PdfViewer and PdfViewerWrapper prop chains
2026-03-21 17:07:01 -06:00
Chandler Copeland
e91f29e555 feat(13-02): implement POST /api/documents/[id]/ai-prepare route
- Auth guard (401), OPENAI_API_KEY guard (503), not-found (404)
- Draft-only guard (403), path traversal guard (403)
- Loads document with client relation via Drizzle with: { client: true }
- Calls extractPdfText then classifyFieldsWithAI in try/catch (500 on error)
- Writes SignatureFieldData[] to DB signatureFields; status stays Draft
- Returns { fields, textFillData } keyed by field UUID
2026-03-21 17:04:00 -06:00