Commit Graph

174 Commits

Author SHA1 Message Date
Chandler Copeland
9117dc4c02 initial install 2026-04-08 12:54:58 -06:00
Chandler Copeland
eec0bd91c9 feat(20-02): add template hint quick-fill chip to PreparePanel
- DocumentPageClient fetches /api/documents/:docId/fields on mount and aiPlacementKey change
- Derives selectedFieldHint from selected field's hint property
- Passes selectedFieldHint prop to PreparePanel
- PreparePanel renders Template Hint chip in Quick Fill section when hint exists
2026-04-06 14:54:09 -06:00
Chandler Copeland
2947fa558c feat(20-01): add My Templates tab to AddDocumentModal
- Add DocumentTemplateRow type and activeTab/docTemplates/selectedDocTemplate state
- Lazy-fetch /api/templates on first My Templates tab click
- handleSwitchToTemplates lazy-loads on first click only
- handleSelectDocTemplate clears selectedTemplate and customFile (mutual exclusivity)
- handleSelectTemplate now also clears selectedDocTemplate
- handleSubmit: new branch at top sends documentTemplateId to POST /api/documents
- Guard and disabled condition updated to include selectedDocTemplate
- Tab bar renders with underline-style active indicator matching project Tailwind patterns
- Existing Forms Library content and custom upload section wrapped in activeTab === 'forms' conditional
2026-04-06 14:51:23 -06:00
Chandler Copeland
bdf0cb02ff feat(20-01): extend POST /api/documents with documentTemplateId branch
- Parse documentTemplateId from JSON body alongside formTemplateId
- Fetch document template with formTemplate relation for PDF filename
- Copy signatureFields with fresh crypto.randomUUID per field (snapshot independence)
- Map template signer role labels to client email + contacts array
- Return early with inserted doc; existing form-library and upload paths unchanged
2026-04-06 14:50:24 -06:00
Chandler Copeland
db7a76defc fix(portal): raise ConfirmDialog z-index to 1000 to clear FieldPlacer overlays and nav stacking context 2026-04-06 13:55:14 -06:00
Chandler Copeland
10ea48d5ba feat(19-02): template editor page, TemplatePageClient, and TemplatePanel
- Server component at /portal/templates/[id] queries documentTemplates with formTemplate relation
- notFound() for archived/missing templates
- TemplatePageClient: state owner mirroring DocumentPageClient pattern
  - deriveRolesFromFields initializes Buyer/Seller defaults or extracts from existing fields
  - handlePersist merges textFillData hints into f.hint for type='text' fields
  - handleAiAutoPlace POSTs to /api/templates/[id]/ai-prepare and increments aiPlacementKey
  - Role rename/remove re-fetches fields and PATCHes with updated signerEmail values
- TemplatePanel: 280px right panel with inline styles matching portal design system
  - Signers/Roles section with color dots, click-to-rename, remove with ConfirmDialog
  - Add role input with preset chips (Buyer, Co-Buyer, Seller, Co-Seller)
  - AI Auto-place button (navy) with spinner/loading state and error display
  - Save Template button (gold) with success 'Saved' text fading after 3s
2026-04-06 13:15:23 -06:00
Chandler Copeland
4ca0769cf2 feat(19-02): templates list page with create-template modal
- Server component at /portal/templates queries documentTemplates with LEFT JOIN to formTemplates
- TemplatesListClient renders list rows with name, form name, field count, last updated
- Create modal POSTs to /api/templates and navigates to /portal/templates/[id] on success
- Empty state with CTA when no templates exist
- Gold #C9A84C accent, navy #1B2B4B headings, inline styles matching portal patterns
2026-04-06 13:13:29 -06:00
Chandler Copeland
275565c933 feat(19-01): create template API routes — file, fields, ai-prepare
- GET /api/templates/[id]/file: streams form PDF from seeds/forms/ with path traversal guard
- GET /api/templates/[id]/fields: returns signatureFields array (or []) from documentTemplates
- POST /api/templates/[id]/ai-prepare: runs AI field placement with null client context, writes to DB
- All routes: auth guard + isNull(archivedAt) soft-delete filter consistent with Phase 18
2026-04-06 13:10:02 -06:00
Chandler Copeland
57efd91fa2 feat(19-01): add onPersist, fieldsUrl, fileUrl props and Templates nav link
- FieldPlacer: add onPersist and fieldsUrl optional props (backwards-compatible)
- FieldPlacer: 4 persistFields call sites now conditionally use onPersist when provided
- FieldPlacer: loadFields useEffect uses fieldsUrl ?? default documents endpoint
- PdfViewer: add onPersist, fieldsUrl, fileUrl props; pass to FieldPlacer; fileUrl ?? default for PDF source
- PdfViewerWrapper: add and pass through onPersist, fieldsUrl, fileUrl to PdfViewer
- PortalNav: insert Templates link between Clients and Profile
2026-04-06 13:09:19 -06:00
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