- preparePdf: remove opaque fill from all field type rectangles (signature,
initials, checkbox, date, text) — underlying PDF content now shows through
- preparePdf: checkbox draws X lines only (no border rectangle); date draws
no placeholder at all; text draws nothing (position marker only)
- sign route: remove white overwrite rectangle on date stamp — date text
draws directly on existing PDF content
- FieldPlacer: suppress resize corner handles for checkbox fields; hide
"Checkbox" label (too small at 24x24pt); checkbox is fixed-size only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When signedFilePath is set, /api/documents/[id]/file now returns the
signed PDF so agent sees embedded signatures and initials in the portal
viewer. Falls back to original PDF when not yet signed.
- Add optional title prop to SignatureModal (defaults to "Add Signature"); button label derives from title
- Add activeFieldType state and setActiveFieldType in SigningPageClient
- handleFieldClick opens modal for both client-signature and initials fields
- handleSubmit and SigningProgressBar total now count both client-signature and initials
- handleJumpToNext jumps to next unsigned required field (client-signature or initials only)
- Non-interactive fields (text/checkbox/date) suppressed from field overlay rendering via early return
- Initials overlays use purple pulse animation (pulse-border-purple); signature overlays use blue
- SignatureModal receives title prop: "Add Initials" for initials, "Add Signature" for client-signature
- Add getFieldType to schema import
- Add PDFDocument, StandardFonts, rgb from @cantoo/pdf-lib for date stamping
- Add readFile, writeFile, unlink from node:fs/promises
- Hoist const now = new Date() to before step 8 (shared for date stamp + DB update)
- Step 8a: stamp signing date onto date fields in prepared PDF before embed
- Step 8b: filter signableFields to client-signature and initials only
- signaturesWithCoords now maps only signable fields (no 500 on text/checkbox/date)
- Update embedSignatureInPdf call to use dateStampedPath
- Fire-and-forget cleanup of temporary .datestamped.tmp file after embed
- handleDragEnd: determine droppedType from active.id with validTypes set guard
- handleDragEnd: checkbox fields drop at 24x24pt; all other types drop at 144x36pt
- handleDragEnd: newField now includes type property set to droppedType
- renderFields: use getFieldType() + PALETTE_TOKENS lookup to get per-field color and label
- renderFields: border, background, and text color are now driven by fieldColor
- renderFields: resize handle corners use fieldColor instead of hardcoded blue
- renderFields: span displays fieldLabel instead of hardcoded 'Signature'
- Add PALETTE_TOKENS array with 5 typed tokens (Signature/blue, Initials/purple, Checkbox/green, Date/amber, Text/slate)
- Update DraggableToken to accept id, label, color props with per-type styling
- Change isDraggingToken from boolean to string | null to track active token id
- Update onDragStart to record active.id instead of just true
- Replace single static token with PALETTE_TOKENS.map() in palette JSX
- Update DragOverlay ghost to show correct label, color, and checkbox-appropriate dimensions (24x24 vs 144x36)
- TextFillForm: add initialData prop; buildInitialRows seeds rows from
pre-seeded data so propertyAddress row renders populated on mount
- PreparePanel: pass initialData={...} to TextFillForm so the lazy
useState in PreparePanel correctly flows through to the visible UI
- TextFillForm: replace AcroForm jargon instruction with friendly copy
- TextFillForm: add Field name / Value column headers for clear layout
- TextFillForm: improve spacing (py-1.5), softer remove button (gray→red on hover)
- TypeScript: npx tsc --noEmit passes clean
- ClientModal: add defaultPropertyAddress prop, property address input field after email
- ClientProfileClient: add propertyAddress to Props type, display address under email when non-null, pass defaultPropertyAddress to edit modal
- documents/[docId]/page.tsx: extend client select to include propertyAddress, pass as clientPropertyAddress to PreparePanel
- PreparePanel: add clientPropertyAddress prop, lazy-initialize textFillData with { propertyAddress } when client has address
- Import getFieldType from @/lib/db/schema
- handleFieldClick returns early for non-client-signature fields (defense-in-depth)
- handleSubmit counts only client-signature fields for completeness check
- SigningProgressBar total reflects client-signature count only
- signatureFields added to handleFieldClick and handleSubmit dependency arrays
- Import isClientVisibleField from @/lib/db/schema
- Filter signatureFields in GET response to exclude agent-signature fields
- POST handler untouched — reads signatureFields from DB directly
- Wrap Download anchor in conditional: only rendered when docStatus !== 'Signed'
- PDF viewer still loads original via /file for in-browser display regardless of status
- PreparePanel presigned URL remains sole download path for signed PDFs
- Remove signedFilePath fallback from /file route
- Route now always serves doc.filePath (unsigned original)
- Signed PDF exclusively available via presigned /download?adt=[token]
- document detail page: import createAgentDownloadToken, generate agentDownloadUrl server-side for
signed docs (signedFilePath present), pass agentDownloadUrl and signedAt props to PreparePanel
- DocumentsTable: add signedAt to DocumentRow type, add Date Signed column header and cell
- dashboard page: add signedAt to db select so allRows includes signed date for Signed documents
- ClientProfileClient: add signedAt to local DocumentRow type (fixes type mismatch with DocumentsTable)
- clients/[id]/page: add signedAt to query select to satisfy updated DocumentRow type
- Added agentDownloadUrl and signedAt to PreparePanelProps interface (optional, nullable)
- Destructure new props in function signature
- Added Signed status branch: green panel with signed timestamp and Download Signed PDF anchor
- Kept Sent/Viewed branch: gray read-only message
- Draft status: existing prepare form unchanged
- Download is a plain <a href> anchor — no fetch/onClick; browser follows link directly
- Streams signed PDF via short-lived agent-download JWT (adt query param)
- Returns 401 for missing/expired token, 403 for ID mismatch or path traversal
- Returns 404 for unsigned documents or missing files on disk
- Path traversal guard: absPath.startsWith(UPLOADS_DIR) before readFile
- Token/route ID cross-check: documentId !== id returns 403
- new Uint8Array(fileBuffer) for Next.js 16 TypeScript strict mode compatibility
- Add POST handler to sign/[token]/route.ts with atomic one-time enforcement
- UPDATE signing_tokens SET usedAt WHERE usedAt IS NULL RETURNING — 0 rows = 409
- Log signature_submitted and pdf_hash_computed audit events
- Merge client dataURLs with server-stored field coordinates (NEVER trust client coords)
- Call embedSignatureInPdf, store pdfHash + signedFilePath in documents table
- Update document status to Signed with signedAt timestamp
- Fire-and-forget sendAgentNotificationEmail (catches errors without failing response)
- Create /sign/[token]/confirmed success page for POST redirect destination
- Create SignatureModal.tsx with signature_pad Draw tab (devicePixelRatio scaling, touch-none)
- Type tab renders name in Dancing Script cursive via offscreen canvas
- Use Saved tab conditionally shown when localStorage has saved signature
- Save for later checkbox persists drawn/typed sig to localStorage on confirm
- Update SigningPageClient.tsx: import modal, track signedFields as Map<string,string>
- Field overlay shows signature preview image after signing
- handleSubmit POSTs to /api/sign/[token] and redirects to /sign/[token]/confirmed on 200
- page.tsx: server component validates JWT + one-time-use before rendering any UI
- Three error states (expired/used/invalid) show static pages with no canvas
- SigningPageClientWrapper: dynamic import (ssr:false) for react-pdf browser requirement
- SigningPageClient: full-scroll PDF viewer with pulsing blue field overlays
- Field overlay coordinates convert PDF user-space (bottom-left) to screen (top-left)
- SigningProgressBar: sticky bottom bar with X/Y count + jump-to-next + submit button
- api/sign/[token]/pdf: token-authenticated PDF streaming route (no agent auth)
- Add send/route.ts: creates signing token, sends branded email, logs email_sent, updates status to Sent
- Auth guard returns 401 for unauthenticated requests; 422 if not prepared; 409 if already signed
- Wraps sendMail in try/catch — returns 502 without DB update if email delivery fails
- Add logAuditEvent(document_prepared) to prepare/route.ts after successful PDF preparation
- Validates JWT with verifySigningToken(); returns expired/invalid/used/pending
- Checks signingTokens.usedAt for one-time-use enforcement
- Logs link_opened + document_viewed audit events on valid pending access
- Extracts IP from x-forwarded-for/x-real-ip headers for audit trail
- Public route — no auth() import or session required
- Add readOnly prop to FieldPlacer; when true: hide palette, disable all pointer
events on field boxes, show fields at 60% opacity, suppress delete button and
all four resize handles
- PdfViewer accepts docStatus prop and derives readOnly={docStatus==='Sent'||'Signed'}
- PdfViewerWrapper forwards docStatus prop to PdfViewer
- page.tsx passes docStatus={doc.status} to PdfViewerWrapper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ResizeCorner type ('se' | 'sw' | 'ne' | 'nw')
- handleResizeStart now accepts a corner argument, stored in DraggingState
- Screen-space math: compute start rect (top/left/right/bottom px), apply delta
per corner, enforce 40px/20px minimums, then convert back to PDF units on pointerup
- renderFields renders four 10x10 blue square handles at each corner with the
correct CSS resize cursor (nw-resize, ne-resize, sw-resize, se-resize)
- Opposite corner is always the anchor; only the two edges adjacent to the dragged
corner move
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Native pointer events on field body for move drag (no dnd-kit conflict)
- 10x10 resize handle in bottom-right corner with se-resize cursor
- Event delegation via DroppableZone onPointerMove/onPointerUp
- DOM mutation during drag for smooth performance; commit to state on pointerUp
- data-no-move attribute prevents resize handle and delete button from triggering move
- draggingRef tracks in-progress drag; activeDragFieldId drives grabbing cursor + box-shadow
- Minimum field size enforced: 40x20 PDF units
- Remove isLocked variable and read-only div for assigned client
- Add primaryEmail state pre-filled with assigned client's email, user-editable
- Show client name as helper text below the input for reference
- Update buildEmailAddresses() to use primaryEmail when assignedClientId is set
- Update button disabled logic to gate on primaryEmail when assigned client exists
- Replace cursor+delta calculation with active.rect.current.translated (ghost bounding rect)
- Measure coordinates relative to the inner <canvas> element, not the DroppableZone wrapper
- Add canvasOffset state + useLayoutEffect to offset field overlays by canvas position within wrapper
- Inline PDF coordinate math in handleDragEnd; clamp uses field pixel dimensions
- PreparePanel now receives separate assignedClientId (nullable) and
defaultClientId props so it can distinguish an explicitly locked
client from just a default
- When document already has assignedClientId: show locked read-only
display; user cannot change the primary recipient
- When document has no assignedClientId: default to document owner in
dropdown but allow changing; option to clear and enter email manually
- Added textarea for additional/CC email addresses (comma or newline
separated) that is always visible for either mode
- POST /api/documents/[id]/prepare now accepts and stores emailAddresses
array alongside assignedClientId
- Added email_addresses jsonb column to documents table via migration
0004_military_maximus.sql
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use pageInfo.width/height (authoritative rendered canvas size from
react-pdf) instead of containerRect.width/height for all coordinate
math; containerRect dimensions could differ if the wrapper div has
extra decoration not matching the canvas
- Track containerSize in state (updated via useLayoutEffect when
pageInfo changes) so renderFields() uses stable values from state
instead of calling getBoundingClientRect() during render
- Refactor screenToPdfCoords/pdfToScreenCoords to take renderedW/H
as explicit params instead of a DOMRect, making the contract clearer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fetch allClients in parallel with doc via Promise.all
- Import PreparePanel and render in 1/3 right column of 2/3 + 1/3 grid
- currentClientId defaults to doc.assignedClientId ?? doc.clientId
- asc import added from drizzle-orm for ordered client list
- npm run build is clean with no TypeScript errors
- TextFillForm: key-value pair builder (up to 10 rows, individually removable)
- PreparePanel: client selector + text fill form + Prepare and Send button
- PreparePanel calls POST /api/documents/[id]/prepare and calls router.refresh() on success
- PreparePanel shows read-only message for non-Draft documents
- Rule 1 fix: PdfViewer page.scale -> scale (PageCallback has no .scale property; use state var)
- Add pageInfo state (PageInfo | null) to track rendered PDF page dimensions
- Wire onLoadSuccess callback on Page to capture originalWidth/Height using Math.max mediaBox pattern
- Import and wrap Document/Page tree inside FieldPlacer component
- Pass docId, pageInfo, currentPage to FieldPlacer for coordinate conversion
- Only scale prop used on Page (not both width+scale — avoids double scaling)
- All existing controls (Prev, Next, Zoom In/Out, Download) preserved unchanged
- Install @dnd-kit/core@^6.3.1 and @dnd-kit/utilities@^3.2.2
- Create FieldPlacer.tsx with DndContext, draggable palette token, droppable PDF zone
- Implements Y-flip coordinate conversion (screenToPdfCoords / pdfToScreenCoords)
- Fetches existing fields from GET /api/documents/[id]/fields on mount
- Persists fields via PUT /api/documents/[id]/fields on every add/remove
- Renders placed fields as absolute-positioned blue-bordered overlays with remove button
- Default field size: 144x36 PDF points (2in x 0.5in at 72 DPI)
- Installed @cantoo/pdf-lib for server-side PDF mutation
- Created src/lib/pdf/prepare-document.ts with preparePdf function using atomic tmp->rename write pattern
- form.flatten() called before drawing signature rectangles
- Created GET/PUT /api/documents/[id]/fields routes for signature field storage
- Created POST /api/documents/[id]/prepare route that calls preparePdf and transitions status to Sent
- Fixed pre-existing null check error in scripts/debug-inspect2.ts (Rule 3: blocking build)
- Build compiles successfully with 2 new API routes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>