Compare commits
10 Commits
64e5f730df
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9117dc4c02 | ||
|
|
71cef4d7b6 | ||
|
|
7621007a60 | ||
|
|
81a39c8e59 | ||
|
|
eec0bd91c9 | ||
|
|
78d579f965 | ||
|
|
2947fa558c | ||
|
|
bdf0cb02ff | ||
|
|
af5beaf5cb | ||
|
|
9081342e1b |
42
.agents/skills/frontend-design/SKILL.md
Normal 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.
|
||||||
1
.claude/worktrees/agent-a42b65af
Submodule
1
.claude/worktrees/agent-a4e62c7a
Submodule
1
.claude/worktrees/agent-a66cf8e5
Submodule
1
.claude/worktrees/agent-afd3aaec
Submodule
@@ -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 |
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
118
.planning/phases/03-agent-portal-shell/03-01-SUMMARY.md
Normal 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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
422
.planning/phases/20-apply-template-and-portal-nav/20-01-PLAN.md
Normal 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'} · {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>
|
||||||
@@ -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
|
||||||
258
.planning/phases/20-apply-template-and-portal-nav/20-02-PLAN.md
Normal 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>
|
||||||
@@ -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*
|
||||||
@@ -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
|
||||||
@@ -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 117–140: 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 80–89: `selectedDocTemplate` branch sends `documentTemplateId` to POST /api/documents; route.ts lines 59–116: 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 78–99: 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 143–165: `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, 50–55, 101–103, 135: fields state fetched from `/api/documents/${docId}/fields`, `selectedFieldHint` derived from `fields.find`, passed as prop; `PreparePanel.tsx` lines 98, 116, 351–359: 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 126–164: 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 81–89: 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 60–63: fetches template with `formTemplate` relation |
|
||||||
|
| POST /api/documents | `clients` table | `db.query.clients.findFirst({ where: eq(clients.id, clientId) })` | WIRED | Lines 88–94: 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 352–355: 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 72–75: `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 78–99. 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 351–359: "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)_
|
||||||
24
.planning/todos/co-buyer-couple-support.md
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -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
|
```bash
|
||||||
cp .env.production.example .env.production
|
# 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
|
||||||
```
|
```
|
||||||
|
|
||||||
Edit `.env.production` and set all 11 variables:
|
Create the repo in Gitea first (via the Gitea web UI) before pushing.
|
||||||
|
|
||||||
- `DATABASE_URL` — Neon PostgreSQL connection string
|
---
|
||||||
- `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
|
## Part 2: Build and Push Docker Image to Gitea Registry
|
||||||
|
|
||||||
Run migrations from the project directory on the host (not inside Docker):
|
Gitea includes a built-in OCI container registry. Replace `YOUR_UNRAID_IP`, `YOUR_GITEA_USERNAME`, and `YOUR_GITEA_PORT` with your values.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
DATABASE_URL=<your-neon-url> npx drizzle-kit migrate
|
# 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
|
||||||
```
|
```
|
||||||
|
|
||||||
This must complete successfully before starting the container. Migrations are never run inside the Docker image.
|
> **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.
|
||||||
|
|
||||||
## Step 3: Build and start
|
> **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
|
||||||
|
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
|
||||||
|
|
||||||
|
### Option B: Unraid Docker UI (manual containers)
|
||||||
|
|
||||||
|
Add two containers via **Docker → Add Container**:
|
||||||
|
|
||||||
|
**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`
|
||||||
|
|
||||||
|
**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
|
||||||
docker compose up -d --build
|
# 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
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 4: Verify
|
### Seed agent account + form templates
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:3000/api/health
|
# 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
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected response: `{"ok":true,"db":"connected"}`
|
Both seed scripts are idempotent — safe to re-run.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 5: Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://YOUR_UNRAID_IP:3000/api/health
|
||||||
|
# Expected: {"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.
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
496
teressa-copeland-homes/drizzle/meta/0011_snapshot.json
Normal 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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
567
teressa-copeland-homes/drizzle/meta/0012_snapshot.json
Normal 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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
BIN
teressa-copeland-homes/scripts/debug-no-forms-1773983391865.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 142 KiB |
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||