initial install
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
@@ -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)
|
||||||
|
|||||||
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 bodyText = await page.locator('body').innerText().catch(() => '');
|
const allNames = new Set<string>();
|
||||||
const lines = bodyText.split('\n').map(l => l.trim()).filter(l => l.length > 3);
|
let prevCount = 0;
|
||||||
const formNames: string[] = [];
|
let stallRounds = 0;
|
||||||
for (let i = 0; i < lines.length; i++) {
|
while (stallRounds < 5) {
|
||||||
if (lines[i] === 'Add' && i > 0 && lines[i - 1] !== 'Add' && lines[i - 1].length > 3) {
|
// Scroll both window and any inner scrollable container to handle virtualized lists
|
||||||
formNames.push(lines[i - 1]);
|
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(() => '');
|
||||||
|
for (const n of extractFormNames(bodyText)) allNames.add(n);
|
||||||
|
if (allNames.size === prevCount) {
|
||||||
|
stallRounds++;
|
||||||
|
} else {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||