initial install
@@ -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_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
|
||||
SIGNING_JWT_SECRET=your-jwt-secret-here
|
||||
AUTH_SECRET=your-auth-secret-here
|
||||
AGENT_EMAIL=agent@example.com
|
||||
AGENT_PASSWORD=your-agent-password
|
||||
# Authentication secrets (generate with: openssl rand -base64 32)
|
||||
SIGNING_JWT_SECRET=CHANGE_ME
|
||||
AUTH_SECRET=CHANGE_ME
|
||||
AUTH_TRUST_HOST=true
|
||||
|
||||
# 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_PASS=your-smtp-password
|
||||
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
|
||||
|
||||
# Application
|
||||
# Application public URL (no trailing slash)
|
||||
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
|
||||
- Docker and Docker Compose installed on the server
|
||||
This app runs as two Docker containers:
|
||||
- **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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
To deploy a new version:
|
||||
When you push a new version:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
# If schema changed, re-run migration first:
|
||||
DATABASE_URL=<your-neon-url> npx drizzle-kit migrate
|
||||
docker compose up -d --build
|
||||
# On local machine — rebuild and push image
|
||||
docker build -t YOUR_UNRAID_IP:PORT/YOUR_GITEA_USERNAME/teressa-copeland-homes:latest .
|
||||
docker push YOUR_UNRAID_IP:PORT/YOUR_GITEA_USERNAME/teressa-copeland-homes:latest
|
||||
|
||||
# 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
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5433:5432"
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: teressa
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
@@ -17,12 +17,10 @@ services:
|
||||
retries: 5
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: ${APP_IMAGE:-teressa-copeland-homes:latest}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3001:3000"
|
||||
- "3000:3000"
|
||||
env_file:
|
||||
- .env.production
|
||||
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,
|
||||
"tag": "0010_sharp_archangel",
|
||||
"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 * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import AdmZip from 'adm-zip';
|
||||
import { config } from 'dotenv';
|
||||
|
||||
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(
|
||||
page: import('playwright').Page,
|
||||
context: import('playwright').BrowserContext,
|
||||
@@ -228,19 +241,35 @@ async function downloadFormsInView(
|
||||
downloaded: 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..."
|
||||
const bodyText = await page.locator('body').innerText().catch(() => '');
|
||||
const lines = bodyText.split('\n').map(l => l.trim()).filter(l => l.length > 3);
|
||||
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]);
|
||||
// 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(() => '');
|
||||
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`);
|
||||
if (names.length === 0) {
|
||||
await page.screenshot({ path: `scripts/debug-no-forms-${Date.now()}.png` });
|
||||
@@ -285,7 +314,9 @@ async function downloadFormsInView(
|
||||
page.waitForEvent('download', { timeout: 20_000 }),
|
||||
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`);
|
||||
downloaded.push(sanitized);
|
||||
} 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 => {
|
||||
console.error('Fatal:', err.message);
|
||||
process.exit(1);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import "dotenv/config";
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
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 { inArray } from "drizzle-orm";
|
||||
|
||||
const client = postgres(process.env.DATABASE_URL!);
|
||||
const db = drizzle({ client });
|
||||
@@ -22,35 +21,6 @@ async function seed() {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||