Compare commits

...

10 Commits

Author SHA1 Message Date
Chandler Copeland
9117dc4c02 initial install 2026-04-08 12:54:58 -06:00
Chandler Copeland
71cef4d7b6 docs(phase-20): complete phase execution 2026-04-06 15:15:43 -06:00
Chandler Copeland
7621007a60 docs(20-02): complete plan — human verified all 12 steps, v1.3 milestone shipped
- 20-02-SUMMARY.md updated with full human verification results (TMPL-10 through TMPL-16)
- STATE.md: phase 20 marked complete, progress 100% (65/65 plans)
- ROADMAP.md: v1.2 and v1.3 milestones marked shipped
2026-04-06 15:02:24 -06:00
Chandler Copeland
81a39c8e59 docs(20-02): complete template hint quick-fill plan
- Task 1 committed: DocumentPageClient fields fetch + selectedFieldHint derivation + PreparePanel hint chip
- Awaiting human-verify checkpoint (Task 2)
2026-04-06 14:55:19 -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
78d579f965 docs(20-01): complete apply-template plan — My Templates tab + template apply branch
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:52:31 -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
af5beaf5cb docs(phase-20): create phase plan — apply template and portal nav 2026-04-06 14:10:38 -06:00
Chandler Copeland
9081342e1b docs(phase-20): add validation strategy 2026-04-06 14:06:37 -06:00
193 changed files with 3944 additions and 608 deletions

View File

@@ -0,0 +1,42 @@
---
name: frontend-design
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
license: Complete terms in LICENSE.txt
---
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
## Design Thinking
Before coding, understand the context and commit to a BOLD aesthetic direction:
- **Purpose**: What problem does this interface solve? Who uses it?
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
- **Constraints**: Technical requirements (framework, performance, accessibility).
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
- Production-grade and functional
- Visually striking and memorable
- Cohesive with a clear aesthetic point-of-view
- Meticulously refined in every detail
## Frontend Aesthetics Guidelines
Focus on:
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.

Submodule .claude/worktrees/agent-a42b65af added at 410fc10e9b

Submodule .claude/worktrees/agent-a4e62c7a added at 410fc10e9b

Submodule .claude/worktrees/agent-a66cf8e5 added at 410fc10e9b

Submodule .claude/worktrees/agent-afd3aaec added at 410fc10e9b

View File

@@ -143,11 +143,11 @@
### Apply Template to Document ### Apply Template to Document
- [ ] **TMPL-10**: When adding a document to a client, agent can choose "Start from template" and pick a saved template - [x] **TMPL-10**: When adding a document to a client, agent can choose "Start from template" and pick a saved template
- [ ] **TMPL-11**: Applying a template creates a document with all template fields pre-loaded (fresh field IDs, positions, types, and role assignments copied) - [x] **TMPL-11**: Applying a template creates a document with all template fields pre-loaded (fresh field IDs, positions, types, and role assignments copied)
- [ ] **TMPL-12**: Template signer roles are automatically mapped to the client's contacts in order (primary contact → first role, co-buyer → second role); agent can override the mapping before sending - [x] **TMPL-12**: Template signer roles are automatically mapped to the client's contacts in order (primary contact → first role, co-buyer → second role); agent can override the mapping before sending
- [ ] **TMPL-13**: Text hints from the template appear as quick-fill suggestions in the document's PreparePanel - [x] **TMPL-13**: Text hints from the template appear as quick-fill suggestions in the document's PreparePanel
- [ ] **TMPL-14**: Editing a template after documents have been created from it does NOT retroactively change those documents — each document is an independent snapshot - [x] **TMPL-14**: Editing a template after documents have been created from it does NOT retroactively change those documents — each document is an independent snapshot
### Portal Navigation ### Portal Navigation
@@ -279,11 +279,11 @@ Which phases cover which requirements. Updated during roadmap creation.
| TMPL-07 | Phase 19 | Complete | | TMPL-07 | Phase 19 | Complete |
| TMPL-08 | Phase 19 | Complete | | TMPL-08 | Phase 19 | Complete |
| TMPL-09 | Phase 19 | Complete | | TMPL-09 | Phase 19 | Complete |
| TMPL-10 | Phase 20 | Pending | | TMPL-10 | Phase 20 | Complete |
| TMPL-11 | Phase 20 | Pending | | TMPL-11 | Phase 20 | Complete |
| TMPL-12 | Phase 20 | Pending | | TMPL-12 | Phase 20 | Complete |
| TMPL-13 | Phase 20 | Pending | | TMPL-13 | Phase 20 | Complete |
| TMPL-14 | Phase 20 | Pending | | TMPL-14 | Phase 20 | Complete |
| TMPL-15 | Phase 20 | Complete | | TMPL-15 | Phase 20 | Complete |
| TMPL-16 | Phase 20 | Complete | | TMPL-16 | Phase 20 | Complete |

View File

@@ -14,8 +14,8 @@ v1.3 adds a document template system — agent creates reusable field layouts wi
-**v1.0 Core Document Signing Platform** - Phases 1-7 (shipped 2026-03-21) -**v1.0 Core Document Signing Platform** - Phases 1-7 (shipped 2026-03-21)
- 🚧 **v1.1 Smart Document Preparation** - Phases 8-13 (in progress) - 🚧 **v1.1 Smart Document Preparation** - Phases 8-13 (in progress)
- 📋 **v1.2 Multi-Signer and Deployment Hardening** - Phases 14-17 (planned) - **v1.2 Multi-Signer and Deployment Hardening** - Phases 14-17 (shipped 2026-04-03)
- 📋 **v1.3 Document Templates** - Phases 18-20 (planned) - **v1.3 Document Templates** - Phases 18-20 (shipped 2026-04-06)
## Phases ## Phases
@@ -188,7 +188,7 @@ Plans:
- [x] **Phase 18: Template Schema and CRUD API** - New `document_templates` table with Drizzle migration; list, create, rename, and soft-delete API routes (completed 2026-04-06) - [x] **Phase 18: Template Schema and CRUD API** - New `document_templates` table with Drizzle migration; list, create, rename, and soft-delete API routes (completed 2026-04-06)
- [x] **Phase 19: Template Editor UI** - FieldPlacer `onPersist` abstraction, template editor page at `/portal/templates/[id]`, AI auto-place wired to template context, signer role label support, save template (completed 2026-04-06) - [x] **Phase 19: Template Editor UI** - FieldPlacer `onPersist` abstraction, template editor page at `/portal/templates/[id]`, AI auto-place wired to template context, signer role label support, save template (completed 2026-04-06)
- [ ] **Phase 20: Apply Template and Portal Nav** - "Start from template" option in AddDocumentModal, apply operation with field snapshot and role-to-email mapping, text hint quick-fill, Templates portal nav section and list page - [x] **Phase 20: Apply Template and Portal Nav** - "Start from template" option in AddDocumentModal, apply operation with field snapshot and role-to-email mapping, text hint quick-fill, Templates portal nav section and list page (completed 2026-04-06)
## Phase Details ## Phase Details
@@ -433,7 +433,11 @@ Plans:
3. Template signer roles are automatically pre-mapped to the client's contacts (first role to primary contact, second role to co-buyer if present); agent can override any mapping before sending 3. Template signer roles are automatically pre-mapped to the client's contacts (first role to primary contact, second role to co-buyer if present); agent can override any mapping before sending
4. Text hints from the template appear as quick-fill suggestion buttons in the new document's PreparePanel 4. Text hints from the template appear as quick-fill suggestion buttons in the new document's PreparePanel
5. "Templates" appears in the portal top nav and `/portal/templates` lists all active templates with form name, field count, and last-updated date 5. "Templates" appears in the portal top nav and `/portal/templates` lists all active templates with form name, field count, and last-updated date
**Plans**: TBD **Plans**: 2 plans
Plans:
- [x] 20-01-PLAN.md — Extend POST /api/documents with template branch + Add My Templates tab to AddDocumentModal
- [x] 20-02-PLAN.md — Template hint quick-fill chips in PreparePanel + human E2E verification
**UI hint**: yes **UI hint**: yes
## Progress ## Progress
@@ -464,4 +468,4 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 →
| 17. Docker Deployment | v1.2 | 2/2 | Complete | 2026-04-03 | | 17. Docker Deployment | v1.2 | 2/2 | Complete | 2026-04-03 |
| 18. Template Schema and CRUD API | v1.3 | 2/2 | Complete | 2026-04-06 | | 18. Template Schema and CRUD API | v1.3 | 2/2 | Complete | 2026-04-06 |
| 19. Template Editor UI | v1.3 | 3/3 | Complete | 2026-04-06 | | 19. Template Editor UI | v1.3 | 3/3 | Complete | 2026-04-06 |
| 20. Apply Template and Portal Nav | v1.3 | 0/TBD | Not started | - | | 20. Apply Template and Portal Nav | v1.3 | 2/2 | Complete | 2026-04-06 |

View File

@@ -3,15 +3,15 @@ gsd_state_version: 1.0
milestone: v1.1 milestone: v1.1
milestone_name: Smart Document Preparation milestone_name: Smart Document Preparation
status: completed status: completed
stopped_at: Completed 19-03-PLAN.md — Phase 19 template editor UI complete, human verification passed stopped_at: Completed 20-02 — human verified all 12 steps; v1.3 milestone complete
last_updated: "2026-04-06T19:55:02.977Z" last_updated: "2026-04-06T21:15:36.957Z"
last_activity: 2026-04-06 last_activity: 2026-04-06
progress: progress:
total_phases: 21 total_phases: 22
completed_phases: 20 completed_phases: 21
total_plans: 63 total_plans: 65
completed_plans: 62 completed_plans: 64
percent: 98 percent: 100
--- ---
# Project State # Project State
@@ -24,9 +24,9 @@ See: .planning/PROJECT.md (updated 2026-04-03)
## Current Position ## Current Position
Phase: 19 Phase: 20
Plan: Not started Plan: Not started
Status: Phase 19 complete — Phase 20 (apply-template-and-portal-nav) ready to begin Status: All plans complete — v1.3 Document Templates milestone shipped
Last activity: 2026-04-06 Last activity: 2026-04-06
## Note on v1.1 ## Note on v1.1
@@ -62,7 +62,7 @@ Start v1.3 with: `/gsd:plan-phase 18`
- v1.2 roadmap created 2026-04-03: Phases 14-17 (Multi-Signer + Docker Deployment) - v1.2 roadmap created 2026-04-03: Phases 14-17 (Multi-Signer + Docker Deployment)
- v1.3 roadmap created 2026-04-06: Phases 18-20 (Document Templates) - v1.3 roadmap created 2026-04-06: Phases 18-20 (Document Templates)
Progress: [██████████████] 98% (19/22 phases complete — counting v1.3 scope) Progress: [██████████████] 100% (22/22 phases complete — v1.0, v1.1, v1.2, v1.3 all shipped)
## Performance Metrics ## Performance Metrics
@@ -108,6 +108,8 @@ Progress: [██████████████░] 98% (19/22 phases comp
| Phase 19-template-editor-ui P02 | 4 | 2 tasks | 5 files | | Phase 19-template-editor-ui P02 | 4 | 2 tasks | 5 files |
| Phase 19-template-editor-ui P03 | 1 | 1 tasks | 0 files | | Phase 19-template-editor-ui P03 | 1 | 1 tasks | 0 files |
| Phase 19 P03 | <1 | 1 tasks | 0 files | | Phase 19 P03 | <1 | 1 tasks | 0 files |
| Phase 20-apply-template-and-portal-nav P01 | 2 | 2 tasks | 2 files |
| Phase 20-apply-template-and-portal-nav P02 | 2 | 1 tasks | 2 files |
## Accumulated Context ## Accumulated Context
@@ -202,6 +204,9 @@ Recent decisions affecting v1.1 work:
- [Phase 19-02]: ConfirmDialog shown for all role removals (not just when fields > 0) — avoids async fetch for conditional UI; simpler and no UX flicker - [Phase 19-02]: ConfirmDialog shown for all role removals (not just when fields > 0) — avoids async fetch for conditional UI; simpler and no UX flicker
- [Phase 19-03]: No code changes in Plan 03 — all deliverables complete in Plans 01 and 02; Plan 03 is pure human E2E verification - [Phase 19-03]: No code changes in Plan 03 — all deliverables complete in Plans 01 and 02; Plan 03 is pure human E2E verification
- [Phase 19-03]: All five TMPL requirements (TMPL-05 through TMPL-09) verified by human in a single 9-step live browser test - [Phase 19-03]: All five TMPL requirements (TMPL-05 through TMPL-09) verified by human in a single 9-step live browser test
- [Phase 20-apply-template-and-portal-nav]: documentTemplateId branch in POST /api/documents returns early so all existing paths unchanged; fresh crypto.randomUUID per field ensures snapshot independence
- [Phase 20-apply-template-and-portal-nav]: My Templates tab lazy-fetches /api/templates on first click via docTemplatesLoaded flag to avoid unnecessary network requests
- [Phase 20-apply-template-and-portal-nav]: Fields fetched on mount and aiPlacementKey change so hint stays current after AI auto-place; selectedFieldHint passed as optional prop to PreparePanel for backwards compatibility
### v1.2 Pre-decisions (from research) ### v1.2 Pre-decisions (from research)
@@ -237,6 +242,6 @@ None yet.
## Session Continuity ## Session Continuity
Last session: 2026-04-06T19:46:16.629Z Last session: 2026-04-06T21:01:00.000Z
Stopped at: Completed 19-03-PLAN.md — Phase 19 template editor UI complete, human verification passed Stopped at: Completed 20-02 — human verified all 12 steps; v1.3 milestone complete
Resume file: None Resume file: None

View File

@@ -1,12 +1,13 @@
{ {
"mode": "yolo", "mode": "yolo",
"depth": "standard",
"parallelization": true, "parallelization": true,
"commit_docs": true, "commit_docs": true,
"model_profile": "balanced", "model_profile": "balanced",
"workflow": { "workflow": {
"research": true, "research": true,
"plan_check": true, "plan_check": true,
"verifier": true "verifier": true,
} "_auto_chain_active": false
},
"granularity": "standard"
} }

View File

@@ -0,0 +1,118 @@
---
phase: 03-agent-portal-shell
plan: 01
subsystem: database
tags: [drizzle, postgres, pgEnum, next-auth, middleware, routing]
# Dependency graph
requires:
- phase: 01-foundation
provides: Drizzle schema.ts with users table, auth.config.ts authorized callback, middleware.ts with /agent matcher
provides:
- clients and documents tables in PostgreSQL with document_status enum
- Drizzle migration 0001_watery_blindfold.sql
- /portal/:path* protected via middleware matcher and auth.config.ts authorized callback
- Post-login redirect lands on /portal/dashboard
- /agent/dashboard silently forwards to /portal/dashboard
affects: [03-02, 03-03, 03-04, all Phase 3 plans — clients/documents tables and /portal routing are foundations]
# Tech tracking
tech-stack:
added: [pgEnum from drizzle-orm/pg-core]
patterns: [pgEnum defined before referencing table, documentStatusEnum as named export, portal route protection mirrors agent route pattern in authorized callback]
key-files:
created:
- teressa-copeland-homes/drizzle/0001_watery_blindfold.sql
- teressa-copeland-homes/drizzle/meta/0001_snapshot.json
modified:
- teressa-copeland-homes/src/lib/db/schema.ts
- teressa-copeland-homes/middleware.ts
- teressa-copeland-homes/src/lib/auth.config.ts
- teressa-copeland-homes/src/app/agent/(protected)/dashboard/page.tsx
key-decisions:
- "documentStatusEnum exported before documents table — pgEnum must be declared before the table that references it or drizzle-kit may omit the CREATE TYPE statement"
- "portal route protection mirrors existing /agent pattern in authorized callback — isPortalRoute check added alongside isAgentRoute, both redirect unauthenticated to /agent/login"
- "Post-login redirect changed from /agent/dashboard to /portal/dashboard — agent portal lives at /portal prefix going forward"
- "DATABASE_URL not loaded from .env.local by drizzle-kit (uses dotenv/config which reads .env) — migration run with explicit env var; .env.local is sufficient for Next.js dev server"
patterns-established:
- "pgEnum: export enum constant before the table that uses it in schema.ts"
- "Route protection: add new route prefix to both middleware.ts matcher array AND auth.config.ts authorized callback isPortalRoute check"
requirements-completed: [CLIENT-01, CLIENT-02, CLIENT-03, DASH-01, DASH-02]
# Metrics
duration: 3min
completed: 2026-03-19
---
# Phase 3 Plan 01: Agent Portal Shell — Data & Routing Foundation Summary
**Drizzle schema extended with clients + documents tables (document_status pgEnum), migration applied to local PostgreSQL, and /portal/* routes protected via middleware + auth.config.ts with post-login redirect updated to /portal/dashboard**
## Performance
- **Duration:** 3 min
- **Started:** 2026-03-19T22:16:34Z
- **Completed:** 2026-03-19T22:19:00Z
- **Tasks:** 2
- **Files modified:** 6
## Accomplishments
- Added `clients` and `documents` tables to Drizzle schema with `documentStatusEnum` (Draft, Sent, Viewed, Signed), generated and applied migration successfully
- Extended middleware.ts matcher and auth.config.ts authorized callback to protect all `/portal/*` routes — unauthenticated requests redirect to `/agent/login`
- Updated post-login redirect from `/agent/dashboard` to `/portal/dashboard`, and replaced the old agent dashboard stub with a forward to `/portal/dashboard`
## Task Commits
Each task was committed atomically:
1. **Task 1: Extend Drizzle schema with clients and documents tables** - `f8f8b8f` (feat)
2. **Task 2: Update middleware and auth config to protect /portal routes** - `00f9c7c` (feat)
**Plan metadata:** (pending docs commit)
## Files Created/Modified
- `teressa-copeland-homes/src/lib/db/schema.ts` - Added pgEnum import, documentStatusEnum, clients table, documents table with FK to clients
- `teressa-copeland-homes/drizzle/0001_watery_blindfold.sql` - Migration: CREATE TYPE document_status AS ENUM, CREATE TABLE clients, CREATE TABLE documents with FK constraint
- `teressa-copeland-homes/drizzle/meta/0001_snapshot.json` - Drizzle schema snapshot for migration tracking
- `teressa-copeland-homes/middleware.ts` - Added "/portal/:path*" to matcher array alongside "/agent/:path*"
- `teressa-copeland-homes/src/lib/auth.config.ts` - Added isPortalRoute check in authorized callback; updated post-login redirect to /portal/dashboard
- `teressa-copeland-homes/src/app/agent/(protected)/dashboard/page.tsx` - Replaced session-check stub with redirect("/portal/dashboard")
## Decisions Made
- `documentStatusEnum` exported before `documents` table in schema.ts — pgEnum must precede the table that references it or drizzle-kit generate may silently omit the CREATE TYPE statement from the migration.
- `DATABASE_URL` not auto-loaded from `.env.local` by drizzle-kit (dotenv/config reads `.env` not `.env.local`) — migration was run with explicit `DATABASE_URL=...` env prefix. The Next.js dev server reads `.env.local` correctly so no change needed there.
- Portal route protection mirrors the existing `/agent` pattern exactly: new `isPortalRoute` variable, redirect to `/agent/login` on unauthenticated access.
## Deviations from Plan
None - plan executed exactly as written.
## Issues Encountered
- drizzle-kit `db:migrate` initially failed with "url: undefined" because it uses `dotenv/config` which reads `.env`, not `.env.local`. Resolved by passing `DATABASE_URL` as an explicit env variable prefix to the npm run command. This is a known behavior difference between Next.js (reads .env.local) and drizzle-kit (reads .env).
## User Setup Required
None - no external service configuration required. Database is local Docker PostgreSQL already running.
## Next Phase Readiness
- `clients` and `documents` tables are live in PostgreSQL; Phase 3 plans 02-04 can reference them immediately
- `/portal/*` route protection is active — any new pages added under `/portal/` are automatically protected
- Post-login redirect lands on `/portal/dashboard` — ready for the dashboard page to be built in plan 03-02
- No blockers for subsequent Phase 3 plans
---
*Phase: 03-agent-portal-shell*
*Completed: 2026-03-19*
## Self-Check: PASSED
All files verified present. All commits verified in git log.

View File

@@ -74,7 +74,7 @@ Each task was committed atomically:
1. **Task 1: Parameterize DraggableToken and add four new palette tokens** - `4140c22` (feat) 1. **Task 1: Parameterize DraggableToken and add four new palette tokens** - `4140c22` (feat)
2. **Task 2: Update handleDragEnd and renderFields for typed field creation** - `1e92ca3` (feat) 2. **Task 2: Update handleDragEnd and renderFields for typed field creation** - `1e92ca3` (feat)
**Plan metadata:** committed in final docs commit **Plan metadata:** `9f190b3` (docs: complete expanded field types palette plan)
## Files Created/Modified ## Files Created/Modified
- `teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx` - Extended with PALETTE_TOKENS, parameterized DraggableToken, typed handleDragEnd, and per-type renderFields - `teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx` - Extended with PALETTE_TOKENS, parameterized DraggableToken, typed handleDragEnd, and per-type renderFields

View File

@@ -125,3 +125,16 @@ The app is ready to be deployed to Teressa's home server via `docker compose up
--- ---
*Phase: 17-docker-deployment* *Phase: 17-docker-deployment*
*Completed: 2026-04-03* *Completed: 2026-04-03*
## Self-Check: PASSED
- FOUND: teressa-copeland-homes/Dockerfile
- FOUND: teressa-copeland-homes/docker-compose.yml
- FOUND: teressa-copeland-homes/.dockerignore
- FOUND: teressa-copeland-homes/.env.production.example
- FOUND: teressa-copeland-homes/DEPLOYMENT.md
- FOUND: .planning/phases/17-docker-deployment/17-02-SUMMARY.md
- FOUND commit e83ced5 (Task 1)
- FOUND commit a107970 (Task 2)
- FOUND commit 72c23f8 (Task 3)
- FOUND commit 4a7605b (metadata)

View File

@@ -0,0 +1,422 @@
---
phase: 20-apply-template-and-portal-nav
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- teressa-copeland-homes/src/app/portal/_components/AddDocumentModal.tsx
- teressa-copeland-homes/src/app/api/documents/route.ts
autonomous: true
requirements: [TMPL-10, TMPL-11, TMPL-12, TMPL-14]
must_haves:
truths:
- "Agent sees a 'My Templates' tab in the Add Document modal alongside the existing Forms Library"
- "Agent can pick a saved template and click Add Document to create a document with all template fields pre-loaded at their saved positions"
- "Every field copied from a template has a fresh UUID — no template field ID appears in the new document"
- "Template signer roles are auto-mapped to client contacts (first role to client email, second role to co-buyer email)"
- "Editing a template afterward does not change the document's fields (snapshot independence)"
artifacts:
- path: "teressa-copeland-homes/src/app/portal/_components/AddDocumentModal.tsx"
provides: "Two-tab modal with Forms Library + My Templates"
contains: "activeTab"
- path: "teressa-copeland-homes/src/app/api/documents/route.ts"
provides: "Template apply branch in POST handler"
contains: "documentTemplateId"
key_links:
- from: "AddDocumentModal.tsx"
to: "POST /api/documents"
via: "fetch with documentTemplateId in JSON body"
pattern: "documentTemplateId.*selectedDocTemplate"
- from: "POST /api/documents"
to: "documentTemplates table"
via: "db.query.documentTemplates.findFirst"
pattern: "documentTemplates"
- from: "POST /api/documents"
to: "clients table"
via: "db.query.clients.findFirst for role-to-email mapping"
pattern: "clients\\.id.*clientId"
---
<objective>
Add "My Templates" tab to AddDocumentModal and extend POST /api/documents to apply a template — copying fields with fresh UUIDs and auto-mapping signer roles to client contacts.
Purpose: Lets the agent start a new document from a saved template instead of a blank form, eliminating repetitive field placement on commonly-used PDFs.
Output: Modified AddDocumentModal.tsx with two tabs, extended POST /api/documents route with documentTemplateId branch.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/20-apply-template-and-portal-nav/20-CONTEXT.md
@.planning/phases/20-apply-template-and-portal-nav/20-RESEARCH.md
<interfaces>
<!-- Key types and contracts. Extracted from codebase. Executor should use directly. -->
From src/lib/db/schema.ts:
```typescript
export interface SignatureFieldData {
id: string;
page: number; // 1-indexed
x: number; // PDF user space, bottom-left origin, points
y: number;
width: number;
height: number;
type?: SignatureFieldType;
signerEmail?: string; // In templates: carries role labels like "Buyer", "Seller"
hint?: string; // Optional label for client-text fields
}
export interface ClientContact {
name: string;
email: string;
}
export interface DocumentSigner {
email: string;
color: string;
}
export const documentTemplates = pgTable("document_templates", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text("name").notNull(),
formTemplateId: text("form_template_id").notNull().references(() => formTemplates.id),
signatureFields: jsonb("signature_fields").$type<SignatureFieldData[]>(),
archivedAt: timestamp("archived_at"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
export const documentTemplatesRelations = relations(documentTemplates, ({ one }) => ({
formTemplate: one(formTemplates, { fields: [documentTemplates.formTemplateId], references: [formTemplates.id] }),
}));
```
From src/app/api/templates/route.ts GET response shape:
```typescript
// Each row in the JSON array:
{
id: string;
name: string;
formTemplateId: string;
formName: string | null;
fieldCount: number;
createdAt: Date;
updatedAt: Date;
}
```
From src/app/api/documents/route.ts — existing POST handler structure:
```typescript
// Content-type branching:
// - multipart/form-data → custom PDF upload (reads file from FormData)
// - else (JSON) → form-library copy (reads formTemplateId, copies PDF from seeds/)
// Both paths: create destDir, copy/write PDF, INSERT into documents table
// The JSON branch is what we extend with documentTemplateId support
```
From src/app/portal/_components/AddDocumentModal.tsx — existing state:
```typescript
type FormTemplate = { id: string; name: string; filename: string };
// Props: { clientId: string; onClose: () => void }
// State: templates (FormTemplate[]), selectedTemplate, customFile, docName, query
// Submit: customFile → FormData POST, else → JSON POST with formTemplateId
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Extend POST /api/documents with documentTemplateId branch</name>
<files>teressa-copeland-homes/src/app/api/documents/route.ts</files>
<read_first>
teressa-copeland-homes/src/app/api/documents/route.ts
teressa-copeland-homes/src/lib/db/schema.ts
teressa-copeland-homes/src/app/api/templates/route.ts
</read_first>
<action>
Extend the JSON body parsing in the POST handler to also extract `documentTemplateId: string | undefined` from `body`.
After the existing `if (!clientId || !name)` guard, add a new branch BEFORE the existing `formTemplateId` branch. The logic:
```
if (documentTemplateId) {
// 1. Fetch document template with its formTemplate relation
const docTemplate = await db.query.documentTemplates.findFirst({
where: eq(documentTemplates.id, documentTemplateId),
with: { formTemplate: true },
});
if (!docTemplate) return Response.json({ error: 'Document template not found' }, { status: 404 });
// 2. Copy PDF from seeds dir using the form template's filename
const srcPath = path.join(SEEDS_DIR, docTemplate.formTemplate.filename);
await copyFile(srcPath, destPath);
// 3. Copy fields with fresh UUIDs (D-07) — hints preserved verbatim (D-09)
const rawFields: SignatureFieldData[] = (docTemplate.signatureFields as SignatureFieldData[] | null) ?? [];
const copiedFields: SignatureFieldData[] = rawFields.map(f => ({
...f,
id: crypto.randomUUID(),
}));
// 4. Role-to-email mapping (D-06)
// Collect unique role labels in order of first appearance
const seenRoles = new Set<string>();
const uniqueRoles: string[] = [];
for (const f of copiedFields) {
if (f.signerEmail && !seenRoles.has(f.signerEmail)) {
seenRoles.add(f.signerEmail);
uniqueRoles.push(f.signerEmail);
}
}
// Fetch client for email + contacts
const client = await db.query.clients.findFirst({
where: eq(clients.id, clientId),
});
const clientEmails = [
client?.email,
...((client?.contacts as ClientContact[] | null) ?? []).map(c => c.email),
].filter(Boolean) as string[];
const SIGNER_COLORS = ['#6366f1', '#f43f5e', '#10b981', '#f59e0b'];
const mappedSigners: DocumentSigner[] = uniqueRoles.map((role, i) => ({
email: clientEmails[i] ?? role,
color: SIGNER_COLORS[i % SIGNER_COLORS.length],
}));
// 5. Override formTemplateId for the DB insert
formTemplateId = docTemplate.formTemplateId;
// 6. Insert with fields and signers
const [doc] = await db.insert(documents).values({
id: docId,
clientId,
name,
formTemplateId: formTemplateId ?? null,
filePath: relPath,
status: 'Draft',
signatureFields: copiedFields,
signers: mappedSigners.length > 0 ? mappedSigners : null,
}).returning();
return Response.json(doc, { status: 201 });
}
```
The existing `else` path (formTemplateId from form library) and file upload path remain completely unchanged.
Add imports at top: `import { documentTemplates, clients } from '@/lib/db/schema';` and `import type { SignatureFieldData, ClientContact, DocumentSigner } from '@/lib/db/schema';`. The `clients` import is new; `documents` and `formTemplates` are already imported.
Also parse `documentTemplateId` from body: add `let documentTemplateId: string | undefined;` alongside existing declarations, and in the JSON else block add `documentTemplateId = body.documentTemplateId;`.
IMPORTANT: The template branch has its OWN db.insert + return, so the existing insert at the bottom of the function only runs for the non-template paths. Structure it so the template branch returns early.
</action>
<verify>
<automated>cd teressa-copeland-homes && npx tsc --noEmit</automated>
</verify>
<acceptance_criteria>
- grep "documentTemplateId" src/app/api/documents/route.ts returns at least 3 matches (declaration, parse, condition)
- grep "crypto.randomUUID" src/app/api/documents/route.ts returns at least 2 matches (docId + field copy)
- grep "signatureFields: copiedFields" src/app/api/documents/route.ts returns 1 match
- grep "SIGNER_COLORS" src/app/api/documents/route.ts returns at least 1 match
- grep "clientEmails" src/app/api/documents/route.ts returns at least 1 match
- grep "import.*clients" src/app/api/documents/route.ts returns 1 match (clients table import)
- npx tsc --noEmit exits 0
</acceptance_criteria>
<done>POST /api/documents accepts documentTemplateId, copies fields with fresh UUIDs, maps roles to client contacts, inserts document with signatureFields and signers</done>
</task>
<task type="auto">
<name>Task 2: Add My Templates tab to AddDocumentModal</name>
<files>teressa-copeland-homes/src/app/portal/_components/AddDocumentModal.tsx</files>
<read_first>
teressa-copeland-homes/src/app/portal/_components/AddDocumentModal.tsx
</read_first>
<action>
Add state variables for the template tab:
```typescript
type DocumentTemplateRow = {
id: string;
name: string;
formName: string | null;
fieldCount: number;
updatedAt: string;
};
const [activeTab, setActiveTab] = useState<'forms' | 'templates'>('forms');
const [docTemplates, setDocTemplates] = useState<DocumentTemplateRow[]>([]);
const [docTemplatesLoaded, setDocTemplatesLoaded] = useState(false);
const [selectedDocTemplate, setSelectedDocTemplate] = useState<DocumentTemplateRow | null>(null);
```
Add lazy fetch function — fetches templates only on first click of the My Templates tab:
```typescript
function handleSwitchToTemplates() {
setActiveTab('templates');
if (!docTemplatesLoaded) {
fetch('/api/templates')
.then(r => r.json())
.then((data: DocumentTemplateRow[]) => { setDocTemplates(data); setDocTemplatesLoaded(true); })
.catch(console.error);
}
}
```
Add mutual exclusivity to selection handlers:
- In `handleSelectTemplate` (existing form-library handler): add `setSelectedDocTemplate(null);` at top
- Add new handler for document template selection:
```typescript
const handleSelectDocTemplate = (t: DocumentTemplateRow) => {
setSelectedDocTemplate(t);
setSelectedTemplate(null);
setCustomFile(null);
setDocName(t.name);
};
```
Extend `handleSubmit`:
- Add a NEW branch at the top of the try block, before the existing `if (customFile)`:
```typescript
if (selectedDocTemplate) {
await fetch('/api/documents', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clientId,
name: docName.trim(),
documentTemplateId: selectedDocTemplate.id,
}),
});
} else if (customFile) {
// ... existing custom file path unchanged
} else {
// ... existing form library path unchanged
}
```
Update the early-return guard in handleSubmit:
```typescript
if (!docName.trim() || (!selectedTemplate && !customFile && !selectedDocTemplate)) return;
```
Update the submit button disabled condition:
```typescript
disabled={saving || (!selectedTemplate && !customFile && !selectedDocTemplate) || !docName.trim()}
```
Render two tab buttons between the h2 heading and the search input. Use underline-style tabs matching the project's plain Tailwind approach:
```tsx
<div className="flex border-b mb-4">
<button
type="button"
onClick={() => setActiveTab('forms')}
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
activeTab === 'forms'
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
Forms Library
</button>
<button
type="button"
onClick={handleSwitchToTemplates}
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
activeTab === 'templates'
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
My Templates
</button>
</div>
```
Conditionally render tab content:
- When `activeTab === 'forms'`: show the existing search input + form list + custom file upload section (ALL existing markup unchanged)
- When `activeTab === 'templates'`: show the templates list:
```tsx
{activeTab === 'templates' && (
<div className="mb-4">
{!docTemplatesLoaded ? (
<p className="text-sm text-gray-500 py-4 text-center">Loading templates...</p>
) : docTemplates.length === 0 ? (
<p className="text-sm text-gray-500 py-4 text-center">
No templates saved yet. Create one from the Templates page.
</p>
) : (
<ul className="border rounded max-h-48 overflow-y-auto">
{docTemplates.map(t => (
<li
key={t.id}
onClick={() => handleSelectDocTemplate(t)}
className={`px-3 py-2 text-sm cursor-pointer hover:bg-gray-100 ${
selectedDocTemplate?.id === t.id ? 'bg-blue-50 font-medium' : ''
}`}
>
<span className="block">{t.name}</span>
<span className="text-xs text-gray-400">
{t.formName ?? 'Unknown form'} &middot; {t.fieldCount} field{t.fieldCount !== 1 ? 's' : ''}
</span>
</li>
))}
</ul>
)}
</div>
)}
```
The `{activeTab === 'forms' && ( ... )}` wraps around the existing search input, form list `<ul>`, and custom file upload `<div>`. All existing elements are preserved exactly — only wrapped in a conditional.
</action>
<verify>
<automated>cd teressa-copeland-homes && npx tsc --noEmit</automated>
</verify>
<acceptance_criteria>
- grep "activeTab" src/app/portal/_components/AddDocumentModal.tsx returns at least 4 matches
- grep "My Templates" src/app/portal/_components/AddDocumentModal.tsx returns at least 1 match
- grep "Forms Library" src/app/portal/_components/AddDocumentModal.tsx returns at least 1 match
- grep "selectedDocTemplate" src/app/portal/_components/AddDocumentModal.tsx returns at least 3 matches
- grep "documentTemplateId" src/app/portal/_components/AddDocumentModal.tsx returns at least 1 match
- grep "/api/templates" src/app/portal/_components/AddDocumentModal.tsx returns at least 1 match
- grep "No templates saved yet" src/app/portal/_components/AddDocumentModal.tsx returns 1 match (empty state)
- npx tsc --noEmit exits 0
</acceptance_criteria>
<done>AddDocumentModal has two tabs (Forms Library + My Templates), lazy-loads templates on first tab click, sends documentTemplateId to POST /api/documents when a template is selected</done>
</task>
</tasks>
<verification>
1. `cd teressa-copeland-homes && npx tsc --noEmit` — zero type errors
2. `cd teressa-copeland-homes && npm run build` — production build succeeds
3. grep confirms: documentTemplateId in both route.ts and AddDocumentModal.tsx
4. grep confirms: crypto.randomUUID in route.ts field copy
5. grep confirms: SIGNER_COLORS in route.ts
6. grep confirms: activeTab, selectedDocTemplate in AddDocumentModal.tsx
</verification>
<success_criteria>
- POST /api/documents accepts documentTemplateId and creates a document with copied fields (fresh UUIDs), mapped signers, and correct formTemplateId
- AddDocumentModal shows two tabs; My Templates tab fetches from GET /api/templates and displays template rows
- Selecting a template and clicking Add Document creates a document via the template branch
- No changes to existing Forms Library tab or custom file upload behavior
- TypeScript compiles with zero errors
</success_criteria>
<output>
After completion, create `.planning/phases/20-apply-template-and-portal-nav/20-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,105 @@
---
phase: 20-apply-template-and-portal-nav
plan: "01"
subsystem: document-template-apply
tags: [templates, documents, modal, api]
dependency_graph:
requires: [phase-18-template-schema-and-crud-api, phase-19-template-editor-ui]
provides: [template-apply-to-document, my-templates-tab]
affects: [AddDocumentModal, POST /api/documents]
tech_stack:
added: []
patterns: [snapshot-copy-with-fresh-uuids, role-to-email-mapping, lazy-fetch-on-tab-switch]
key_files:
created: []
modified:
- teressa-copeland-homes/src/app/api/documents/route.ts
- teressa-copeland-homes/src/app/portal/_components/AddDocumentModal.tsx
decisions:
- "documentTemplateId branch placed before fileBuffer and formTemplateId branches — returns early so existing paths are fully unchanged"
- "SIGNER_COLORS constant extracted at module level for reuse in role-to-email mapping"
- "resolvedFormTemplateId used (not reassigning formTemplateId let var) to avoid confusion with the existing library branch variable"
- "handleSwitchToTemplates lazy-fetches only on first click — docTemplatesLoaded flag prevents repeated fetches on tab toggle"
- "handleSelectDocTemplate clears selectedTemplate and customFile for mutual exclusivity; handleSelectTemplate now also clears selectedDocTemplate"
metrics:
duration_minutes: 2
completed_date: "2026-04-06"
tasks_completed: 2
files_modified: 2
---
# Phase 20 Plan 01: Apply Template and Portal Nav Summary
**One-liner:** Template apply branch in POST /api/documents copies fields with fresh UUIDs and maps signer roles to client contacts; AddDocumentModal gains a My Templates tab with lazy-loaded template list.
## Tasks Completed
| # | Task | Commit | Files |
|---|------|--------|-------|
| 1 | Extend POST /api/documents with documentTemplateId branch | bdf0cb0 | src/app/api/documents/route.ts |
| 2 | Add My Templates tab to AddDocumentModal | 2947fa5 | src/app/portal/_components/AddDocumentModal.tsx |
## What Was Built
### Task 1 — POST /api/documents template branch
Extended the JSON body path of the POST handler to accept `documentTemplateId`. When provided:
1. Fetches the `documentTemplates` record with its `formTemplate` relation (for the PDF filename)
2. Copies the PDF from `seeds/forms/` using the form template's filename
3. Maps every field to a new object with a fresh `crypto.randomUUID()` — template field IDs never appear in documents
4. Collects unique signer role labels (stored in `signerEmail` on template fields) in order of first appearance
5. Fetches the client record and builds `clientEmails = [client.email, ...contacts[].email]`
6. Maps roles to emails: role[0] → clientEmails[0], role[1] → clientEmails[1], etc.; falls back to the role label if no email at that index
7. Inserts the document with `signatureFields: copiedFields` and `signers: mappedSigners` and returns 201
The existing custom-upload path and form-library path are completely unchanged.
### Task 2 — AddDocumentModal My Templates tab
Added a two-tab UI (Forms Library | My Templates) to the AddDocumentModal:
- Tab bar uses underline-style active indicator matching project Tailwind conventions
- My Templates tab lazy-fetches `GET /api/templates` on first click via `docTemplatesLoaded` flag
- Empty state shown when no templates exist; loading state shown while fetching
- Template rows show name, form name, and field count
- Selecting a template clears `selectedTemplate` and `customFile` (mutual exclusivity)
- Selecting a form or uploading a file clears `selectedDocTemplate`
- `handleSubmit` has a new top branch: when `selectedDocTemplate` is set, sends `documentTemplateId` to POST /api/documents as JSON
- Guard condition and submit button disabled state both updated to include `selectedDocTemplate`
- All existing Forms Library and custom upload markup preserved exactly — only wrapped in `activeTab === 'forms'` conditional
## Verification
- `npx tsc --noEmit` — zero type errors (both tasks)
- `npm run build` — production build succeeds
- `documentTemplateId` present in route.ts (4 matches) and AddDocumentModal.tsx (1 match)
- `crypto.randomUUID` present in route.ts (2 matches: docId + field copy)
- `SIGNER_COLORS` present in route.ts
- `activeTab` (5 matches) and `selectedDocTemplate` (6 matches) in AddDocumentModal.tsx
## Deviations from Plan
### Auto-fixed Issues
None.
### Plan Adjustments
**handleFileChange also clears selectedDocTemplate** — The plan specified mutual exclusivity in `handleSelectTemplate` and `handleSelectDocTemplate`, but `handleFileChange` (custom file picker) also needed to clear `selectedDocTemplate` to maintain full mutual exclusivity across all three selection paths. Added `setSelectedDocTemplate(null)` to `handleFileChange`. This is a completeness fix, not a deviation from intent.
**resolvedFormTemplateId instead of reassigning formTemplateId** — The plan showed `formTemplateId = docTemplate.formTemplateId` to override the let variable. Instead used a new `const resolvedFormTemplateId` to avoid modifying a shared variable that existing branches also read. Cleaner and avoids any unintended side effects.
## Known Stubs
None — all data paths are fully wired. The template apply branch fetches real data from the database and inserts real documents with copied fields and mapped signers.
## Self-Check: PASSED
Files exist:
- FOUND: teressa-copeland-homes/src/app/api/documents/route.ts
- FOUND: teressa-copeland-homes/src/app/portal/_components/AddDocumentModal.tsx
Commits exist:
- bdf0cb0 — feat(20-01): extend POST /api/documents with documentTemplateId branch
- 2947fa5 — feat(20-01): add My Templates tab to AddDocumentModal

View File

@@ -0,0 +1,258 @@
---
phase: 20-apply-template-and-portal-nav
plan: 02
type: execute
wave: 2
depends_on: [20-01]
files_modified:
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
autonomous: false
requirements: [TMPL-13, TMPL-15, TMPL-16]
must_haves:
truths:
- "When agent selects a text field that has a hint (from template), a 'Template Hint' chip appears in the Quick Fill section of PreparePanel"
- "Clicking the hint chip fills the selected field with the hint text (same behavior as existing quick-fill)"
- "Fields without hints show no extra chip — existing quick-fill behavior unchanged"
- "Templates nav link exists in portal navigation (Phase 19, already done)"
- "Templates list page shows all templates with form name, field count, last-updated (Phase 19, already done)"
artifacts:
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx"
provides: "Fields state fetch + selectedFieldHint derivation"
contains: "selectedFieldHint"
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx"
provides: "Template Hint chip in Quick Fill section"
contains: "selectedFieldHint"
key_links:
- from: "DocumentPageClient.tsx"
to: "GET /api/documents/:docId/fields"
via: "fetch on mount + aiPlacementKey change"
pattern: "api/documents.*fields"
- from: "DocumentPageClient.tsx"
to: "PreparePanel"
via: "selectedFieldHint prop"
pattern: "selectedFieldHint="
- from: "PreparePanel.tsx"
to: "onQuickFill callback"
via: "hint chip onClick"
pattern: "Template Hint"
---
<objective>
Surface template text hints as quick-fill suggestions in PreparePanel and verify the complete Phase 20 feature set with human testing.
Purpose: When a document is created from a template, text fields with hints (e.g., "Property Address", "Purchase Price") show those hints as one-click quick-fill chips, saving the agent from remembering what each blank field is for.
Output: Modified DocumentPageClient.tsx (fields state + hint derivation), modified PreparePanel.tsx (hint chip), human verification of the full template-to-document flow.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/20-apply-template-and-portal-nav/20-CONTEXT.md
@.planning/phases/20-apply-template-and-portal-nav/20-RESEARCH.md
@.planning/phases/20-apply-template-and-portal-nav/20-01-SUMMARY.md
<interfaces>
<!-- Key types and contracts from codebase. -->
From src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx:
```typescript
interface DocumentPageClientProps {
docId: string;
docStatus: string;
defaultEmail: string;
clientName: string;
agentDownloadUrl?: string | null;
signedAt?: Date | null;
clientPropertyAddress?: string | null;
initialSigners: DocumentSigner[];
clientContacts?: { name: string; email: string }[];
}
// Existing state:
// selectedFieldId: string | null
// textFillData: Record<string, string>
// aiPlacementKey: number
// signers: DocumentSigner[]
```
From src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx:
```typescript
interface PreparePanelProps {
docId: string;
defaultEmail: string;
clientName: string;
currentStatus: string;
agentDownloadUrl?: string | null;
signedAt?: Date | null;
clientPropertyAddress?: string | null;
previewToken: string | null;
onPreviewTokenChange: (token: string | null) => void;
textFillData: Record<string, string>;
selectedFieldId: string | null;
onQuickFill: (fieldId: string, value: string) => void;
onAiAutoPlace: () => Promise<void>;
signers?: DocumentSigner[];
onSignersChange?: (signers: DocumentSigner[]) => void;
unassignedFieldIds?: Set<string>;
onUnassignedFieldIdsChange?: (ids: Set<string>) => void;
}
```
Quick Fill section pattern (PreparePanel lines 313-355):
```typescript
{/* Quick-fill panel — only shown when a text field is selected */}
{selectedFieldId ? (
<div className="space-y-1.5">
<p className="text-xs text-gray-400">Click a suggestion to fill the selected field.</p>
{clientName && ( <button onClick={() => onQuickFill(selectedFieldId, clientName)}>...</button> )}
{clientPropertyAddress && ( <button onClick={() => onQuickFill(selectedFieldId, clientPropertyAddress)}>...</button> )}
<button onClick={() => onQuickFill(selectedFieldId, defaultEmail)}>Client Email chip</button>
</div>
) : (
<p>Click a text field on the document...</p>
)}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add fields state to DocumentPageClient and pass selectedFieldHint to PreparePanel</name>
<files>
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
</files>
<read_first>
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
teressa-copeland-homes/src/lib/db/schema.ts
</read_first>
<action>
**In DocumentPageClient.tsx:**
1. Add import: `import type { SignatureFieldData } from '@/lib/db/schema';` (DocumentSigner is already imported).
2. Add state for fields:
```typescript
const [fields, setFields] = useState<SignatureFieldData[]>([]);
```
3. Add useEffect to fetch fields on mount and when aiPlacementKey changes:
```typescript
useEffect(() => {
fetch(`/api/documents/${docId}/fields`)
.then(r => r.json())
.then((data: SignatureFieldData[]) => setFields(Array.isArray(data) ? data : []))
.catch(() => {});
}, [docId, aiPlacementKey]);
```
4. Derive the hint for the selected field:
```typescript
const selectedFieldHint = selectedFieldId
? fields.find(f => f.id === selectedFieldId)?.hint
: undefined;
```
5. Pass new prop to PreparePanel:
```typescript
<PreparePanel
// ... all existing props unchanged ...
selectedFieldHint={selectedFieldHint}
/>
```
**In PreparePanel.tsx:**
1. Add `selectedFieldHint?: string;` to the `PreparePanelProps` interface (after `onQuickFill`).
2. Destructure it in the function signature: add `selectedFieldHint,` to the destructured props.
3. In the Quick Fill section (inside the `{selectedFieldId ? (` branch), add a new chip AFTER the existing Client Email chip button (before the closing `</div>` of `space-y-1.5`):
```tsx
{selectedFieldHint && (
<button
type="button"
onClick={() => onQuickFill(selectedFieldId, selectedFieldHint)}
className="w-full text-left px-3 py-2 text-sm border rounded bg-white hover:bg-blue-50 hover:border-blue-300 transition-colors"
>
<span className="text-xs text-gray-400 block">Template Hint</span>
<span className="truncate block">{selectedFieldHint}</span>
</button>
)}
```
This follows the exact same markup pattern as the existing Client Name / Property Address / Client Email chips. The prop is optional with no default — existing callers (DocumentPageClient without template-sourced documents) simply don't pass it, and `undefined` means no chip renders.
</action>
<verify>
<automated>cd teressa-copeland-homes && npx tsc --noEmit</automated>
</verify>
<acceptance_criteria>
- grep "selectedFieldHint" src/app/portal/\(protected\)/documents/\[docId\]/_components/DocumentPageClient.tsx returns at least 2 matches (derivation + prop pass)
- grep "selectedFieldHint" src/app/portal/\(protected\)/documents/\[docId\]/_components/PreparePanel.tsx returns at least 3 matches (interface + destructure + render)
- grep "Template Hint" src/app/portal/\(protected\)/documents/\[docId\]/_components/PreparePanel.tsx returns 1 match
- grep "api/documents.*fields" src/app/portal/\(protected\)/documents/\[docId\]/_components/DocumentPageClient.tsx returns at least 1 match
- grep "SignatureFieldData" src/app/portal/\(protected\)/documents/\[docId\]/_components/DocumentPageClient.tsx returns at least 1 match (import)
- npx tsc --noEmit exits 0
</acceptance_criteria>
<done>DocumentPageClient fetches fields on mount, derives selectedFieldHint from selected field, passes it to PreparePanel. PreparePanel renders a "Template Hint" quick-fill chip when the hint exists.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 2: Human verification of full template-to-document flow</name>
<what-built>
Complete "Start from template" feature:
- My Templates tab in Add Document modal (Plan 01)
- Template apply with fresh field UUIDs and role mapping (Plan 01)
- Text hint quick-fill chips in PreparePanel (Plan 02 Task 1)
- Templates nav + list page (Phase 19, already live)
</what-built>
<how-to-verify>
Prerequisites: At least one saved template with fields and text hints must exist (created in Phase 19 testing).
1. Navigate to /portal/templates — verify the list page shows saved templates with form name, field count, and updated date (TMPL-16)
2. Verify "Templates" appears in the portal top nav (TMPL-15)
3. Go to a client profile page → click "Add Document"
4. Verify the modal shows two tabs: "Forms Library" and "My Templates" (TMPL-10)
5. Click "My Templates" tab → verify saved templates appear with name, form name, and field count
6. Select a template → verify the document name auto-fills with the template name
7. Click "Add Document" → verify the document is created and you're returned to the client page
8. Open the newly created document → verify fields are pre-loaded at the correct positions on the PDF (TMPL-11)
9. In PreparePanel, click a text field that has a hint → verify a "Template Hint" chip appears in the Quick Fill section alongside Client Name/Address/Email (TMPL-13)
10. Click the hint chip → verify the field is filled with the hint text
11. Click the "Forms Library" tab back in a new Add Document modal → verify the existing form library still works exactly as before (D-04)
12. (TMPL-14 — snapshot independence): Go to /portal/templates → edit the template (change a field position) → go back to the document created in step 7 → verify the document's fields are unchanged (still at original positions)
</how-to-verify>
<resume-signal>Type "approved" or describe any issues found</resume-signal>
</task>
</tasks>
<verification>
1. `cd teressa-copeland-homes && npx tsc --noEmit` — zero type errors
2. `cd teressa-copeland-homes && npm run build` — production build succeeds
3. Human verification confirms all 12 steps pass
4. TMPL-10 through TMPL-16 all satisfied (TMPL-14/15/16 by design or Phase 19)
</verification>
<success_criteria>
- Template Hint chip appears in Quick Fill when a text field with a hint is selected
- Chip click fills the field (same behavior as existing quick-fill chips)
- Fields without hints show no extra chip
- Human confirms full template-to-document flow works end-to-end
- All 7 TMPL requirements (TMPL-10 through TMPL-16) are satisfied
</success_criteria>
<output>
After completion, create `.planning/phases/20-apply-template-and-portal-nav/20-02-SUMMARY.md`
</output>

View File

@@ -0,0 +1,116 @@
---
phase: 20-apply-template-and-portal-nav
plan: 02
subsystem: ui
tags: [react, nextjs, typescript, templates, quick-fill]
# Dependency graph
requires:
- phase: 20-01
provides: "Start from template in AddDocumentModal, field snapshot with fresh UUIDs, role mapping"
- phase: 18-template-schema-and-crud-api
provides: "documentTemplates table, SignatureFieldData.hint field in schema"
provides:
- "Template Hint quick-fill chip in PreparePanel for text fields with hints"
- "DocumentPageClient fetches fields and derives hint for selected field"
affects: [future template enhancements, PreparePanel quick-fill UI]
# Tech tracking
tech-stack:
added: []
patterns:
- "Fields state fetched in DocumentPageClient using same /api/documents/:id/fields endpoint used by handlePrepare"
- "selectedFieldHint derived from fields array lookup — no new API endpoint needed"
- "Optional prop pattern: selectedFieldHint? — undefined means no chip, existing callers unaffected"
key-files:
created: []
modified:
- "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx"
- "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx"
key-decisions:
- "Fields fetched on mount and on aiPlacementKey change so hint stays current after AI auto-place"
- "selectedFieldHint passed as optional prop to PreparePanel — undefined means chip not rendered, backwards-compatible"
- "Template Hint chip placed after Client Email chip in Quick Fill section, matching exact same markup pattern"
patterns-established:
- "Hint chip: two-span button (label + value) matching Client Name / Property Address / Email pattern"
requirements-completed: [TMPL-13, TMPL-15, TMPL-16]
# Metrics
duration: 2min
completed: 2026-04-06
---
# Phase 20 Plan 02: Apply Template and Portal Nav (Hint Quick-fill) Summary
**Template Hint quick-fill chip in PreparePanel surfaces text field hints from document_templates, wired via fields fetch in DocumentPageClient**
## Performance
- **Duration:** ~8 min
- **Started:** 2026-04-06T20:53:00Z
- **Completed:** 2026-04-06T21:01:00Z
- **Tasks:** 2 of 2
- **Files modified:** 2
## Accomplishments
- DocumentPageClient now fetches /api/documents/:id/fields on mount and after AI auto-place, storing fields in local state
- selectedFieldHint derived from selected field's hint property — zero cost, no new API endpoint
- PreparePanel accepts selectedFieldHint? prop and renders a "Template Hint" chip in the Quick Fill section when hint exists
- Backwards-compatible: existing document consumers without template hints pass no prop, no chip rendered
## Task Commits
Each task was committed atomically:
1. **Task 1: Add fields state to DocumentPageClient and pass selectedFieldHint to PreparePanel** - `eec0bd9` (feat)
2. **Task 2: Human E2E verification of full template-to-document flow** - Human approved all 12 steps (checkpoint)
## Files Created/Modified
- `teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx` - Added fields state, useEffect to fetch fields, selectedFieldHint derivation, prop pass
- `teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx` - Added selectedFieldHint prop to interface + destructure + Template Hint chip in Quick Fill section
## Decisions Made
- Fields fetched on mount and aiPlacementKey change so the hint refreshes after AI auto-place replaces fields
- Optional prop threading pattern maintains backwards compatibility with existing callers
## Deviations from Plan
None - plan executed exactly as written.
## Issues Encountered
None.
## User Setup Required
None - no external service configuration required.
## Human Verification Results
Task 2 checkpoint approved. All 12 verification steps passed:
1. /portal/templates list page shows saved templates with form name, field count, and updated date (TMPL-16)
2. "Templates" appears in the portal top nav (TMPL-15)
3. Client profile "Add Document" modal opens
4. Modal shows two tabs: "Forms Library" and "My Templates" (TMPL-10)
5. My Templates tab shows saved templates with name, form name, and field count
6. Selecting a template auto-fills the document name with the template name
7. "Add Document" creates the document and returns to client page
8. Newly created document shows fields pre-loaded at correct positions (TMPL-11)
9. Clicking a text field with a hint shows "Template Hint" chip in PreparePanel Quick Fill (TMPL-13)
10. Clicking the hint chip fills the field with the hint text
11. Forms Library tab in a new modal still works as before (D-04)
12. Editing the template does NOT change the already-created document's fields (TMPL-14 — snapshot independence)
Requirements satisfied: TMPL-10, TMPL-11, TMPL-12, TMPL-13, TMPL-14, TMPL-15, TMPL-16 — all 7 confirmed
## Next Phase Readiness
- Phase 20 complete — v1.3 (Document Templates) milestone fully shipped
- All 7 TMPL requirements (TMPL-10 through TMPL-16) verified by human
- v1.3 roadmap objective achieved: agent can create reusable field layouts, apply them to new client documents, and get text hint quick-fill suggestions
---
*Phase: 20-apply-template-and-portal-nav*
*Completed: 2026-04-06*

View File

@@ -0,0 +1,78 @@
---
phase: 20
slug: apply-template-and-portal-nav
status: draft
nyquist_compliant: false
wave_0_complete: false
created: 2026-04-06
---
# Phase 20 — Validation Strategy
> Per-phase validation contract for feedback sampling during execution.
---
## Test Infrastructure
| Property | Value |
|----------|-------|
| **Framework** | None (established project pattern — manual E2E verification only) |
| **Config file** | N/A |
| **Quick run command** | `cd teressa-copeland-homes && npx tsc --noEmit` |
| **Full suite command** | `cd teressa-copeland-homes && npm run build` |
| **Estimated runtime** | ~30 seconds |
---
## Sampling Rate
- **After every task commit:** Run `npx tsc --noEmit`
- **After every plan wave:** Run `npm run build`
- **Before `/gsd:verify-work`:** Full build must succeed with no type errors
- **Max feedback latency:** 30 seconds
---
## Per-Task Verification Map
| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
| 20-01-01 | 01 | 1 | TMPL-10, TMPL-11 | type-check | `npx tsc --noEmit` | ❌ W0 | ⬜ pending |
| 20-01-02 | 01 | 1 | TMPL-12, TMPL-13 | type-check | `npx tsc --noEmit` | ✅ | ⬜ pending |
| 20-02-01 | 02 | 2 | TMPL-10, TMPL-11, TMPL-12 | manual | see manual table | — | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
---
## Wave 0 Requirements
- [ ] No new files — all changes are to existing files (AddDocumentModal.tsx, POST documents route, PreparePanel.tsx / DocumentPageClient.tsx)
*Existing infrastructure covers all phase requirements. No stub files needed.*
---
## Manual-Only Verifications
| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| My Templates tab appears in Add Document modal | TMPL-10 | Browser interaction | Open client profile → Add Document → verify "My Templates" tab present |
| Picking a template creates doc with pre-loaded fields | TMPL-11 | Browser + DB | Pick saved template → create doc → open doc editor → verify fields at saved positions |
| Template field UUIDs are fresh (not template IDs) | TMPL-12 | DB inspection | Compare new document fields IDs against template `signatureFields` IDs |
| Roles auto-mapped to client contacts | TMPL-13 | Browser | Create doc from template → open PreparePanel → verify Buyer = client email, Seller = co-buyer email |
| Text hints appear as quick-fill chips | TMPL-10 | Browser | Click a text field with a hint → verify chip appears in Quick Fill area |
---
## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 30s
- [ ] `nyquist_compliant: true` set in frontmatter
**Approval:** pending

View File

@@ -0,0 +1,147 @@
---
phase: 20-apply-template-and-portal-nav
verified: 2026-04-06T21:30:00Z
status: passed
score: 8/8 must-haves verified
re_verification: false
gaps: []
human_verification:
- test: "Start from template end-to-end flow"
expected: "Agent can pick a saved template in Add Document modal, create a document with pre-loaded fields, and use hint chips in PreparePanel — confirmed by human in Plan 02 Task 2 checkpoint (all 12 steps approved)"
why_human: "Visual rendering, field position accuracy, and real-time quick-fill behavior cannot be verified programmatically"
---
# Phase 20: Apply Template and Portal Nav — Verification Report
**Phase Goal:** Agent can start any new client document from a saved template — all fields are pre-loaded with fresh IDs, roles map to real signer emails, text hints appear as quick-fill suggestions — and "Templates" is a top-level portal destination
**Verified:** 2026-04-06T21:30:00Z
**Status:** PASSED
**Re-verification:** No — initial verification
---
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|---------|
| 1 | Agent sees a "My Templates" tab in the Add Document modal alongside the existing Forms Library | VERIFIED | `AddDocumentModal.tsx` lines 117140: tab bar with "Forms Library" and "My Templates" buttons using underline-style active state |
| 2 | Agent can pick a saved template and click Add Document to create a document with all template fields pre-loaded | VERIFIED | `AddDocumentModal.tsx` lines 8089: `selectedDocTemplate` branch sends `documentTemplateId` to POST /api/documents; route.ts lines 59116: template branch fetches fields, copies with fresh UUIDs, inserts document with `signatureFields: copiedFields` |
| 3 | Every field copied from a template has a fresh UUID — no template field ID appears in the new document | VERIFIED | `route.ts` line 74: `rawFields.map(f => ({ ...f, id: crypto.randomUUID() }))` — spread preserves all field data, only id is overwritten |
| 4 | Template signer roles are auto-mapped to client contacts (first role to client email, second to co-buyer email) | VERIFIED | `route.ts` lines 7899: unique roles collected in appearance order, client fetched, `clientEmails` built from `[client.email, ...contacts[].email]`, mapped with `clientEmails[i] ?? role` fallback |
| 5 | Agent can override the signer mapping before sending | VERIFIED | `PreparePanel.tsx` lines 143165: `handleAddSigner` / `handleRemoveSigner` with `onSignersChange` callback; agent can add/remove any signer at any time before sending |
| 6 | Text field hints from template appear as quick-fill suggestions in PreparePanel | VERIFIED | `DocumentPageClient.tsx` lines 37, 5055, 101103, 135: fields state fetched from `/api/documents/${docId}/fields`, `selectedFieldHint` derived from `fields.find`, passed as prop; `PreparePanel.tsx` lines 98, 116, 351359: optional prop accepted, "Template Hint" chip rendered when hint exists |
| 7 | "Templates" is a top-level portal navigation destination | VERIFIED | `PortalNav.tsx` line 10: `{ href: "/portal/templates", label: "Templates" }` in `navLinks` array; route exists at `src/app/portal/(protected)/templates/page.tsx` |
| 8 | Templates list page shows all templates with form name, field count, and last-updated date | VERIFIED | `TemplatesListClient.tsx` lines 126164: each row renders `template.name`, `template.formName`, field count (`signatureFields.length`), and `new Date(template.updatedAt).toLocaleDateString()` |
**Score:** 8/8 truths verified
---
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|---------|--------|---------|
| `src/app/portal/_components/AddDocumentModal.tsx` | Two-tab modal with Forms Library + My Templates | VERIFIED | 238 lines, contains `activeTab`, `docTemplates`, `selectedDocTemplate`, "My Templates" text, lazy fetch on tab switch |
| `src/app/api/documents/route.ts` | Template apply branch in POST handler | VERIFIED | 143 lines, contains `documentTemplateId`, `documentTemplates`, `clients`, `SIGNER_COLORS`, `signatureFields: copiedFields` |
| `src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx` | Fields state fetch + selectedFieldHint derivation | VERIFIED | Contains `fields` state, `useEffect` fetching `/api/documents/${docId}/fields`, `selectedFieldHint` derivation, prop pass to PreparePanel |
| `src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx` | Template Hint chip in Quick Fill section | VERIFIED | Contains `selectedFieldHint?: string` in interface, destructured, renders "Template Hint" chip at line 357 |
| `src/app/portal/(protected)/templates/page.tsx` | Templates list page | VERIFIED | Server component, queries `documentTemplates` left-joined with `formTemplates`, passes to `TemplatesListClient` |
| `src/app/portal/_components/PortalNav.tsx` | "Templates" nav link | VERIFIED | `{ href: "/portal/templates", label: "Templates" }` confirmed |
---
### Key Link Verification
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `AddDocumentModal.tsx` | POST /api/documents | `fetch('/api/documents', { body: JSON.stringify({ documentTemplateId: selectedDocTemplate.id }) })` | WIRED | Lines 8189: method POST, Content-Type application/json, `documentTemplateId` in body |
| POST /api/documents | `documentTemplates` table | `db.query.documentTemplates.findFirst({ where: eq(documentTemplates.id, documentTemplateId) })` | WIRED | Lines 6063: fetches template with `formTemplate` relation |
| POST /api/documents | `clients` table | `db.query.clients.findFirst({ where: eq(clients.id, clientId) })` | WIRED | Lines 8894: fetches client, builds `clientEmails` array |
| `DocumentPageClient.tsx` | GET /api/documents/:id/fields | `fetch(\`/api/documents/${docId}/fields\`)` in useEffect | WIRED | Line 51: fetches on mount and on `aiPlacementKey` change, sets `fields` state |
| `DocumentPageClient.tsx` | PreparePanel | `selectedFieldHint={selectedFieldHint}` prop | WIRED | Line 135: prop passed; PreparePanel interface declares `selectedFieldHint?: string` at line 98 |
| `PreparePanel.tsx` | `onQuickFill` callback | hint chip `onClick={() => onQuickFill(selectedFieldId, selectedFieldHint)}` | WIRED | Lines 352355: standard quick-fill callback pattern matching existing chips |
---
### Data-Flow Trace (Level 4)
| Artifact | Data Variable | Source | Produces Real Data | Status |
|----------|---------------|--------|--------------------|--------|
| `AddDocumentModal.tsx` — My Templates tab | `docTemplates` | `fetch('/api/templates')` → `GET /api/templates/route.ts` → `db.select(...)from(documentTemplates)` | Yes — real DB query | FLOWING |
| `route.ts` template branch | `copiedFields` | `docTemplate.signatureFields` from DB, mapped with fresh UUIDs | Yes — real DB data | FLOWING |
| `route.ts` template branch | `mappedSigners` | `clientEmails` from `db.query.clients.findFirst`, mapped to `uniqueRoles` | Yes — real DB data | FLOWING |
| `DocumentPageClient.tsx` | `selectedFieldHint` | `fields.find(f => f.id === selectedFieldId)?.hint` from `/api/documents/${docId}/fields` → `doc.signatureFields` from DB | Yes — real DB query in fields route | FLOWING |
| `TemplatesListClient.tsx` | `templates` prop | Server component `db.select(...).from(documentTemplates).leftJoin(formTemplates, ...)` | Yes — real DB query | FLOWING |
---
### Behavioral Spot-Checks
Step 7b: SKIPPED for UI/modal components (requires running browser). TypeScript compilation substitute:
| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| TypeScript compiles without errors | `npx tsc --noEmit` | Zero output (exit 0) | PASS |
---
### Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|------------|-------------|--------|---------|
| TMPL-10 | 20-01-PLAN | Agent can choose "Start from template" and pick a saved template when adding a document | SATISFIED | My Templates tab in `AddDocumentModal.tsx` with template list and `selectedDocTemplate` flow |
| TMPL-11 | 20-01-PLAN | Applying a template creates a document with all template fields pre-loaded (fresh field IDs, positions, types, role assignments copied) | SATISFIED | `route.ts` lines 7275: `rawFields.map(f => ({ ...f, id: crypto.randomUUID() }))` — all fields copied verbatim with only id replaced; inserted as `signatureFields: copiedFields` |
| TMPL-12 | 20-01-PLAN | Template signer roles auto-mapped to client contacts; agent can override before sending | SATISFIED | Auto-map: `route.ts` lines 7899. Override: `PreparePanel.tsx` `handleAddSigner` / `handleRemoveSigner` with `onSignersChange` persists to DB |
| TMPL-13 | 20-02-PLAN | Text hints from template appear as quick-fill suggestions in PreparePanel | SATISFIED | `PreparePanel.tsx` lines 351359: "Template Hint" chip renders when `selectedFieldHint` is truthy, calls `onQuickFill` |
| TMPL-14 | 20-01-PLAN | Editing a template does NOT retroactively change documents created from it | SATISFIED BY DESIGN | Template branch writes a deep copy (spread + new UUID per field) at document creation time. Template `signatureFields` and document `signatureFields` are independent JSONB columns. Confirmed by human in Plan 02 checkpoint step 12. |
| TMPL-15 | 20-02-PLAN (Phase 19 deliverable, verified here) | "Templates" appears as top-level section in portal navigation | SATISFIED | `PortalNav.tsx` line 10: `{ href: "/portal/templates", label: "Templates" }` |
| TMPL-16 | 20-02-PLAN (Phase 19 deliverable, verified here) | Templates list page shows all templates with form name, field count, last-updated date | SATISFIED | `TemplatesListClient.tsx` renders all three data points; `page.tsx` queries DB with `leftJoin` for `formName` and `updatedAt` |
All 7 TMPL requirements (TMPL-10 through TMPL-16) are accounted for. No orphaned requirements found.
---
### Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `AddDocumentModal.tsx` | 185 | `Loading templates...` — shown while fetch is in-flight | Info | Intentional loading state, not a stub |
No blockers or warnings found. The `Loading templates...` text is correct behavior (lazy-load state), not a placeholder.
---
### Human Verification Required
#### 1. Full Template-to-Document Flow
**Test:** (Already performed — Plan 02 Task 2 checkpoint approved by human)
Steps verified:
1. /portal/templates list page shows saved templates with form name, field count, updated date (TMPL-16)
2. "Templates" appears in portal top nav (TMPL-15)
3. Add Document modal shows two tabs: "Forms Library" and "My Templates" (TMPL-10)
4. My Templates tab shows saved templates with name, form name, field count
5. Selecting a template auto-fills document name
6. Add Document creates the document and returns to client page
7. Newly created document shows fields pre-loaded at correct positions (TMPL-11)
8. Text field with hint shows "Template Hint" chip in PreparePanel Quick Fill (TMPL-13)
9. Clicking the chip fills the field with the hint text
10. Forms Library tab still works as before (D-04 regression)
11. Editing the template does NOT change an already-created document's fields (TMPL-14)
**Status:** APPROVED by human — all 12 steps confirmed in 20-02-SUMMARY.md
---
### Gaps Summary
No gaps. All 8 must-haves are verified at all four levels (exists, substantive, wired, data-flowing). All 7 TMPL requirements are satisfied. TypeScript compiles with zero errors. Human checkpoint confirmed the end-to-end flow.
---
_Verified: 2026-04-06T21:30:00Z_
_Verifier: Claude (gsd-verifier)_

View File

@@ -0,0 +1,24 @@
---
title: "Co-buyer/couple support on client profiles"
area: clients
status: backlog
created: 2026-04-03
---
# Co-buyer/couple support on client profiles
## Summary
Agent wants to add multiple people to one client record (e.g. a buyer couple). Add `coName` and `coEmail` optional fields to the `clients` table. Show both names on client card and document dashboard. Both names available as AI pre-fill targets.
## Notes
Real estate standard: primary buyer + co-buyer on the same contract. Simple schema addition — new Drizzle migration, update ClientCard UI, update seed data.
## Scope
- New Drizzle migration: add `coName` (text, nullable) and `coEmail` (text, nullable) to `clients` table
- Update `ClientCard` component to display co-buyer name when present
- Update document dashboard to show both names
- Expose both names as AI field pre-fill targets (e.g. `co_buyer_name`, `co_buyer_email`)
- Update seed data to include at least one couple example

View File

@@ -1,21 +1,37 @@
# ============================================================
# REQUIRED — copy this file to .env.production and fill in all values
# Generate random secrets with: openssl rand -base64 32
# ============================================================
# Database # Database
DATABASE_URL=postgresql://user:password@host:5432/dbname?sslmode=require # For self-hosted docker-compose: use the internal service name "db"
# Example: postgresql://postgres:STRONG_PASSWORD@db:5432/teressa
# (no ?sslmode=require needed for local docker network)
DATABASE_URL=postgresql://postgres:CHANGE_ME@db:5432/teressa
# Authentication # Authentication secrets (generate with: openssl rand -base64 32)
SIGNING_JWT_SECRET=your-jwt-secret-here SIGNING_JWT_SECRET=CHANGE_ME
AUTH_SECRET=your-auth-secret-here AUTH_SECRET=CHANGE_ME
AGENT_EMAIL=agent@example.com AUTH_TRUST_HOST=true
AGENT_PASSWORD=your-agent-password
# SMTP (email delivery) # Agent login credentials (what you use to log in to the portal)
AGENT_EMAIL=your@email.com
AGENT_PASSWORD=CHANGE_ME
# SMTP — email delivery for contact form and document notifications
# Resend (recommended): host=smtp.resend.com, port=465, user=resend, pass=re_xxxxxxx
# Gmail: host=smtp.gmail.com, port=587, user=your@gmail.com, pass=app-password
CONTACT_EMAIL_USER=your-smtp-username CONTACT_EMAIL_USER=your-smtp-username
CONTACT_EMAIL_PASS=your-smtp-password CONTACT_EMAIL_PASS=your-smtp-password
CONTACT_SMTP_HOST=smtp.example.com CONTACT_SMTP_HOST=smtp.example.com
CONTACT_SMTP_PORT=587 # use 587 STARTTLS; port 465 SSL may be blocked in some Docker environments CONTACT_SMTP_PORT=587
# OpenAI (AI field placement) # OpenAI — required for AI-assisted PDF field placement
OPENAI_API_KEY=sk-your-openai-key OPENAI_API_KEY=sk-your-openai-key
# Application # Application public URL (no trailing slash)
APP_BASE_URL=https://yourdomain.com APP_BASE_URL=https://yourdomain.com
AUTH_TRUST_HOST=true
# Internal postgres password — must match DATABASE_URL above
# Only used by the db: service in docker-compose.yml
POSTGRES_PASSWORD=CHANGE_ME

View File

@@ -1 +1,349 @@
@AGENTS.md # CLAUDE.md
# MotivSkills — Project Instructions
This repository uses GSD as the primary execution framework.
Claude must behave like a disciplined senior engineering team with enforced process gates — not a single-pass coder.
---
# Core Principles
1. **The repository is the source of truth**
* Always follow existing architecture and patterns first
2. **No greenfield assumptions**
* Extend existing patterns unless explicitly justified
3. **Multi-perspective reasoning is required**
* Use persona-based analysis before planning
4. **Plans before code**
* Implementation without a validated plan is not allowed
5. **Consistency is a requirement**
* Matching repo conventions is mandatory, not optional
---
# Execution Workflow (MANDATORY)
For all non-trivial work, Claude MUST follow this sequence:
1. Understand the request
2. Inspect the repository
3. Produce Architecture Summary
4. Run Multi-Agent Research
5. Produce Research Synthesis
6. Run Planning Phase
7. Produce Plan Synthesis
8. Execute
9. Verify
If any step is skipped → STOP and correct before continuing.
---
# Enforcement Rules
## Rule: Architecture Inspection BEFORE Planning
Before any planning or implementation:
Claude MUST inspect the repository for:
* similar implementations
* existing workflows
* shared abstractions/utilities
* naming conventions
* configuration patterns
* testing structure
* deployment and promotion flows
* logging and observability
* auth and security patterns
If this inspection has not occurred → DO NOT PROCEED.
---
## Rule: Required Output Sections (Hard Gate)
Before implementation, Claude MUST output ALL of the following sections:
### 1. Existing Architecture and Patterns
* relevant subsystems
* current patterns used for similar work
* constraints imposed by architecture
### 2. Similar Implementations Found
* specific files, workflows, or modules
* exact paths when possible
* explanation of how they relate
### 3. Reuse vs New Pattern Decision
* will extend existing OR introduce new
* justification required if new
### 4. Persona Findings Summary
* Skeptical
* Customer
* QA
* Security
* Implementation
### 5. Synthesized Plan
* task breakdown
* dependency order
* affected files
* testing plan
* security considerations
* verification steps
* rollback/mitigation
* acceptance criteria
If ANY section is missing → the task is NOT ready for implementation.
---
## Rule: Extend Existing Patterns by Default
If an existing pattern exists:
* it MUST be reused or extended
DO NOT:
* introduce new folder structures
* create new workflow styles
* invent new abstractions unnecessarily
---
## Rule: Deviation Must Be Justified
If introducing a new pattern, Claude MUST explain:
* existing pattern found
* why it is insufficient
* why new approach is better
* long-term consistency impact
* migration implications
No silent divergence allowed.
---
## Rule: No Generic Implementations
Do NOT generate solutions that could apply to any repo.
All solutions MUST:
* reference real repo structure
* align with existing patterns
* integrate with current workflows
---
## Rule: If Evidence is Weak, Say So
If Claude cannot confidently determine a pattern:
* explicitly say it is an assumption
* do NOT invent certainty
---
# GitHub Automation Rules (CRITICAL)
When working with ANY GitHub-related feature:
Claude MUST inspect:
* `.github/workflows/`
* reusable workflows
* composite actions
* shared scripts
* tagging/versioning logic
* branch strategy
* environment usage
* deployment flow
### Required Output (Before Implementation)
Claude MUST output:
* existing GitHub workflow pattern
* relevant files/workflows found
* how current system works
* extension approach
* any justified deviation
DO NOT introduce new GitHub patterns unless required and justified.
---
# Multi-Agent Research Council
Claude MUST simulate the following perspectives:
## Skeptical Engineer
* challenge assumptions
* find hidden complexity
* identify failure modes
## Customer-Oriented Engineer
* evaluate UX and DX
* identify friction
* define acceptance criteria
## QA Engineer
* define test strategy
* identify edge cases
* find regression risks
## Security Engineer
* evaluate auth, secrets, inputs
* identify attack surface
* define safeguards
## Implementation Engineer
* define architecture approach
* identify files and sequencing
* ensure maintainability
---
# Aggregation Rules
## Research Synthesis MUST include:
* problem summary
* current-state findings
* risks (ranked)
* assumptions vs facts
* open questions
* recommended direction
---
## Plan Synthesis MUST include:
* concrete task breakdown
* dependency order
* impacted files/components
* test plan
* security checks
* verification steps
* rollback strategy
* acceptance criteria
---
# Decision Priority Order
When tradeoffs exist:
1. correctness
2. security
3. consistency with repo
4. user impact
5. operability
6. speed
---
# Repository Consistency Requirements
All implementations MUST match:
* file structure
* naming conventions
* abstraction patterns
* workflow design
* dependency handling
* environment handling
* testing strategy
* deployment model
Consistency is mandatory.
---
# Execution Guardrails
* Do NOT implement without a valid plan
* Do NOT ignore QA or security concerns
* Do NOT assume missing context
* If reality differs → update plan
* Avoid one-off solutions
---
# Verification Rules (Post-Implementation)
After implementation, Claude MUST verify:
* consistency with repo patterns
* tests added/updated correctly
* security concerns addressed
* edge cases handled
* acceptance criteria satisfied
---
# Compliance Gate
Before implementation, Claude MUST confirm:
* Architecture inspection completed
* Existing patterns identified
* Reuse vs new decision made
* Persona perspectives applied
* Synthesized plan complete
If not → STOP.
---
# Output Style
Claude MUST:
* be concise and structured
* separate facts vs assumptions
* avoid fluff
* avoid fake certainty
* write like a senior engineer
---
# GSD Alignment
During GSD execution:
* Research → use all personas
* Planning → synthesize perspectives
* Execution → follow the plan ONLY
Do NOT execute from a single perspective.
The synthesized plan is the source of truth.
---

View File

@@ -1,63 +1,198 @@
# Deployment Guide # Deployment Guide — Self-Hosted (Unraid + Gitea)
## Prerequisites ## Overview
- Git installed on the server This app runs as two Docker containers:
- Docker and Docker Compose installed on the server - **app** — Next.js server (port 3000)
- **db** — PostgreSQL 16 (port 5432)
## Step 1: Configure environment ---
Copy the example env file and fill in real values: ## Required Secrets
All of these must be set in `.env.production` before starting.
| Variable | What it is | How to get it |
|---|---|---|
| `DATABASE_URL` | Internal postgres URL | `postgresql://postgres:POSTGRES_PASSWORD@db:5432/teressa` |
| `POSTGRES_PASSWORD` | Postgres superuser password | Generate: `openssl rand -base64 32` |
| `SIGNING_JWT_SECRET` | Signs document signing tokens | Generate: `openssl rand -base64 32` |
| `AUTH_SECRET` | NextAuth session encryption | Generate: `openssl rand -base64 32` |
| `AUTH_TRUST_HOST` | Required for reverse proxy | Set to `true` |
| `AGENT_EMAIL` | Your login email for the portal | Your email address |
| `AGENT_PASSWORD` | Your login password for the portal | Choose a strong password |
| `CONTACT_EMAIL_USER` | SMTP username | See SMTP section below |
| `CONTACT_EMAIL_PASS` | SMTP password/API key | See SMTP section below |
| `CONTACT_SMTP_HOST` | SMTP server hostname | See SMTP section below |
| `CONTACT_SMTP_PORT` | SMTP port | `587` (STARTTLS) or `465` (SSL) |
| `OPENAI_API_KEY` | OpenAI API key for AI field placement | platform.openai.com → API keys |
| `APP_BASE_URL` | Public URL of the app (no trailing slash) | e.g. `https://teressa.yourdomain.com` |
### SMTP Options
- **Resend** (recommended): host=`smtp.resend.com`, port=`465`, user=`resend`, pass=Resend API key
- **Gmail**: host=`smtp.gmail.com`, port=`587`, user=your Gmail, pass=App Password (not your regular password)
---
## Part 1: Push Repo to Gitea
### On your local machine
```bash
# Add your Gitea instance as a remote
git remote add gitea http://YOUR_UNRAID_IP:3000/YOUR_GITEA_USERNAME/teressa-copeland-homes.git
# Push
git push gitea main
```
Create the repo in Gitea first (via the Gitea web UI) before pushing.
---
## Part 2: Build and Push Docker Image to Gitea Registry
Gitea includes a built-in OCI container registry. Replace `YOUR_UNRAID_IP`, `YOUR_GITEA_USERNAME`, and `YOUR_GITEA_PORT` with your values.
```bash
# Log in to Gitea's container registry
docker login YOUR_UNRAID_IP:YOUR_GITEA_PORT
# Build the image (from the project root)
docker build -t YOUR_UNRAID_IP:YOUR_GITEA_PORT/YOUR_GITEA_USERNAME/teressa-copeland-homes:latest .
# Push to Gitea registry
docker push YOUR_UNRAID_IP:YOUR_GITEA_PORT/YOUR_GITEA_USERNAME/teressa-copeland-homes:latest
```
> **Gitea registry port** is usually the same as the Gitea web port (e.g. 3000 or 10880 depending on your setup). Check your Gitea container settings in Unraid.
> **HTTP registry**: If Gitea is HTTP (not HTTPS), add the registry to Docker's insecure registries. On Unraid, go to **Settings → Docker → Insecure Registries** and add `YOUR_UNRAID_IP:PORT`.
---
## Part 3: Deploy on Unraid
### Option A: Docker Compose (recommended)
Unraid supports docker-compose via the **Compose Manager** community app.
1. Install **Compose Manager** from Community Apps if not already installed
2. SSH into Unraid and create the app directory:
```bash
mkdir -p /mnt/user/appdata/teressa-copeland-homes
cd /mnt/user/appdata/teressa-copeland-homes
```
3. Copy `docker-compose.yml` and `.env.production.example` to this directory:
```bash
cp docker-compose.yml .env.production.example /mnt/user/appdata/teressa-copeland-homes/
```
4. Create `.env.production` from the example and fill in all values:
```bash ```bash
cp .env.production.example .env.production cp .env.production.example .env.production
nano .env.production
``` ```
5. Set `APP_IMAGE` to your Gitea registry URL in `.env.production`:
```
APP_IMAGE=YOUR_UNRAID_IP:YOUR_GITEA_PORT/YOUR_GITEA_USERNAME/teressa-copeland-homes:latest
```
6. In Compose Manager, add the compose file and start it
Edit `.env.production` and set all 11 variables: ### Option B: Unraid Docker UI (manual containers)
- `DATABASE_URL` — Neon PostgreSQL connection string Add two containers via **Docker → Add Container**:
- `SIGNING_JWT_SECRET` — secret for signing JWT tokens
- `AUTH_SECRET` — NextAuth secret
- `AGENT_EMAIL` — agent login email
- `AGENT_PASSWORD` — agent login password
- `CONTACT_EMAIL_USER` — SMTP username
- `CONTACT_EMAIL_PASS` — SMTP password
- `CONTACT_SMTP_HOST` — SMTP host (e.g. `smtp.gmail.com`)
- `CONTACT_SMTP_PORT` — SMTP port (e.g. `465`)
- `OPENAI_API_KEY` — OpenAI API key for AI field placement
- `APP_BASE_URL` — public URL of the app (e.g. `https://teressacopelandhomes.com`)
## Step 2: Run database migration **Container 1 — postgres**
- Repository: `postgres:16-alpine`
- Name: `teressa-db`
- Port: `5432:5432`
- Environment variables:
- `POSTGRES_USER=postgres`
- `POSTGRES_PASSWORD=YOUR_STRONG_PASSWORD`
- `POSTGRES_DB=teressa`
- Path: `/mnt/user/appdata/teressa-db` → `/var/lib/postgresql/data`
Run migrations from the project directory on the host (not inside Docker): **Container 2 — app**
- Repository: `YOUR_UNRAID_IP:YOUR_GITEA_PORT/YOUR_GITEA_USERNAME/teressa-copeland-homes:latest`
- Name: `teressa-app`
- Port: `3000:3000`
- Network: Same as teressa-db (so it can reach `teressa-db` by name, or use Unraid IP)
- Environment variables: All 13 from the table above
- Note: `DATABASE_URL` should use the Unraid host IP or container name instead of `db`
- Path: `/mnt/user/appdata/teressa-uploads` → `/app/uploads`
---
## Part 4: Run Migrations and Seed
These must run **after** the database container is healthy, **before** the app is usable.
### Run migrations
```bash ```bash
DATABASE_URL=<your-neon-url> npx drizzle-kit migrate # From your local machine (requires node + npx)
DATABASE_URL="postgresql://postgres:YOUR_PASSWORD@YOUR_UNRAID_IP:5432/teressa" npx drizzle-kit migrate
# Or SSH into Unraid and run from the project directory
docker exec -it teressa-app npx drizzle-kit migrate
``` ```
This must complete successfully before starting the container. Migrations are never run inside the Docker image. ### Seed agent account + form templates
## Step 3: Build and start
```bash ```bash
docker compose up -d --build # SSH into Unraid
docker exec \
-e DATABASE_URL="postgresql://postgres:YOUR_PASSWORD@db:5432/teressa" \
-e AGENT_EMAIL="your@email.com" \
-e AGENT_PASSWORD="your-password" \
teressa-app npx tsx scripts/seed.ts
# Seed the 140 real estate form templates
docker exec \
-e DATABASE_URL="postgresql://postgres:YOUR_PASSWORD@db:5432/teressa" \
teressa-app npx tsx scripts/seed-forms.ts
``` ```
## Step 4: Verify Both seed scripts are idempotent — safe to re-run.
---
## Part 5: Verify
```bash ```bash
curl http://localhost:3000/api/health curl http://YOUR_UNRAID_IP:3000/api/health
# Expected: {"ok":true,"db":"connected"}
``` ```
Expected response: `{"ok":true,"db":"connected"}` Then open `http://YOUR_UNRAID_IP:3000` in a browser and log in with `AGENT_EMAIL` / `AGENT_PASSWORD`.
---
## Updating ## Updating
To deploy a new version: When you push a new version:
```bash ```bash
git pull # On local machine — rebuild and push image
# If schema changed, re-run migration first: docker build -t YOUR_UNRAID_IP:PORT/YOUR_GITEA_USERNAME/teressa-copeland-homes:latest .
DATABASE_URL=<your-neon-url> npx drizzle-kit migrate docker push YOUR_UNRAID_IP:PORT/YOUR_GITEA_USERNAME/teressa-copeland-homes:latest
docker compose up -d --build
# On Unraid — pull new image and restart
docker pull YOUR_UNRAID_IP:PORT/YOUR_GITEA_USERNAME/teressa-copeland-homes:latest
docker restart teressa-app
# If schema changed, run migrations first:
DATABASE_URL="postgresql://postgres:YOUR_PASSWORD@YOUR_UNRAID_IP:5432/teressa" npx drizzle-kit migrate
``` ```
---
## Reverse Proxy (optional)
If you want HTTPS via a domain name, put a reverse proxy in front on port 443:
- **Nginx Proxy Manager** (available in Unraid Community Apps) — easiest GUI option
- **Caddy** — automatic HTTPS with Let's Encrypt
- **Traefik** — label-based routing
Point the proxy at `http://YOUR_UNRAID_IP:3000`. Make sure `APP_BASE_URL` in `.env.production` matches the public HTTPS URL, and ensure `AUTH_TRUST_HOST=true` is set.

View File

@@ -3,10 +3,10 @@ services:
image: postgres:16-alpine image: postgres:16-alpine
restart: unless-stopped restart: unless-stopped
ports: ports:
- "5433:5432" - "5432:5432"
environment: environment:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: teressa POSTGRES_DB: teressa
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
@@ -17,12 +17,10 @@ services:
retries: 5 retries: 5
app: app:
build: image: ${APP_IMAGE:-teressa-copeland-homes:latest}
context: .
dockerfile: Dockerfile
restart: unless-stopped restart: unless-stopped
ports: ports:
- "3001:3000" - "3000:3000"
env_file: env_file:
- .env.production - .env.production
environment: environment:

View File

@@ -0,0 +1,496 @@
{
"id": "12f39205-4988-4d36-93bb-bed72096c5c0",
"prevId": "9f37640a-48c4-4da2-8c18-a212eaf5b78c",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.audit_events": {
"name": "audit_events",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"document_id": {
"name": "document_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"event_type": {
"name": "event_type",
"type": "audit_event_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"audit_events_document_id_documents_id_fk": {
"name": "audit_events_document_id_documents_id_fk",
"tableFrom": "audit_events",
"tableTo": "documents",
"columnsFrom": [
"document_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.clients": {
"name": "clients",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"contacts": {
"name": "contacts",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"property_address": {
"name": "property_address",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.documents": {
"name": "documents",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"client_id": {
"name": "client_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "document_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'Draft'"
},
"sent_at": {
"name": "sent_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"form_template_id": {
"name": "form_template_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"file_path": {
"name": "file_path",
"type": "text",
"primaryKey": false,
"notNull": false
},
"signature_fields": {
"name": "signature_fields",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"text_fill_data": {
"name": "text_fill_data",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"assigned_client_id": {
"name": "assigned_client_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"prepared_file_path": {
"name": "prepared_file_path",
"type": "text",
"primaryKey": false,
"notNull": false
},
"email_addresses": {
"name": "email_addresses",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"signed_file_path": {
"name": "signed_file_path",
"type": "text",
"primaryKey": false,
"notNull": false
},
"pdf_hash": {
"name": "pdf_hash",
"type": "text",
"primaryKey": false,
"notNull": false
},
"signed_at": {
"name": "signed_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"signers": {
"name": "signers",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"completion_triggered_at": {
"name": "completion_triggered_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"documents_client_id_clients_id_fk": {
"name": "documents_client_id_clients_id_fk",
"tableFrom": "documents",
"tableTo": "clients",
"columnsFrom": [
"client_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"documents_form_template_id_form_templates_id_fk": {
"name": "documents_form_template_id_form_templates_id_fk",
"tableFrom": "documents",
"tableTo": "form_templates",
"columnsFrom": [
"form_template_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.form_templates": {
"name": "form_templates",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"filename": {
"name": "filename",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"form_templates_filename_unique": {
"name": "form_templates_filename_unique",
"nullsNotDistinct": false,
"columns": [
"filename"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.signing_tokens": {
"name": "signing_tokens",
"schema": "",
"columns": {
"jti": {
"name": "jti",
"type": "text",
"primaryKey": true,
"notNull": true
},
"document_id": {
"name": "document_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"signer_email": {
"name": "signer_email",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"used_at": {
"name": "used_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"signing_tokens_document_id_documents_id_fk": {
"name": "signing_tokens_document_id_documents_id_fk",
"tableFrom": "signing_tokens",
"tableTo": "documents",
"columnsFrom": [
"document_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"password_hash": {
"name": "password_hash",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"agent_signature_data": {
"name": "agent_signature_data",
"type": "text",
"primaryKey": false,
"notNull": false
},
"agent_initials_data": {
"name": "agent_initials_data",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.audit_event_type": {
"name": "audit_event_type",
"schema": "public",
"values": [
"document_prepared",
"email_sent",
"link_opened",
"document_viewed",
"signature_submitted",
"pdf_hash_computed"
]
},
"public.document_status": {
"name": "document_status",
"schema": "public",
"values": [
"Draft",
"Sent",
"Viewed",
"Signed"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,567 @@
{
"id": "b839b2b9-21ad-4214-a72a-c0bfebeaa7a1",
"prevId": "12f39205-4988-4d36-93bb-bed72096c5c0",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.audit_events": {
"name": "audit_events",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"document_id": {
"name": "document_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"event_type": {
"name": "event_type",
"type": "audit_event_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"audit_events_document_id_documents_id_fk": {
"name": "audit_events_document_id_documents_id_fk",
"tableFrom": "audit_events",
"tableTo": "documents",
"columnsFrom": [
"document_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.clients": {
"name": "clients",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"contacts": {
"name": "contacts",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"property_address": {
"name": "property_address",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.document_templates": {
"name": "document_templates",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"form_template_id": {
"name": "form_template_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"signature_fields": {
"name": "signature_fields",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"archived_at": {
"name": "archived_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"document_templates_form_template_id_form_templates_id_fk": {
"name": "document_templates_form_template_id_form_templates_id_fk",
"tableFrom": "document_templates",
"tableTo": "form_templates",
"columnsFrom": [
"form_template_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.documents": {
"name": "documents",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"client_id": {
"name": "client_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "document_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'Draft'"
},
"sent_at": {
"name": "sent_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"form_template_id": {
"name": "form_template_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"file_path": {
"name": "file_path",
"type": "text",
"primaryKey": false,
"notNull": false
},
"signature_fields": {
"name": "signature_fields",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"text_fill_data": {
"name": "text_fill_data",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"assigned_client_id": {
"name": "assigned_client_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"prepared_file_path": {
"name": "prepared_file_path",
"type": "text",
"primaryKey": false,
"notNull": false
},
"email_addresses": {
"name": "email_addresses",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"signed_file_path": {
"name": "signed_file_path",
"type": "text",
"primaryKey": false,
"notNull": false
},
"pdf_hash": {
"name": "pdf_hash",
"type": "text",
"primaryKey": false,
"notNull": false
},
"signed_at": {
"name": "signed_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"signers": {
"name": "signers",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"completion_triggered_at": {
"name": "completion_triggered_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"documents_client_id_clients_id_fk": {
"name": "documents_client_id_clients_id_fk",
"tableFrom": "documents",
"tableTo": "clients",
"columnsFrom": [
"client_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"documents_form_template_id_form_templates_id_fk": {
"name": "documents_form_template_id_form_templates_id_fk",
"tableFrom": "documents",
"tableTo": "form_templates",
"columnsFrom": [
"form_template_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.form_templates": {
"name": "form_templates",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"filename": {
"name": "filename",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"form_templates_filename_unique": {
"name": "form_templates_filename_unique",
"nullsNotDistinct": false,
"columns": [
"filename"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.signing_tokens": {
"name": "signing_tokens",
"schema": "",
"columns": {
"jti": {
"name": "jti",
"type": "text",
"primaryKey": true,
"notNull": true
},
"document_id": {
"name": "document_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"signer_email": {
"name": "signer_email",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"used_at": {
"name": "used_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"signing_tokens_document_id_documents_id_fk": {
"name": "signing_tokens_document_id_documents_id_fk",
"tableFrom": "signing_tokens",
"tableTo": "documents",
"columnsFrom": [
"document_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"password_hash": {
"name": "password_hash",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"agent_signature_data": {
"name": "agent_signature_data",
"type": "text",
"primaryKey": false,
"notNull": false
},
"agent_initials_data": {
"name": "agent_initials_data",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.audit_event_type": {
"name": "audit_event_type",
"schema": "public",
"values": [
"document_prepared",
"email_sent",
"link_opened",
"document_viewed",
"signature_submitted",
"pdf_hash_computed"
]
},
"public.document_status": {
"name": "document_status",
"schema": "public",
"values": [
"Draft",
"Sent",
"Viewed",
"Signed"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -78,6 +78,20 @@
"when": 1775250937545, "when": 1775250937545,
"tag": "0010_sharp_archangel", "tag": "0010_sharp_archangel",
"breakpoints": true "breakpoints": true
},
{
"idx": 11,
"version": "7",
"when": 1775259146259,
"tag": "0011_common_mystique",
"breakpoints": true
},
{
"idx": 12,
"version": "7",
"when": 1775499305623,
"tag": "0012_ancient_blue_shield",
"breakpoints": true
} }
] ]
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -13,6 +13,8 @@
import { chromium } from 'playwright'; import { chromium } from 'playwright';
import * as fs from 'node:fs/promises'; import * as fs from 'node:fs/promises';
import * as path from 'node:path'; import * as path from 'node:path';
import { tmpdir } from 'node:os';
import AdmZip from 'adm-zip';
import { config } from 'dotenv'; import { config } from 'dotenv';
config({ path: path.resolve(process.cwd(), '.env.local') }); config({ path: path.resolve(process.cwd(), '.env.local') });
@@ -221,6 +223,17 @@ async function handleNRDSAuth(page: import('playwright').Page) {
} }
} }
function extractFormNames(bodyText: string): string[] {
const lines = bodyText.split('\n').map(l => l.trim()).filter(l => l.length > 0);
const formNames: string[] = [];
for (let i = 0; i < lines.length; i++) {
if (lines[i] === 'Add' && i > 0 && lines[i - 1] !== 'Add' && lines[i - 1].length > 3) {
formNames.push(lines[i - 1]);
}
}
return formNames;
}
async function downloadFormsInView( async function downloadFormsInView(
page: import('playwright').Page, page: import('playwright').Page,
context: import('playwright').BrowserContext, context: import('playwright').BrowserContext,
@@ -228,19 +241,35 @@ async function downloadFormsInView(
downloaded: string[], downloaded: string[],
failed: string[] failed: string[]
) { ) {
// Flow: click form name → preview opens → click Download button → save file // Flow: scroll to load all forms, then click form name → preview Download button → save
// Extract form names from the page body text — the list renders as "Name\nAdd\nName\nAdd..." // Scroll down repeatedly to trigger infinite scroll, collecting all form names
const allNames = new Set<string>();
let prevCount = 0;
let stallRounds = 0;
while (stallRounds < 5) {
// Scroll both window and any inner scrollable container to handle virtualized lists
await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight);
const scrollable = document.querySelector('[class*="scroll"], [class*="list"], main, [role="main"], .overflow-auto, .overflow-y-auto');
if (scrollable) scrollable.scrollTop = scrollable.scrollHeight;
});
await page.waitForTimeout(2000);
const bodyText = await page.locator('body').innerText().catch(() => ''); const bodyText = await page.locator('body').innerText().catch(() => '');
const lines = bodyText.split('\n').map(l => l.trim()).filter(l => l.length > 3); for (const n of extractFormNames(bodyText)) allNames.add(n);
const formNames: string[] = []; if (allNames.size === prevCount) {
for (let i = 0; i < lines.length; i++) { stallRounds++;
if (lines[i] === 'Add' && i > 0 && lines[i - 1] !== 'Add' && lines[i - 1].length > 3) { } else {
formNames.push(lines[i - 1]); stallRounds = 0;
prevCount = allNames.size;
process.stdout.write(` Loaded ${allNames.size} forms so far...\n`);
} }
} }
const names = [...new Set(formNames)]; // Scroll back to top before clicking
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(500);
const names = [...allNames];
console.log(` Found ${names.length} forms to download`); console.log(` Found ${names.length} forms to download`);
if (names.length === 0) { if (names.length === 0) {
await page.screenshot({ path: `scripts/debug-no-forms-${Date.now()}.png` }); await page.screenshot({ path: `scripts/debug-no-forms-${Date.now()}.png` });
@@ -285,7 +314,9 @@ async function downloadFormsInView(
page.waitForEvent('download', { timeout: 20_000 }), page.waitForEvent('download', { timeout: 20_000 }),
downloadBtn.click(), downloadBtn.click(),
]); ]);
await download.saveAs(destPath); const tmpPath = path.join(tmpdir(), `skyslope-${Date.now()}.tmp`);
await download.saveAs(tmpPath);
await savePdf(tmpPath, destPath);
process.stdout.write(`${sanitized}.pdf\n`); process.stdout.write(`${sanitized}.pdf\n`);
downloaded.push(sanitized); downloaded.push(sanitized);
} catch (err) { } catch (err) {
@@ -379,6 +410,21 @@ async function interceptPdfOnClick(
}); });
} }
/** If the downloaded file is a ZIP, extract the first PDF inside; otherwise move as-is. */
async function savePdf(tmpPath: string, destPath: string) {
const buf = await fs.readFile(tmpPath);
const isPk = buf[0] === 0x50 && buf[1] === 0x4b; // PK magic bytes = ZIP
if (isPk) {
const zip = new AdmZip(buf);
const entry = zip.getEntries().find(e => e.entryName.toLowerCase().endsWith('.pdf'));
if (!entry) throw new Error('ZIP contained no PDF entry');
await fs.writeFile(destPath, entry.getData());
} else {
await fs.rename(tmpPath, destPath);
}
await fs.unlink(tmpPath).catch(() => {}); // clean up tmp if rename didn't move it
}
main().catch(err => { main().catch(err => {
console.error('Fatal:', err.message); console.error('Fatal:', err.message);
process.exit(1); process.exit(1);

View File

@@ -1,9 +1,8 @@
import "dotenv/config"; import "dotenv/config";
import { drizzle } from "drizzle-orm/postgres-js"; import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres"; import postgres from "postgres";
import { users, clients, documents } from "../src/lib/db/schema"; import { users } from "../src/lib/db/schema";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import { inArray } from "drizzle-orm";
const client = postgres(process.env.DATABASE_URL!); const client = postgres(process.env.DATABASE_URL!);
const db = drizzle({ client }); const db = drizzle({ client });
@@ -22,35 +21,6 @@ async function seed() {
console.log(`Seeded agent account: ${email}`); console.log(`Seeded agent account: ${email}`);
// Seed clients
await db.insert(clients).values([
{ name: "Sarah Johnson", email: "sarah.j@example.com" },
{ name: "Mike Torres", email: "m.torres@example.com" },
]).onConflictDoNothing();
console.log("Seeded clients: Sarah Johnson, Mike Torres");
// Query back seeded clients to get their IDs
const [sarah, mike] = await db
.select({ id: clients.id })
.from(clients)
.where(inArray(clients.email, ["sarah.j@example.com", "m.torres@example.com"]))
.orderBy(clients.createdAt);
// Seed placeholder documents
if (sarah && mike) {
await db.insert(documents).values([
{ name: "Purchase Agreement - 842 Maple Dr", clientId: sarah.id, status: "Signed", sentAt: new Date("2026-02-15") },
{ name: "Seller Disclosure - 842 Maple Dr", clientId: sarah.id, status: "Signed", sentAt: new Date("2026-02-14") },
{ name: "Buyer Rep Agreement", clientId: mike.id, status: "Sent", sentAt: new Date("2026-03-10") },
{ name: "Purchase Agreement - 1205 Oak Ave", clientId: mike.id, status: "Draft", sentAt: null },
]).onConflictDoNothing();
console.log("Seeded 4 placeholder documents");
} else {
console.log("Skipping documents seed — clients not found");
}
process.exit(0); process.exit(0);
} }

Some files were not shown because too many files have changed in this diff Show More