diff --git a/HEARTBEAT.md b/HEARTBEAT.md
index 76a82cc..9719c17 100644
--- a/HEARTBEAT.md
+++ b/HEARTBEAT.md
@@ -1,26 +1,37 @@
# HEARTBEAT.md — Current Focus
## Now
-- **Upwork email pipeline LIVE** — cron every 5 min (8AM-11PM ET), checking Gmail for alerts, auto-scoring, auto-applying
-- Rate filter: $50/hr default, $25+/hr exception for legit clients (5.0★, $2K+ spent)
-- Pipeline processed ~6 emails today, 0 qualified yet (all below threshold or already processed)
-- Mastermind meeting tonight ~10:15PM — may produce follow-up items
+- **Upwork email pipeline LIVE** — cron every 5 min (24/7), Gmail Pub/Sub push trigger is primary, polling is fallback
+- **Gmail watch active** — expires ~Feb 24, auto-renewed every 3 days via cron
+- Rate filter: $50/hr minimum on all proposals (Jake directive)
+- **19 connects remaining** — be selective, high-value applications only
+
+## Active Applications (Awaiting Response)
+- **Claude Code + MCP + n8n Automation Coach** — $85/hr, $216K client, 4.99★, submitted ~9PM Feb 17
+- **GovGPT Senior Python Backend** — $65/hr, $97K client, 4.65★, contract-to-hire, submitted ~10:21PM Feb 17
+- **OpenClaw Workflow Consultant** — applied previously
## Next
-- Monitor pipeline for first qualified auto-apply
-- Follow up on Robert Hartline / CallProof meeting (still pending from last week)
-- Jake needs to manually submit 2 proposals: Fractional Claude Code + OpenClaw consultant
-- Consider upgrade to push-based Gmail trigger (Pub/Sub → CF Worker) if latency is an issue
-- Jacob's OAuth skill idea needs Jake's review
+- Monitor responses on the 2 applications submitted today
+- Jake needs to manually apply to: Agentic Frameworks + OpenClaw Consultant (92 score, US-only, 25 connects)
+- Follow up on Robert Hartline / CallProof meeting (still pending)
+- CRESyncFlow: wire Anthropic real OAuth into provider modal, Reonomy scraper fix (type --slowly for React inputs)
+- Ecomm portfolio v2 live at ecomport.mcpengage.com — may need Jake's review
+- Contractor proposal demo needs redeployment if Jake wants it
+- Twilio + CloseBot combined app — check Jake's laptop
## Blocked
- **GitHub shadow banned** — MCP factory paused until resolved
- **OSKV coaching paused** — awaiting Jake's decision on Oliver & Kevin silence (since Feb 12)
-- **dec-004** (MCP registry listing approval) — zero reactions after 5+ days, 6 reminders sent
+- **dec-004** (MCP registry listing approval) — zero reactions, Jake was briefed on what it means
## Key Infra
- Portfolio: https://portfolio.mcpengage.com (Cloudflare Workers)
+- Ecomm Portfolio: https://ecomport.mcpengage.com (Cloudflare Workers)
- NicheQuiz: thenichequiz.com (permanent CF tunnel)
-- Upwork email pipeline cron: `2205ac65` (every 5 min, 8AM-11PM ET)
+- CRESyncFlow: `/tmp/CRESyncFlow` (port 8900)
+- Upwork email pipeline cron: `2205ac65` (every 5 min, 24/7)
- Upwork deep scan cron: `116d2c44` (4x daily at 8,12,16,20)
-- Connects: ~107-122 remaining
+- Gmail Pub/Sub daemon: `com.clawdbot.gmail-pubsub-daemon` (launchd)
+- Gmail watch renewal cron: `gmail-watch-renewal` (every 3 days at 6AM)
+- Connects: ~19 remaining
diff --git a/SOUL.md b/SOUL.md
index d54817e..507d251 100644
--- a/SOUL.md
+++ b/SOUL.md
@@ -35,6 +35,7 @@
## Boundaries
- Confirm before spending money. Warn before breaking things.
+- **Open Claw:** Never call myself the owner — I'm a contributor. Jake is the creator/owner.
## Speed
- Don't narrate routine tool calls — just do them.
diff --git a/a2p-wizard-rebuild/ARCHITECTURE.md b/a2p-wizard-rebuild/ARCHITECTURE.md
new file mode 100644
index 0000000..b9df684
--- /dev/null
+++ b/a2p-wizard-rebuild/ARCHITECTURE.md
@@ -0,0 +1,333 @@
+# A2P Wizard - Complete Backend Architecture & Rebuild Guide
+
+## What A2P Wizard Does (TL;DR)
+
+A user fills out a simple form with basic business info. The backend:
+1. **Generates a fully compliant landing page/website** branded to the client
+2. **Creates screenshots** of the opt-in process on that website
+3. **Produces a complete A2P 10DLC compliance packet** (descriptions, sample messages, opt-in flow documentation, screenshots)
+4. **Outputs white-labeled documentation** the agency can copy/paste into their TCR campaign registration (via HighLevel, Twilio, etc.)
+
+---
+
+## PHASE 1: Form Intake
+
+### Input Fields (from the live form at `/automated-setup-trial`)
+
+| Field | Purpose |
+|-------|---------|
+| Your Name (Agency or Owner) | Agency contact — NOT the end client |
+| Your Email | Agency email for delivery |
+| Client LEGAL Business Name | Must match EIN exactly, no special chars |
+| Client Business ADDRESS | Must match EIN paperwork |
+| Client Support Email | Should match brand registration email |
+| Client Business Phone | Client's contact number |
+| Business Description (1-2 sentences) | Used to generate website content + campaign description |
+| Logo Upload | Client logo (max 400px) for website branding |
+| TCPA Compliance Checkbox | Legal CYA — confirms client meets requirements |
+
+### Form Backend
+- **reCAPTCHA v3** on the form (Google)
+- Form submission triggers the automation pipeline (likely via webhook to n8n or similar)
+
+---
+
+## PHASE 2: Content Generation (AI + Automation)
+
+This is the core engine. Based on the "Domains to Dollars" approach by Getting Automated:
+
+### 2A. AI Content Generation
+
+**Input:** Business name, description, address, phone, email, logo
+**Engine:** LLM (Claude/GPT) via n8n workflow or direct API call
+
+**AI generates all of the following from the business description:**
+
+#### For the Landing Page/Website:
+- **Hero section:** Title, subtitle, CTA button text
+- **Business description:** Expanded from the 1-2 sentence input
+- **Pain points section:** 3-4 industry-specific pain points the business solves
+- **How It Works:** 3-step process description
+- **FAQ section:** 4-6 relevant Q&As about the business
+- **Testimonials/Social proof:** Generic but industry-appropriate
+- **Footer text:** Copyright + business name
+
+#### For the A2P Compliance Packet:
+- **Campaign Description:** Who sends messages, who receives them, why (TCR requirement)
+- **Sample Messages (2-3):**
+ - Appointment reminder with `[brackets]` for templated fields
+ - Promotional/marketing message with brand name
+ - At least 1 with opt-out language ("Reply STOP to unsubscribe")
+- **Opt-in Flow Description:** How end-users consent (website form, in-person, etc.)
+- **Opt-in Confirmation Message:** Under 160 chars, includes brand name, HELP, STOP, frequency, rates
+- **Opt-out Confirmation Message:** Brand name + confirmation no more messages
+- **Help Message:** Brand name + support contact info
+- **Use Case Description:** Maps to TCR campaign types (Mixed, Marketing, etc.)
+
+### 2B. JSON Config Generation
+
+All generated content gets structured into a `config.json`:
+
+```json
+{
+ "businessName": "Naples Cleaning LLC",
+ "businessAddress": "123 Main St, Naples, FL 34102",
+ "businessPhone": "(239) 555-0123",
+ "businessEmail": "support@naplescleaning.com",
+ "logoUrl": "https://cdn.a2pwizard.com/logos/abc123.png",
+
+ "header": "Naples Cleaning LLC",
+ "icon": "broom",
+ "title": "Professional Cleaning Services in Naples, FL",
+ "description": "Naples Cleaning LLC provides reliable residential and commercial cleaning...",
+ "buttonText": "Book Your Cleaning Today",
+ "heroButtonLink": "#contact",
+
+ "painpointsTitle": "Why Choose Professional Cleaning?",
+ "painpoints": [
+ "Time-Consuming DIY Cleaning: Spend your weekends enjoying life...",
+ "Inconsistent Results: Professional-grade equipment and training...",
+ "Health Concerns: Deep cleaning eliminates allergens..."
+ ],
+
+ "howItWorksTitle": "How It Works",
+ "howItWorksSteps": [
+ "Step 1: Book online or call us",
+ "Step 2: We arrive and clean",
+ "Step 3: Enjoy your spotless space"
+ ],
+
+ "faqTitle": "Frequently Asked Questions",
+ "faqItems": [
+ {"question": "What areas do you serve?", "answer": "We serve Naples and surrounding areas..."},
+ {"question": "Are you insured?", "answer": "Yes, we are fully licensed and insured..."}
+ ],
+
+ "footerText": "© 2026 Naples Cleaning LLC. All rights reserved.",
+
+ "optInForm": {
+ "enabled": true,
+ "consentText": "I consent to receive SMS notifications and alerts from Naples Cleaning LLC. Message frequency varies. Message & data rates may apply. Text HELP to (239) 555-0123 for assistance. Reply STOP to unsubscribe at any time.",
+ "privacyPolicyUrl": "/privacy-policy",
+ "termsUrl": "/terms-of-service"
+ },
+
+ "privacyPolicy": "...generated full privacy policy text...",
+ "termsOfService": "...generated full terms of service text...",
+
+ "a2pPacket": {
+ "campaignDescription": "Messages are sent by Naples Cleaning LLC to customers who have opted in via the company website. Messages include appointment confirmations, reminders, and occasional promotional offers about cleaning services in the Naples, FL area.",
+ "sampleMessages": [
+ "Naples Cleaning LLC: Your cleaning appointment is confirmed for [date] at [time]. Reply STOP to opt out.",
+ "Naples Cleaning LLC: Hi [name]! We have a special offer this month - 20% off deep cleaning services. Book at naplescleaning.com. Msg&data rates apply. Reply STOP to unsubscribe.",
+ "Naples Cleaning LLC: Reminder - your scheduled cleaning is tomorrow at [time]. Questions? Call (239) 555-0123. Reply HELP for help, STOP to opt out."
+ ],
+ "optInFlowDescription": "End users opt in by visiting the Naples Cleaning LLC website and filling out the contact form. Users must check a non-pre-selected checkbox to consent to receiving SMS messages. The opt-in form includes disclosure language about message frequency, data rates, and opt-out instructions. Privacy Policy and Terms of Service links are provided at the bottom of the form. Website URL: https://naplescleaning.a2pwizard.com/contact",
+ "optInConfirmation": "You're now subscribed to Naples Cleaning LLC updates. Msg frequency varies. Msg&data rates may apply. Reply HELP for help, STOP to cancel.",
+ "optOutConfirmation": "You have been unsubscribed from Naples Cleaning LLC. You will not receive any more messages from this number.",
+ "helpMessage": "Naples Cleaning LLC: For help, visit naplescleaning.com or call (239) 555-0123. Reply STOP to opt out.",
+ "optInKeywords": "START, OPTIN, UNSTOP, IN",
+ "optOutKeywords": "STOP, UNSUBSCRIBE, END, QUIT, CANCEL",
+ "helpKeywords": "HELP, INFO"
+ }
+}
+```
+
+---
+
+## PHASE 3: Website Generation
+
+### Tech Stack (from the GitHub repo: Getting-Automated/landing-page-generator)
+
+**React app** with these pre-built components:
+- `HeaderBar` — Business name + icon
+- `LandingHeader` — Hero section with CTA, reviews, optional video
+- `IndustryPainpoints` — Pain points + optional lead capture form (Tally)
+- `HowItWorks` — 3-step process
+- `SocialValidation` — Testimonials
+- `FAQSection` — Expandable FAQ
+- `SecondCTA` — Final call to action
+- `FooterBar` — Copyright + links
+
+**CRITICAL A2P COMPLIANCE PAGES** (these are what make the magic happen):
+
+1. **Contact/Opt-in Page** — Form with:
+ - Name, email, phone fields
+ - **Non-pre-selected SMS consent checkbox** with full disclosure text
+ - Separate marketing consent checkbox (optional)
+ - Privacy Policy link in footer
+ - Terms of Service link in footer
+
+2. **Privacy Policy page** (`/privacy-policy`) — Auto-generated, includes:
+ - Company name and contact info
+ - What data is collected
+ - **CRITICAL:** "No mobile information will be shared with third parties/affiliates for marketing/promotional purposes"
+ - How data is used
+ - Cookie policy
+
+3. **Terms of Service page** (`/terms-of-service`) — Auto-generated, includes:
+ - SMS messaging terms
+ - Message frequency disclosure
+ - Data rates disclaimer
+ - Opt-out instructions
+ - HELP instructions
+
+### Build Process
+```
+config.json → React app reads config → npm run build → static HTML/CSS/JS
+```
+
+### Deployment
+- **AWS S3 + CloudFront** (original "Domains to Dollars" approach)
+- OR custom subdomain hosting (a2pwizard likely uses their own infra)
+- Each client gets a unique URL like `clientname.a2pwizard.com` or a custom domain
+
+---
+
+## PHASE 4: Screenshot Generation
+
+This is the **key differentiator** — screenshots are the #1 reason A2P campaigns get rejected.
+
+### What Screenshots Are Needed:
+1. **Opt-in form screenshot** — Shows the contact form with the SMS consent checkbox, disclosure text, and Privacy Policy/ToS links visible
+2. **Privacy Policy page screenshot** — Shows the live privacy policy page
+3. **Terms of Service page screenshot** — Shows the live terms page
+4. **Website homepage screenshot** — Shows the branded business website is real and functional
+
+### How to Generate:
+- **Puppeteer/Playwright** (headless browser)
+- Navigate to the deployed website
+- Take full-page screenshots of each critical page
+- Crop/annotate to highlight opt-in elements
+- Upload to publicly accessible storage (S3, Google Drive, HighLevel media library)
+- Generate shareable URLs for each screenshot
+
+### Screenshot Output Format:
+- PNG files, clearly showing the opt-in flow
+- Hosted at public URLs that TCR reviewers can access
+- Typically 3-5 screenshots per client
+
+---
+
+## PHASE 5: Compliance Packet Assembly & Delivery
+
+### What Gets Delivered to the Agency:
+
+**A complete copy-paste packet containing:**
+
+1. **Campaign Description** — Ready to paste into TCR/HighLevel campaign registration
+2. **Message Flow / Opt-in Description** — Exactly how users consent, with website URL and screenshot URLs
+3. **Sample Messages (2-3)** — With `[bracketed]` template fields, brand name, and opt-out language
+4. **Opt-in Confirmation Message** — Under 160 chars
+5. **Opt-out Confirmation Message** — With brand name
+6. **Help Message** — With brand name + contact info
+7. **Keywords** — Opt-in, opt-out, help keywords
+8. **Screenshot URLs** — Publicly hosted images of opt-in flow
+9. **Website URL** — Live, functional compliance website
+10. **White-labeled export** — Agency branding, not A2P Wizard branding
+
+### Delivery Method:
+- Email to agency with all assets
+- Possibly a dashboard/portal to download
+- PDF export option for client handoff
+
+---
+
+## COMPLETE BACKEND PIPELINE (End to End)
+
+```
+[User fills form]
+ ↓
+[Webhook fires to automation engine (n8n)]
+ ↓
+[Logo uploaded to CDN/S3]
+ ↓
+[AI generates all content: website copy + A2P compliance text]
+ ↓
+[config.json assembled with all content + business info]
+ ↓
+[React app builds static site from config.json]
+ ↓
+[Static site deployed to hosting (S3/CloudFront or subdomain)]
+ ↓
+[Headless browser takes screenshots of deployed site]
+ ↓
+[Screenshots uploaded to public storage, URLs generated]
+ ↓
+[Compliance packet assembled (all text + screenshot URLs + website URL)]
+ ↓
+[Email sent to agency with complete packet + white-label export]
+ ↓
+[Agency copy/pastes everything into TCR/HighLevel campaign registration]
+ ↓
+[Campaign approved ✓]
+```
+
+---
+
+## REBUILD TECH STACK RECOMMENDATION
+
+### Option A: n8n-Based (Like Original)
+- **Form:** Custom form or Tally → webhook
+- **Automation:** n8n workflow
+- **AI:** Claude API or OpenAI API
+- **Website:** React static site generator (fork Getting-Automated template)
+- **Screenshots:** Puppeteer/Playwright in n8n or Lambda
+- **Hosting:** AWS S3 + CloudFront or Cloudflare Pages
+- **Storage:** S3 for logos + screenshots
+- **Delivery:** Email via SendGrid/SES
+
+### Option B: All-in-One (Simpler)
+- **Form + Backend:** Next.js API routes or Cloudflare Workers
+- **AI:** Claude API
+- **Website:** Dynamic server-rendered pages (no build step needed — just serve from config)
+- **Screenshots:** Playwright on a serverless function
+- **Hosting:** Cloudflare Workers/Pages (free tier generous)
+- **Storage:** Cloudflare R2 for logos + screenshots
+- **Delivery:** Email via Resend or SES
+
+### Option C: Minimal/Fast MVP
+- **Form:** Simple HTML form → serverless function
+- **AI:** Single Claude API call generates everything
+- **Website:** Server-rendered HTML template (no React build step)
+- **Screenshots:** Playwright
+- **Hosting:** Single VPS or Cloudflare Workers
+- **Everything in one codebase**
+
+---
+
+## A2P COMPLIANCE REQUIREMENTS CHECKLIST
+
+For the generated website to pass TCR review, it MUST have:
+
+- [ ] Live, accessible website URL
+- [ ] Business name prominently displayed
+- [ ] Contact form with phone number field
+- [ ] **SMS consent checkbox (NOT pre-selected, NOT required to submit)**
+- [ ] Consent text with: program description, originating number, brand identity, opt-in language, fee disclosure, frequency disclosure, HELP instructions, STOP instructions
+- [ ] Privacy Policy page (accessible, mentions no third-party sharing of mobile data)
+- [ ] Terms of Service page (accessible, includes SMS terms)
+- [ ] Links to Privacy Policy and ToS in form footer (NOT in checkbox text)
+- [ ] Business email matching brand (not gmail for large corps)
+- [ ] Consistent branding across website, sample messages, and campaign description
+
+### Sample Messages Must:
+- [ ] Include brand name in at least one
+- [ ] Use `[brackets]` for templated fields
+- [ ] Include opt-out language in at least one ("Reply STOP to unsubscribe")
+- [ ] Match the registered use case (don't say "OTP" if registered as Marketing)
+
+### Campaign Description Must:
+- [ ] State who sends messages (brand name)
+- [ ] State who receives messages (customers, opted-in users)
+- [ ] State why messages are sent (purpose)
+- [ ] Match the sample messages in intent
+
+---
+
+## KEY FILES/REPOS TO REFERENCE
+
+- **Landing page React template:** https://github.com/Getting-Automated/landing-page-generator
+- **n8n workflow pattern:** "Domains to Dollars" Part 1 (content generation)
+- **HighLevel A2P best practices:** https://help.gohighlevel.com/support/solutions/articles/48001229784
+- **TCR campaign requirements:** Brand registration + Campaign registration via The Campaign Registry
diff --git a/a2p-wizard-rebuild/package.json b/a2p-wizard-rebuild/package.json
new file mode 100644
index 0000000..456f767
--- /dev/null
+++ b/a2p-wizard-rebuild/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "a2p-compliance-wizard",
+ "version": "1.0.0",
+ "description": "A2P 10DLC Compliance Wizard - Generate compliant websites and compliance packets",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js",
+ "dev": "node --watch server.js"
+ },
+ "dependencies": {
+ "@anthropic-ai/sdk": "^0.39.0",
+ "archiver": "^7.0.1",
+ "dotenv": "^16.4.7",
+ "ejs": "^3.1.10",
+ "express": "^4.21.1",
+ "multer": "^1.4.5-lts.1",
+ "nanoid": "^3.3.7",
+ "playwright": "^1.49.1",
+ "sharp": "^0.33.5"
+ }
+}
diff --git a/a2p-wizard-rebuild/public/css/form.css b/a2p-wizard-rebuild/public/css/form.css
new file mode 100644
index 0000000..5bf89fa
--- /dev/null
+++ b/a2p-wizard-rebuild/public/css/form.css
@@ -0,0 +1,551 @@
+/* ═══════════════════════════════════════════════
+ A2P Compliance Wizard - Form Styles
+ Premium, modern design
+ ═══════════════════════════════════════════════ */
+
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
+
+*, *::before, *::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+:root {
+ --primary: #2563EB;
+ --primary-dark: #1D4ED8;
+ --primary-light: #EFF6FF;
+ --accent: #7C3AED;
+ --accent-light: #F5F3FF;
+ --success: #059669;
+ --error: #DC2626;
+ --gray-50: #F9FAFB;
+ --gray-100: #F3F4F6;
+ --gray-200: #E5E7EB;
+ --gray-300: #D1D5DB;
+ --gray-400: #9CA3AF;
+ --gray-500: #6B7280;
+ --gray-600: #4B5563;
+ --gray-700: #374151;
+ --gray-800: #1F2937;
+ --gray-900: #111827;
+ --radius: 12px;
+ --radius-lg: 16px;
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
+ --shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
+ --shadow-lg: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1);
+ --shadow-xl: 0 25px 50px -12px rgba(0,0,0,0.25);
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
+ background: var(--gray-50);
+ color: var(--gray-900);
+ line-height: 1.6;
+ min-height: 100vh;
+}
+
+/* ── Background Pattern ── */
+.page-bg {
+ position: fixed;
+ inset: 0;
+ z-index: -1;
+ background:
+ radial-gradient(ellipse at 20% 50%, rgba(37, 99, 235, 0.08) 0%, transparent 50%),
+ radial-gradient(ellipse at 80% 20%, rgba(124, 58, 237, 0.06) 0%, transparent 50%),
+ radial-gradient(ellipse at 50% 100%, rgba(37, 99, 235, 0.04) 0%, transparent 50%),
+ var(--gray-50);
+}
+
+/* ── Header ── */
+.header {
+ text-align: center;
+ padding: 60px 20px 20px;
+}
+
+.header-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ background: var(--primary-light);
+ color: var(--primary);
+ font-size: 13px;
+ font-weight: 600;
+ padding: 6px 16px;
+ border-radius: 100px;
+ margin-bottom: 20px;
+ letter-spacing: 0.3px;
+}
+
+.header-badge svg {
+ width: 16px;
+ height: 16px;
+}
+
+.header h1 {
+ font-size: clamp(28px, 5vw, 42px);
+ font-weight: 800;
+ letter-spacing: -0.03em;
+ line-height: 1.15;
+ background: linear-gradient(135deg, var(--gray-900) 0%, var(--primary-dark) 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ margin-bottom: 12px;
+}
+
+.header p {
+ font-size: 17px;
+ color: var(--gray-500);
+ max-width: 520px;
+ margin: 0 auto;
+ line-height: 1.6;
+}
+
+/* ── Steps indicator ── */
+.steps {
+ display: flex;
+ justify-content: center;
+ gap: 8px;
+ padding: 24px 20px;
+ max-width: 500px;
+ margin: 0 auto;
+}
+
+.step {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--gray-400);
+}
+
+.step-num {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ font-weight: 700;
+ background: var(--gray-200);
+ color: var(--gray-500);
+ transition: all 0.3s ease;
+}
+
+.step.active .step-num {
+ background: var(--primary);
+ color: white;
+ box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.2);
+}
+
+.step.active {
+ color: var(--primary);
+}
+
+.step.done .step-num {
+ background: var(--success);
+ color: white;
+}
+
+.step-line {
+ width: 40px;
+ height: 2px;
+ background: var(--gray-200);
+ border-radius: 2px;
+}
+
+/* ── Form Container ── */
+.form-container {
+ max-width: 640px;
+ margin: 0 auto 80px;
+ padding: 0 20px;
+}
+
+.form-card {
+ background: white;
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-lg);
+ border: 1px solid var(--gray-200);
+ overflow: hidden;
+}
+
+.form-section {
+ padding: 32px;
+}
+
+.form-section + .form-section {
+ border-top: 1px solid var(--gray-100);
+}
+
+.section-title {
+ font-size: 16px;
+ font-weight: 700;
+ color: var(--gray-800);
+ margin-bottom: 4px;
+}
+
+.section-subtitle {
+ font-size: 13px;
+ color: var(--gray-500);
+ margin-bottom: 24px;
+}
+
+/* ── Form Fields ── */
+.form-group {
+ margin-bottom: 20px;
+}
+
+.form-group:last-child {
+ margin-bottom: 0;
+}
+
+.form-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 16px;
+}
+
+label {
+ display: block;
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--gray-700);
+ margin-bottom: 6px;
+}
+
+label .required {
+ color: var(--error);
+ margin-left: 2px;
+}
+
+input[type="text"],
+input[type="email"],
+input[type="tel"],
+textarea {
+ width: 100%;
+ padding: 10px 14px;
+ font-size: 15px;
+ font-family: inherit;
+ border: 1.5px solid var(--gray-300);
+ border-radius: var(--radius);
+ background: white;
+ color: var(--gray-900);
+ transition: all 0.2s ease;
+ outline: none;
+}
+
+input:focus,
+textarea:focus {
+ border-color: var(--primary);
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
+}
+
+input::placeholder,
+textarea::placeholder {
+ color: var(--gray-400);
+}
+
+textarea {
+ resize: vertical;
+ min-height: 80px;
+}
+
+.field-hint {
+ font-size: 12px;
+ color: var(--gray-400);
+ margin-top: 4px;
+}
+
+/* ── Logo Upload ── */
+.logo-upload {
+ border: 2px dashed var(--gray-300);
+ border-radius: var(--radius);
+ padding: 32px;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ background: var(--gray-50);
+}
+
+.logo-upload:hover {
+ border-color: var(--primary);
+ background: var(--primary-light);
+}
+
+.logo-upload.has-file {
+ border-color: var(--success);
+ background: #F0FDF4;
+}
+
+.logo-upload input[type="file"] {
+ display: none;
+}
+
+.logo-upload-icon {
+ width: 48px;
+ height: 48px;
+ margin: 0 auto 12px;
+ border-radius: 12px;
+ background: var(--gray-200);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--gray-500);
+ transition: all 0.2s ease;
+}
+
+.logo-upload:hover .logo-upload-icon {
+ background: rgba(37, 99, 235, 0.1);
+ color: var(--primary);
+}
+
+.logo-upload-text {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--gray-700);
+}
+
+.logo-upload-hint {
+ font-size: 12px;
+ color: var(--gray-400);
+ margin-top: 4px;
+}
+
+.logo-preview {
+ max-width: 200px;
+ max-height: 100px;
+ margin: 12px auto 0;
+ border-radius: 8px;
+}
+
+/* ── Checkbox ── */
+.checkbox-group {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ padding: 16px;
+ background: var(--accent-light);
+ border-radius: var(--radius);
+ border: 1px solid rgba(124, 58, 237, 0.15);
+}
+
+.checkbox-group input[type="checkbox"] {
+ width: 20px;
+ height: 20px;
+ margin-top: 2px;
+ flex-shrink: 0;
+ accent-color: var(--accent);
+ cursor: pointer;
+}
+
+.checkbox-label {
+ font-size: 13px;
+ color: var(--gray-600);
+ line-height: 1.5;
+ cursor: pointer;
+}
+
+/* ── Submit Button ── */
+.submit-section {
+ padding: 24px 32px;
+ background: var(--gray-50);
+ border-top: 1px solid var(--gray-100);
+}
+
+.btn-submit {
+ width: 100%;
+ padding: 14px 28px;
+ font-size: 16px;
+ font-weight: 700;
+ font-family: inherit;
+ color: white;
+ background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
+ border: none;
+ border-radius: var(--radius);
+ cursor: pointer;
+ transition: all 0.3s ease;
+ position: relative;
+ overflow: hidden;
+}
+
+.btn-submit:hover:not(:disabled) {
+ transform: translateY(-1px);
+ box-shadow: 0 8px 25px rgba(37, 99, 235, 0.3);
+}
+
+.btn-submit:active:not(:disabled) {
+ transform: translateY(0);
+}
+
+.btn-submit:disabled {
+ opacity: 0.7;
+ cursor: not-allowed;
+}
+
+.btn-text {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+}
+
+/* ── Loading Overlay ── */
+.loading-overlay {
+ display: none;
+ position: fixed;
+ inset: 0;
+ z-index: 1000;
+ background: rgba(15, 23, 42, 0.7);
+ backdrop-filter: blur(8px);
+ justify-content: center;
+ align-items: center;
+}
+
+.loading-overlay.active {
+ display: flex;
+}
+
+.loading-card {
+ background: white;
+ border-radius: var(--radius-lg);
+ padding: 48px;
+ text-align: center;
+ max-width: 420px;
+ width: 90%;
+ box-shadow: var(--shadow-xl);
+}
+
+.loading-spinner {
+ width: 56px;
+ height: 56px;
+ margin: 0 auto 24px;
+ border: 3px solid var(--gray-200);
+ border-top: 3px solid var(--primary);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+.loading-title {
+ font-size: 20px;
+ font-weight: 700;
+ color: var(--gray-900);
+ margin-bottom: 8px;
+}
+
+.loading-message {
+ font-size: 14px;
+ color: var(--gray-500);
+ margin-bottom: 32px;
+}
+
+.progress-bar {
+ width: 100%;
+ height: 6px;
+ background: var(--gray-200);
+ border-radius: 3px;
+ overflow: hidden;
+ margin-bottom: 16px;
+}
+
+.progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, var(--primary), var(--accent));
+ border-radius: 3px;
+ width: 0%;
+ transition: width 0.5s ease;
+}
+
+.progress-steps {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ text-align: left;
+}
+
+.progress-step {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 13px;
+ color: var(--gray-400);
+ transition: color 0.3s ease;
+}
+
+.progress-step.active {
+ color: var(--primary);
+ font-weight: 600;
+}
+
+.progress-step.done {
+ color: var(--success);
+}
+
+.progress-step-icon {
+ width: 20px;
+ height: 20px;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* ── Error ── */
+.error-banner {
+ display: none;
+ background: #FEF2F2;
+ border: 1px solid #FECACA;
+ color: var(--error);
+ padding: 12px 16px;
+ border-radius: var(--radius);
+ font-size: 14px;
+ margin-bottom: 20px;
+ animation: fadeIn 0.3s ease;
+}
+
+.error-banner.visible {
+ display: block;
+}
+
+/* ── Footer ── */
+.footer {
+ text-align: center;
+ padding: 40px 20px;
+ color: var(--gray-400);
+ font-size: 13px;
+}
+
+/* ── Animations ── */
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(8px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* ── Responsive ── */
+@media (max-width: 640px) {
+ .form-row {
+ grid-template-columns: 1fr;
+ }
+
+ .form-section {
+ padding: 24px 20px;
+ }
+
+ .submit-section {
+ padding: 20px;
+ }
+
+ .steps {
+ flex-wrap: wrap;
+ }
+}
diff --git a/a2p-wizard-rebuild/public/css/site.css b/a2p-wizard-rebuild/public/css/site.css
new file mode 100644
index 0000000..98ab0fc
--- /dev/null
+++ b/a2p-wizard-rebuild/public/css/site.css
@@ -0,0 +1,737 @@
+/* ═══════════════════════════════════════════════
+ A2P Compliance Wizard - Generated Site Styles
+ Premium SaaS-quality landing page
+ ═══════════════════════════════════════════════ */
+
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
+
+*, *::before, *::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+:root {
+ --primary: #2563EB;
+ --primary-dark: #1D4ED8;
+ --primary-light: #EFF6FF;
+ --accent: #F59E0B;
+ --gray-50: #F9FAFB;
+ --gray-100: #F3F4F6;
+ --gray-200: #E5E7EB;
+ --gray-300: #D1D5DB;
+ --gray-400: #9CA3AF;
+ --gray-500: #6B7280;
+ --gray-600: #4B5563;
+ --gray-700: #374151;
+ --gray-800: #1F2937;
+ --gray-900: #111827;
+ --radius: 12px;
+ --radius-lg: 16px;
+ --radius-xl: 24px;
+}
+
+html { scroll-behavior: smooth; }
+
+body {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
+ color: var(--gray-900);
+ line-height: 1.6;
+ background: white;
+ -webkit-font-smoothing: antialiased;
+}
+
+a { color: var(--primary); text-decoration: none; }
+a:hover { text-decoration: underline; }
+
+img { max-width: 100%; height: auto; }
+
+.container {
+ max-width: 1140px;
+ margin: 0 auto;
+ padding: 0 24px;
+}
+
+/* ── Navigation ── */
+.nav {
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ background: rgba(255,255,255,0.85);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border-bottom: 1px solid var(--gray-200);
+ padding: 0 24px;
+}
+
+.nav-inner {
+ max-width: 1140px;
+ margin: 0 auto;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 64px;
+}
+
+.nav-brand {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-weight: 800;
+ font-size: 18px;
+ color: var(--gray-900);
+ text-decoration: none;
+}
+
+.nav-brand img {
+ height: 36px;
+ width: auto;
+ border-radius: 6px;
+}
+
+.nav-links {
+ display: flex;
+ align-items: center;
+ gap: 32px;
+ list-style: none;
+}
+
+.nav-links a {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--gray-600);
+ text-decoration: none;
+ transition: color 0.2s;
+}
+
+.nav-links a:hover {
+ color: var(--primary);
+ text-decoration: none;
+}
+
+.nav-cta {
+ background: var(--primary) !important;
+ color: white !important;
+ padding: 8px 20px;
+ border-radius: 8px;
+ font-weight: 600 !important;
+ transition: all 0.2s ease !important;
+}
+
+.nav-cta:hover {
+ background: var(--primary-dark) !important;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(37,99,235,0.3);
+}
+
+.nav-mobile-toggle {
+ display: none;
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 8px;
+ color: var(--gray-700);
+}
+
+/* ── Hero ── */
+.hero {
+ padding: 100px 0 80px;
+ text-align: center;
+ background:
+ radial-gradient(ellipse at 30% 0%, rgba(37,99,235,0.07) 0%, transparent 50%),
+ radial-gradient(ellipse at 70% 100%, rgba(37,99,235,0.05) 0%, transparent 50%),
+ white;
+ position: relative;
+ overflow: hidden;
+}
+
+.hero::before {
+ content: '';
+ position: absolute;
+ top: -50%;
+ left: -50%;
+ width: 200%;
+ height: 200%;
+ background: radial-gradient(circle, rgba(37,99,235,0.03) 1px, transparent 1px);
+ background-size: 40px 40px;
+ z-index: 0;
+}
+
+.hero .container {
+ position: relative;
+ z-index: 1;
+}
+
+.hero-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ background: var(--primary-light);
+ color: var(--primary);
+ font-size: 13px;
+ font-weight: 600;
+ padding: 6px 16px;
+ border-radius: 100px;
+ margin-bottom: 24px;
+}
+
+.hero h1 {
+ font-size: clamp(36px, 6vw, 60px);
+ font-weight: 900;
+ line-height: 1.1;
+ letter-spacing: -0.03em;
+ margin-bottom: 20px;
+ max-width: 700px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.hero-subtitle {
+ font-size: clamp(17px, 2.5vw, 20px);
+ color: var(--gray-500);
+ max-width: 560px;
+ margin: 0 auto 36px;
+ line-height: 1.6;
+}
+
+.hero-cta {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ background: var(--primary);
+ color: white;
+ font-size: 17px;
+ font-weight: 700;
+ padding: 16px 36px;
+ border-radius: var(--radius);
+ text-decoration: none;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 14px rgba(37,99,235,0.3);
+}
+
+.hero-cta:hover {
+ background: var(--primary-dark);
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(37,99,235,0.35);
+ text-decoration: none;
+ color: white;
+}
+
+.hero-cta svg {
+ width: 20px;
+ height: 20px;
+}
+
+/* ── Section common ── */
+.section {
+ padding: 80px 0;
+}
+
+.section-alt {
+ background: var(--gray-50);
+}
+
+.section-header {
+ text-align: center;
+ margin-bottom: 56px;
+}
+
+.section-header h2 {
+ font-size: clamp(28px, 4vw, 40px);
+ font-weight: 800;
+ letter-spacing: -0.02em;
+ margin-bottom: 12px;
+}
+
+.section-header p {
+ font-size: 17px;
+ color: var(--gray-500);
+ max-width: 520px;
+ margin: 0 auto;
+}
+
+/* ── Pain Points ── */
+.painpoints-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+ gap: 24px;
+}
+
+.painpoint-card {
+ background: white;
+ border: 1px solid var(--gray-200);
+ border-radius: var(--radius-lg);
+ padding: 32px;
+ transition: all 0.3s ease;
+}
+
+.section-alt .painpoint-card {
+ background: white;
+}
+
+.painpoint-card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 12px 32px rgba(0,0,0,0.08);
+ border-color: transparent;
+}
+
+.painpoint-icon {
+ width: 52px;
+ height: 52px;
+ border-radius: 14px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 20px;
+ background: var(--primary-light);
+ color: var(--primary);
+}
+
+.painpoint-card h3 {
+ font-size: 18px;
+ font-weight: 700;
+ margin-bottom: 8px;
+ color: var(--gray-900);
+}
+
+.painpoint-card p {
+ font-size: 15px;
+ color: var(--gray-500);
+ line-height: 1.6;
+}
+
+/* ── How It Works ── */
+.steps-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 32px;
+ position: relative;
+}
+
+.steps-grid::before {
+ content: '';
+ position: absolute;
+ top: 44px;
+ left: 15%;
+ right: 15%;
+ height: 2px;
+ background: linear-gradient(90deg, var(--primary), var(--gray-300), var(--primary));
+ z-index: 0;
+}
+
+.step-card {
+ text-align: center;
+ position: relative;
+ z-index: 1;
+}
+
+.step-number {
+ width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ background: var(--primary);
+ color: white;
+ font-size: 22px;
+ font-weight: 800;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0 auto 20px;
+ box-shadow: 0 4px 14px rgba(37,99,235,0.3);
+}
+
+.step-card h3 {
+ font-size: 18px;
+ font-weight: 700;
+ margin-bottom: 8px;
+}
+
+.step-card p {
+ font-size: 15px;
+ color: var(--gray-500);
+ max-width: 280px;
+ margin: 0 auto;
+ line-height: 1.6;
+}
+
+/* ── FAQ ── */
+.faq-list {
+ max-width: 720px;
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.faq-item {
+ background: white;
+ border: 1px solid var(--gray-200);
+ border-radius: var(--radius);
+ overflow: hidden;
+ transition: all 0.2s ease;
+}
+
+.faq-item:hover {
+ border-color: var(--gray-300);
+}
+
+.faq-question {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ padding: 20px 24px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-family: inherit;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--gray-900);
+ text-align: left;
+ transition: color 0.2s;
+}
+
+.faq-question:hover {
+ color: var(--primary);
+}
+
+.faq-icon {
+ width: 24px;
+ height: 24px;
+ flex-shrink: 0;
+ color: var(--gray-400);
+ transition: transform 0.3s ease;
+}
+
+.faq-item.open .faq-icon {
+ transform: rotate(45deg);
+ color: var(--primary);
+}
+
+.faq-answer {
+ max-height: 0;
+ overflow: hidden;
+ transition: max-height 0.3s ease;
+}
+
+.faq-answer-inner {
+ padding: 0 24px 20px;
+ font-size: 15px;
+ color: var(--gray-500);
+ line-height: 1.7;
+}
+
+/* ── CTA Section ── */
+.cta-section {
+ padding: 80px 0;
+ text-align: center;
+ background: linear-gradient(135deg, var(--gray-900) 0%, #1e3a5f 100%);
+ color: white;
+}
+
+.cta-section h2 {
+ font-size: clamp(28px, 4vw, 40px);
+ font-weight: 800;
+ margin-bottom: 16px;
+}
+
+.cta-section p {
+ font-size: 17px;
+ color: rgba(255,255,255,0.7);
+ max-width: 480px;
+ margin: 0 auto 32px;
+}
+
+.cta-section .hero-cta {
+ background: white;
+ color: var(--gray-900);
+ box-shadow: 0 4px 14px rgba(0,0,0,0.2);
+}
+
+.cta-section .hero-cta:hover {
+ background: var(--gray-100);
+ color: var(--gray-900);
+}
+
+/* ── Contact / Opt-in Form ── */
+.contact-section {
+ padding: 80px 0;
+}
+
+.contact-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 56px;
+ align-items: start;
+}
+
+.contact-info h2 {
+ font-size: 32px;
+ font-weight: 800;
+ margin-bottom: 16px;
+}
+
+.contact-info p {
+ font-size: 16px;
+ color: var(--gray-500);
+ margin-bottom: 32px;
+ line-height: 1.6;
+}
+
+.contact-detail {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 16px;
+ font-size: 15px;
+ color: var(--gray-600);
+}
+
+.contact-detail-icon {
+ width: 40px;
+ height: 40px;
+ border-radius: 10px;
+ background: var(--primary-light);
+ color: var(--primary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.contact-form-card {
+ background: white;
+ border: 1px solid var(--gray-200);
+ border-radius: var(--radius-lg);
+ padding: 36px;
+ box-shadow: var(--shadow-lg);
+}
+
+.contact-form .form-group {
+ margin-bottom: 16px;
+}
+
+.contact-form label {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--gray-700);
+ margin-bottom: 6px;
+ display: block;
+}
+
+.contact-form input,
+.contact-form textarea {
+ width: 100%;
+ padding: 11px 14px;
+ font-size: 15px;
+ font-family: inherit;
+ border: 1.5px solid var(--gray-300);
+ border-radius: 10px;
+ background: white;
+ color: var(--gray-900);
+ outline: none;
+ transition: all 0.2s ease;
+}
+
+.contact-form input:focus,
+.contact-form textarea:focus {
+ border-color: var(--primary);
+ box-shadow: 0 0 0 3px rgba(37,99,235,0.12);
+}
+
+.consent-group {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ padding: 16px;
+ background: var(--gray-50);
+ border: 1px solid var(--gray-200);
+ border-radius: 10px;
+ margin-top: 20px;
+}
+
+.consent-group input[type="checkbox"] {
+ width: 20px;
+ height: 20px;
+ margin-top: 2px;
+ flex-shrink: 0;
+ accent-color: var(--primary);
+ cursor: pointer;
+}
+
+.consent-text {
+ font-size: 12px;
+ color: var(--gray-500);
+ line-height: 1.6;
+}
+
+.contact-form .btn-submit {
+ width: 100%;
+ padding: 14px 28px;
+ font-size: 16px;
+ font-weight: 700;
+ font-family: inherit;
+ color: white;
+ background: var(--primary);
+ border: none;
+ border-radius: 10px;
+ cursor: pointer;
+ margin-top: 20px;
+ transition: all 0.2s ease;
+}
+
+.contact-form .btn-submit:hover {
+ background: var(--primary-dark);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(37,99,235,0.3);
+}
+
+/* ── Legal Pages ── */
+.legal-page {
+ padding: 60px 0 80px;
+}
+
+.legal-page .container {
+ max-width: 780px;
+}
+
+.legal-page h1 {
+ font-size: 36px;
+ font-weight: 800;
+ margin-bottom: 8px;
+}
+
+.legal-meta {
+ font-size: 14px;
+ color: var(--gray-500);
+ margin-bottom: 40px;
+ padding-bottom: 24px;
+ border-bottom: 1px solid var(--gray-200);
+}
+
+.legal-content {
+ font-size: 15px;
+ color: var(--gray-600);
+ line-height: 1.8;
+}
+
+.legal-content h2 {
+ font-size: 22px;
+ font-weight: 700;
+ color: var(--gray-900);
+ margin: 36px 0 12px;
+}
+
+.legal-content h3 {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--gray-800);
+ margin: 28px 0 8px;
+}
+
+.legal-content p {
+ margin-bottom: 16px;
+}
+
+.legal-content ul, .legal-content ol {
+ margin: 12px 0 16px 24px;
+}
+
+.legal-content li {
+ margin-bottom: 8px;
+}
+
+/* ── Footer ── */
+.footer {
+ background: var(--gray-900);
+ color: rgba(255,255,255,0.6);
+ padding: 48px 0;
+}
+
+.footer-inner {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 20px;
+}
+
+.footer-brand {
+ font-size: 16px;
+ font-weight: 700;
+ color: white;
+}
+
+.footer-links {
+ display: flex;
+ gap: 24px;
+ list-style: none;
+}
+
+.footer-links a {
+ font-size: 14px;
+ color: rgba(255,255,255,0.5);
+ text-decoration: none;
+ transition: color 0.2s;
+}
+
+.footer-links a:hover {
+ color: white;
+ text-decoration: none;
+}
+
+.footer-copy {
+ width: 100%;
+ text-align: center;
+ font-size: 13px;
+ margin-top: 24px;
+ padding-top: 24px;
+ border-top: 1px solid rgba(255,255,255,0.1);
+}
+
+/* ── Mobile ── */
+@media (max-width: 768px) {
+ .nav-links { display: none; }
+ .nav-mobile-toggle { display: block; }
+
+ .nav-links.open {
+ display: flex;
+ flex-direction: column;
+ position: absolute;
+ top: 64px;
+ left: 0;
+ right: 0;
+ background: white;
+ border-bottom: 1px solid var(--gray-200);
+ padding: 16px 24px;
+ gap: 16px;
+ box-shadow: 0 8px 24px rgba(0,0,0,0.1);
+ }
+
+ .steps-grid {
+ grid-template-columns: 1fr;
+ gap: 40px;
+ }
+
+ .steps-grid::before { display: none; }
+
+ .contact-grid {
+ grid-template-columns: 1fr;
+ gap: 40px;
+ }
+
+ .hero { padding: 60px 0 50px; }
+ .section { padding: 60px 0; }
+
+ .footer-inner {
+ flex-direction: column;
+ text-align: center;
+ }
+}
+
+/* ── SVG Icon helpers ── */
+.icon-svg {
+ width: 24px;
+ height: 24px;
+}
diff --git a/a2p-wizard-rebuild/public/js/form.js b/a2p-wizard-rebuild/public/js/form.js
new file mode 100644
index 0000000..41b5332
--- /dev/null
+++ b/a2p-wizard-rebuild/public/js/form.js
@@ -0,0 +1,204 @@
+// ═══════════════════════════════════════════════
+// A2P Compliance Wizard - Form Handler
+// ═══════════════════════════════════════════════
+
+document.addEventListener('DOMContentLoaded', () => {
+ const form = document.getElementById('wizardForm');
+ const submitBtn = document.getElementById('submitBtn');
+ const overlay = document.getElementById('loadingOverlay');
+ const errorBanner = document.getElementById('errorBanner');
+ const logoInput = document.getElementById('logoInput');
+ const logoUpload = document.querySelector('.logo-upload');
+ const logoPreview = document.getElementById('logoPreview');
+ const logoUploadText = document.querySelector('.logo-upload-text');
+
+ // Logo upload preview
+ logoUpload.addEventListener('click', () => logoInput.click());
+ logoUpload.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ logoUpload.style.borderColor = '#2563EB';
+ logoUpload.style.background = '#EFF6FF';
+ });
+ logoUpload.addEventListener('dragleave', () => {
+ logoUpload.style.borderColor = '';
+ logoUpload.style.background = '';
+ });
+ logoUpload.addEventListener('drop', (e) => {
+ e.preventDefault();
+ logoUpload.style.borderColor = '';
+ logoUpload.style.background = '';
+ if (e.dataTransfer.files.length) {
+ logoInput.files = e.dataTransfer.files;
+ handleLogoSelect();
+ }
+ });
+
+ logoInput.addEventListener('change', handleLogoSelect);
+
+ function handleLogoSelect() {
+ const file = logoInput.files[0];
+ if (!file) return;
+
+ if (!file.type.startsWith('image/')) {
+ showError('Please upload an image file (PNG, JPG, SVG, WebP)');
+ return;
+ }
+
+ if (file.size > 5 * 1024 * 1024) {
+ showError('Logo must be under 5MB');
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ logoPreview.src = e.target.result;
+ logoPreview.style.display = 'block';
+ logoUpload.classList.add('has-file');
+ logoUploadText.textContent = file.name;
+ };
+ reader.readAsDataURL(file);
+ }
+
+ // Form submission
+ form.addEventListener('submit', async (e) => {
+ e.preventDefault();
+ hideError();
+
+ // Validate
+ if (!validateForm()) return;
+
+ // Show loading
+ overlay.classList.add('active');
+ submitBtn.disabled = true;
+ updateProgress(0, 'Preparing your request...');
+
+ try {
+ const formData = new FormData(form);
+
+ // Simulate progress during generation
+ const progressInterval = simulateProgress();
+
+ const response = await fetch('/api/generate', {
+ method: 'POST',
+ body: formData,
+ });
+
+ clearInterval(progressInterval);
+
+ const result = await response.json();
+
+ if (!result.success) {
+ throw new Error(result.error || 'Generation failed');
+ }
+
+ // Show completion
+ updateProgress(100, 'Complete!');
+ setStepDone(0);
+ setStepDone(1);
+ setStepDone(2);
+ setStepDone(3);
+
+ // Redirect to results
+ setTimeout(() => {
+ window.location.href = result.resultsUrl;
+ }, 800);
+
+ } catch (err) {
+ overlay.classList.remove('active');
+ submitBtn.disabled = false;
+ showError(err.message || 'Something went wrong. Please try again.');
+ }
+ });
+
+ function validateForm() {
+ const required = ['agencyName', 'agencyEmail', 'businessName', 'businessAddress', 'businessEmail', 'businessPhone', 'businessDescription'];
+
+ for (const field of required) {
+ const input = form.querySelector(`[name="${field}"]`);
+ if (!input || !input.value.trim()) {
+ showError(`Please fill in all required fields`);
+ input?.focus();
+ return false;
+ }
+ }
+
+ // Validate email
+ const email = form.querySelector('[name="agencyEmail"]').value;
+ if (!email.includes('@') || !email.includes('.')) {
+ showError('Please enter a valid email address');
+ return false;
+ }
+
+ // Validate checkbox
+ const checkbox = form.querySelector('[name="tcpaConsent"]');
+ if (!checkbox || !checkbox.checked) {
+ showError('Please confirm TCPA compliance to continue');
+ return false;
+ }
+
+ return true;
+ }
+
+ function simulateProgress() {
+ let progress = 0;
+ const steps = [
+ { at: 10, msg: 'Generating AI content...' },
+ { at: 45, msg: 'Building website pages...' },
+ { at: 65, msg: 'Taking screenshots...' },
+ { at: 85, msg: 'Assembling compliance packet...' },
+ ];
+ let stepIndex = 0;
+
+ return setInterval(() => {
+ if (progress < 90) {
+ progress += Math.random() * 3 + 0.5;
+ progress = Math.min(progress, 92);
+ updateProgress(progress);
+
+ if (stepIndex < steps.length && progress >= steps[stepIndex].at) {
+ setStepActive(stepIndex);
+ if (stepIndex > 0) setStepDone(stepIndex - 1);
+ document.querySelector('.loading-message').textContent = steps[stepIndex].msg;
+ stepIndex++;
+ }
+ }
+ }, 500);
+ }
+
+ function updateProgress(pct, msg) {
+ const fill = document.querySelector('.progress-fill');
+ if (fill) fill.style.width = `${pct}%`;
+ if (msg) {
+ const el = document.querySelector('.loading-message');
+ if (el) el.textContent = msg;
+ }
+ }
+
+ function setStepActive(index) {
+ const steps = document.querySelectorAll('.progress-step');
+ if (steps[index]) {
+ steps[index].classList.remove('done');
+ steps[index].classList.add('active');
+ }
+ }
+
+ function setStepDone(index) {
+ const steps = document.querySelectorAll('.progress-step');
+ if (steps[index]) {
+ steps[index].classList.remove('active');
+ steps[index].classList.add('done');
+ const icon = steps[index].querySelector('.progress-step-icon');
+ if (icon) icon.innerHTML = '';
+ }
+ }
+
+ function showError(msg) {
+ errorBanner.textContent = msg;
+ errorBanner.classList.add('visible');
+ errorBanner.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }
+
+ function hideError() {
+ errorBanner.classList.remove('visible');
+ }
+});
diff --git a/a2p-wizard-rebuild/routes/api.js b/a2p-wizard-rebuild/routes/api.js
new file mode 100644
index 0000000..cce1d92
--- /dev/null
+++ b/a2p-wizard-rebuild/routes/api.js
@@ -0,0 +1,126 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const path = require('path');
+const fs = require('fs');
+const { nanoid } = require('nanoid');
+const { generateContent } = require('../services/ai-generator');
+const { buildSite } = require('../services/site-builder');
+const { takeScreenshots } = require('../services/screenshot');
+const { assemblePacket } = require('../services/packet-assembler');
+
+// Configure multer for logo uploads
+const storage = multer.memoryStorage();
+const upload = multer({
+ storage,
+ limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
+ fileFilter: (req, file, cb) => {
+ const allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'];
+ if (allowed.includes(file.mimetype)) {
+ cb(null, true);
+ } else {
+ cb(new Error('Invalid file type. Only images are allowed.'));
+ }
+ }
+});
+
+// POST /api/generate - Main pipeline
+router.post('/generate', upload.single('logo'), async (req, res) => {
+ const clientId = nanoid(12);
+ const clientDir = path.join(__dirname, '..', 'data', clientId);
+
+ try {
+ // Create client directory
+ fs.mkdirSync(path.join(clientDir, 'screenshots'), { recursive: true });
+ fs.mkdirSync(path.join(clientDir, 'site'), { recursive: true });
+
+ // Parse form data
+ const formData = {
+ agencyName: req.body.agencyName,
+ agencyEmail: req.body.agencyEmail,
+ businessName: req.body.businessName,
+ businessAddress: req.body.businessAddress,
+ businessEmail: req.body.businessEmail,
+ businessPhone: req.body.businessPhone,
+ businessDescription: req.body.businessDescription,
+ };
+
+ // Save logo if uploaded
+ let logoPath = null;
+ if (req.file) {
+ const ext = path.extname(req.file.originalname) || '.png';
+ logoPath = path.join(clientDir, `logo${ext}`);
+ fs.writeFileSync(logoPath, req.file.buffer);
+ formData.logoUrl = `/data/${clientId}/logo${ext}`;
+ }
+
+ // Step 1: Generate AI content
+ console.log(`[${clientId}] Step 1: Generating AI content...`);
+ const aiContent = await generateContent(formData);
+
+ // Step 2: Build config — generate subdomain from business name
+ const MAIN_DOMAIN = req.app.locals.MAIN_DOMAIN || 'solvedby.us';
+ const subdomain = formData.businessName
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-|-$/g, '')
+ .substring(0, 40);
+
+ const siteUrl = `https://${subdomain}.${MAIN_DOMAIN}`;
+ const localSiteUrl = `http://localhost:8899/sites/${clientId}`;
+
+ const config = {
+ clientId,
+ subdomain,
+ ...formData,
+ ...aiContent,
+ siteUrl,
+ localSiteUrl,
+ generatedAt: new Date().toISOString(),
+ };
+
+ // Save config
+ fs.writeFileSync(
+ path.join(clientDir, 'config.json'),
+ JSON.stringify(config, null, 2)
+ );
+
+ // Step 3: Build the static site
+ console.log(`[${clientId}] Step 2: Building website...`);
+ await buildSite(config, clientDir);
+
+ // Step 4: Take screenshots
+ console.log(`[${clientId}] Step 3: Taking screenshots...`);
+ await takeScreenshots(clientId, clientDir);
+
+ // Step 5: Assemble compliance packet zip
+ console.log(`[${clientId}] Step 4: Assembling compliance packet...`);
+ await assemblePacket(config, clientDir);
+
+ // Step 6: Register subdomain → clientId mapping
+ const subdomainIndex = req.app.locals.subdomainIndex;
+ const saveSubdomainIndex = req.app.locals.saveSubdomainIndex;
+ subdomainIndex[subdomain] = clientId;
+ saveSubdomainIndex();
+ console.log(`[${clientId}] Step 5: Registered subdomain ${subdomain}.${MAIN_DOMAIN}`);
+
+ console.log(`[${clientId}] ✅ Complete! Site live at ${siteUrl}`);
+
+ res.json({
+ success: true,
+ clientId,
+ resultsUrl: `/results/${clientId}`,
+ siteUrl,
+ localSiteUrl: `/sites/${clientId}/`,
+ });
+
+ } catch (error) {
+ console.error(`[${clientId}] ❌ Error:`, error);
+ res.status(500).json({
+ success: false,
+ error: error.message || 'Generation failed',
+ });
+ }
+});
+
+module.exports = router;
diff --git a/a2p-wizard-rebuild/routes/sites.js b/a2p-wizard-rebuild/routes/sites.js
new file mode 100644
index 0000000..4865b3a
--- /dev/null
+++ b/a2p-wizard-rebuild/routes/sites.js
@@ -0,0 +1,52 @@
+const express = require('express');
+const router = express.Router();
+const path = require('path');
+const fs = require('fs');
+
+// Serve generated static sites at /sites/{clientId}/
+// (Subdomain serving is handled in server.js middleware)
+
+const pageMap = {
+ '': 'index.html',
+ 'contact': 'contact.html',
+ 'privacy-policy': 'privacy-policy.html',
+ 'terms': 'terms.html',
+};
+
+// Serve static assets for the site (CSS, images, etc.)
+router.use('/:clientId/assets', (req, res) => {
+ const { clientId } = req.params;
+ const filePath = path.join(__dirname, '..', 'data', clientId, 'site', 'assets', req.path);
+ if (fs.existsSync(filePath)) {
+ res.sendFile(filePath);
+ } else {
+ res.status(404).send('Not found');
+ }
+});
+
+// Serve pages
+router.get('/:clientId/:page?', (req, res) => {
+ const { clientId } = req.params;
+ const page = (req.params.page || '').replace(/\/$/, '');
+ const filename = pageMap[page];
+
+ if (!filename) {
+ return res.status(404).send('Page not found');
+ }
+
+ const filePath = path.join(__dirname, '..', 'data', clientId, 'site', filename);
+ if (!fs.existsSync(filePath)) {
+ return res.status(404).send('Site not found');
+ }
+
+ // When served via /sites/{id}/, rewrite links to include the prefix
+ let html = fs.readFileSync(filePath, 'utf8');
+ const baseUrl = `/sites/${clientId}`;
+ html = html.replace(/href="\//g, `href="${baseUrl}/`);
+ html = html.replace(/src="\//g, `src="${baseUrl}/`);
+ html = html.replace(/href="#/g, 'href="#'); // keep anchor links
+
+ res.type('html').send(html);
+});
+
+module.exports = router;
diff --git a/a2p-wizard-rebuild/server.js b/a2p-wizard-rebuild/server.js
new file mode 100644
index 0000000..0cb34ab
--- /dev/null
+++ b/a2p-wizard-rebuild/server.js
@@ -0,0 +1,144 @@
+require('dotenv').config();
+const express = require('express');
+const path = require('path');
+const fs = require('fs');
+
+const app = express();
+const PORT = process.env.PORT || 8899;
+const MAIN_DOMAIN = process.env.MAIN_DOMAIN || 'solvedby.us';
+
+// Ensure data directory exists
+const dataDir = path.join(__dirname, 'data');
+if (!fs.existsSync(dataDir)) {
+ fs.mkdirSync(dataDir, { recursive: true });
+}
+
+// Subdomain-to-clientId index (loaded from disk)
+const subdomainIndexPath = path.join(dataDir, 'subdomain-index.json');
+let subdomainIndex = {};
+if (fs.existsSync(subdomainIndexPath)) {
+ subdomainIndex = JSON.parse(fs.readFileSync(subdomainIndexPath, 'utf8'));
+}
+
+function saveSubdomainIndex() {
+ fs.writeFileSync(subdomainIndexPath, JSON.stringify(subdomainIndex, null, 2));
+}
+
+function getSubdomain(host) {
+ // Strip port
+ const hostname = (host || '').split(':')[0];
+ // Check if it's a subdomain of MAIN_DOMAIN
+ if (hostname.endsWith('.' + MAIN_DOMAIN)) {
+ return hostname.replace('.' + MAIN_DOMAIN, '').toLowerCase();
+ }
+ return null;
+}
+
+// View engine
+app.set('view engine', 'ejs');
+app.set('views', path.join(__dirname, 'templates'));
+
+// Middleware
+app.use(express.json({ limit: '10mb' }));
+app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+
+// ─── Subdomain routing: {clientname}.solvedby.us serves that client's site ───
+app.use((req, res, next) => {
+ const subdomain = getSubdomain(req.headers.host);
+
+ if (!subdomain) {
+ // No subdomain — serve the main wizard app
+ return next();
+ }
+
+ // Look up clientId from subdomain
+ const clientId = subdomainIndex[subdomain];
+ if (!clientId) {
+ return res.status(404).send(`
Site not found
No business site exists at ${subdomain}.${MAIN_DOMAIN}
Create one at ${MAIN_DOMAIN}
`);
+ }
+
+ const siteDir = path.join(dataDir, clientId, 'site');
+ if (!fs.existsSync(siteDir)) {
+ return res.status(404).send('Site files not found');
+ }
+
+ // Map paths to files
+ let filePath;
+ const urlPath = req.path.replace(/\/$/, '') || '';
+
+ if (urlPath === '' || urlPath === '/') {
+ filePath = path.join(siteDir, 'index.html');
+ } else if (urlPath === '/contact') {
+ filePath = path.join(siteDir, 'contact.html');
+ } else if (urlPath === '/privacy-policy') {
+ filePath = path.join(siteDir, 'privacy-policy.html');
+ } else if (urlPath === '/terms') {
+ filePath = path.join(siteDir, 'terms.html');
+ } else if (urlPath.startsWith('/assets/')) {
+ filePath = path.join(siteDir, urlPath);
+ } else if (urlPath.startsWith('/data/')) {
+ // Allow serving logos etc from data dir
+ filePath = path.join(__dirname, urlPath);
+ } else {
+ return res.status(404).send('Page not found');
+ }
+
+ if (fs.existsSync(filePath)) {
+ return res.sendFile(filePath);
+ }
+ return res.status(404).send('Page not found');
+});
+
+// ─── Main domain routes ───
+app.use('/public', express.static(path.join(__dirname, 'public')));
+app.use('/data', express.static(path.join(__dirname, 'data')));
+
+// Routes
+const apiRoutes = require('./routes/api');
+const sitesRoutes = require('./routes/sites');
+
+app.use('/api', apiRoutes);
+app.use('/sites', sitesRoutes);
+
+// Main form page
+app.get('/', (req, res) => {
+ res.render('form');
+});
+
+// Results page
+app.get('/results/:clientId', (req, res) => {
+ const { clientId } = req.params;
+ const configPath = path.join(dataDir, clientId, 'config.json');
+
+ if (!fs.existsSync(configPath)) {
+ return res.status(404).send('Project not found');
+ }
+
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
+ res.render('packet/results', { config, clientId });
+});
+
+// Download zip
+app.get('/download/:clientId', (req, res) => {
+ const { clientId } = req.params;
+ const zipPath = path.join(dataDir, clientId, 'compliance-packet.zip');
+
+ if (!fs.existsSync(zipPath)) {
+ return res.status(404).send('Zip not found');
+ }
+
+ const config = JSON.parse(fs.readFileSync(path.join(dataDir, clientId, 'config.json'), 'utf8'));
+ const safeName = config.businessName.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase();
+ res.download(zipPath, `${safeName}-a2p-compliance-packet.zip`);
+});
+
+// Export for use in API routes
+app.locals.subdomainIndex = subdomainIndex;
+app.locals.saveSubdomainIndex = saveSubdomainIndex;
+app.locals.MAIN_DOMAIN = MAIN_DOMAIN;
+
+app.listen(PORT, () => {
+ console.log(`\n🚀 A2P Compliance Wizard running at http://localhost:${PORT}`);
+ console.log(` Main domain: https://${MAIN_DOMAIN}`);
+ console.log(` Client sites: https://{name}.${MAIN_DOMAIN}\n`);
+});
diff --git a/a2p-wizard-rebuild/services/ai-generator.js b/a2p-wizard-rebuild/services/ai-generator.js
new file mode 100644
index 0000000..d23a791
--- /dev/null
+++ b/a2p-wizard-rebuild/services/ai-generator.js
@@ -0,0 +1,112 @@
+const Anthropic = require('@anthropic-ai/sdk');
+
+// Support both standard API keys and OAuth tokens
+const apiKey = process.env.ANTHROPIC_API_KEY || '';
+const clientOpts = {};
+if (apiKey.startsWith('sk-ant-oat')) {
+ // OAuth token — use as auth token instead of API key
+ clientOpts.authToken = apiKey;
+ clientOpts.apiKey = null;
+} else {
+ clientOpts.apiKey = apiKey;
+}
+const client = new Anthropic(clientOpts);
+
+async function generateContent(formData) {
+ const { businessName, businessAddress, businessEmail, businessPhone, businessDescription } = formData;
+
+ const prompt = `You are an expert A2P 10DLC compliance consultant and web copywriter. Generate ALL content for a professional business website AND a complete A2P SMS compliance packet.
+
+Business Details:
+- Legal Business Name: ${businessName}
+- Business Address: ${businessAddress}
+- Support Email: ${businessEmail}
+- Business Phone: ${businessPhone}
+- Business Description: ${businessDescription}
+
+Generate the following as a single JSON object. Be specific, professional, and thorough. All content should be tailored to this specific business and industry.
+
+IMPORTANT: Return ONLY valid JSON. No markdown code fences, no explanations outside the JSON.
+
+{
+ "website": {
+ "heroTitle": "A compelling headline for the business (5-10 words)",
+ "heroSubtitle": "A supporting subtitle (15-25 words)",
+ "ctaText": "CTA button text (2-4 words)",
+ "expandedDescription": "A 3-4 sentence professional business description",
+ "painpoints": [
+ {
+ "icon": "one of: shield, clock, chart, heart, star, zap, target, users, check, phone",
+ "title": "Pain point title (3-6 words)",
+ "description": "Pain point description (1-2 sentences)"
+ }
+ ],
+ "howItWorks": [
+ {
+ "step": 1,
+ "title": "Step title (2-5 words)",
+ "description": "Step description (1-2 sentences)"
+ }
+ ],
+ "faqItems": [
+ {
+ "question": "A relevant FAQ question",
+ "answer": "A thorough, helpful answer (2-4 sentences)"
+ }
+ ],
+ "privacyPolicy": "COMPLETE privacy policy text in HTML format. MUST include: company name, data collection practices, how info is used, cookie policy, third-party sharing policy, and CRITICALLY: 'No mobile information will be shared with third parties/affiliates for marketing/promotional purposes. All the above categories exclude text messaging originator opt-in data and consent; this information will not be shared with any third parties.' Include contact info. Make it thorough and professional.",
+ "termsOfService": "COMPLETE terms of service text in HTML format. MUST include: SMS/text messaging terms section covering message frequency, data rates, opt-out (STOP), help (HELP), supported carriers, and that consent is not a condition of purchase. Include standard legal terms. Make it thorough."
+ },
+ "a2pPacket": {
+ "campaignDescription": "A detailed description of who sends messages (brand name), who receives them (opted-in customers via website), and why (purpose). 3-5 sentences. Reference the website URL.",
+ "sampleMessages": [
+ "First sample message with brand name and [bracketed] template fields. Include opt-out.",
+ "Second sample message - different type (reminder/promo/info). Include brand name.",
+ "Third sample message - yet another type. Include HELP instructions."
+ ],
+ "optInFlowDescription": "Detailed description of how users opt in: they visit the website, fill out the contact form, check a non-pre-selected SMS consent checkbox. Describe the disclosure language on the form. Mention Privacy Policy and Terms links.",
+ "optInConfirmation": "Under 160 chars. Brand name, frequency, rates, HELP, STOP instructions.",
+ "optOutConfirmation": "Brand name + confirmation that no more messages will be sent.",
+ "helpMessage": "Brand name + support contact info + opt-out instructions.",
+ "optInKeywords": "START, SUBSCRIBE, YES",
+ "optOutKeywords": "STOP, UNSUBSCRIBE, END, QUIT, CANCEL",
+ "helpKeywords": "HELP, INFO",
+ "consentText": "The exact checkbox consent text for the opt-in form. Must include: brand name, message frequency varies, msg & data rates may apply, reply HELP for help, reply STOP to unsubscribe. Do NOT include links in the consent text itself."
+ },
+ "colorScheme": {
+ "primary": "A professional hex color that fits the business industry (e.g., #2563EB for tech, #059669 for health, #DC2626 for food)",
+ "primaryDark": "A darker shade of the primary",
+ "primaryLight": "A lighter/muted shade for backgrounds",
+ "accent": "A complementary accent color"
+ }
+}
+
+Generate 3-4 painpoints, exactly 3 howItWorks steps, and 4-6 faqItems. Make sure sample messages are realistic and include the actual business name. The privacy policy and terms of service should be COMPLETE, thorough legal documents ready for use. Return ONLY the JSON.`;
+
+ const response = await client.messages.create({
+ model: 'claude-sonnet-4-20250514',
+ max_tokens: 8000,
+ messages: [
+ { role: 'user', content: prompt }
+ ]
+ });
+
+ const text = response.content[0].text;
+
+ // Parse the JSON - handle potential markdown code fences
+ let jsonStr = text;
+ const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
+ if (jsonMatch) {
+ jsonStr = jsonMatch[1];
+ }
+
+ try {
+ const parsed = JSON.parse(jsonStr.trim());
+ return parsed;
+ } catch (e) {
+ console.error('Failed to parse AI response:', text.substring(0, 500));
+ throw new Error('AI generated invalid JSON. Please try again.');
+ }
+}
+
+module.exports = { generateContent };
diff --git a/a2p-wizard-rebuild/services/packet-assembler.js b/a2p-wizard-rebuild/services/packet-assembler.js
new file mode 100644
index 0000000..a2caa79
--- /dev/null
+++ b/a2p-wizard-rebuild/services/packet-assembler.js
@@ -0,0 +1,175 @@
+const archiver = require('archiver');
+const fs = require('fs');
+const path = require('path');
+
+async function assemblePacket(config, clientDir) {
+ const zipPath = path.join(clientDir, 'compliance-packet.zip');
+ const output = fs.createWriteStream(zipPath);
+ const archive = archiver('zip', { zlib: { level: 9 } });
+
+ return new Promise((resolve, reject) => {
+ output.on('close', () => {
+ console.log(` → Zip created: ${archive.pointer()} bytes`);
+ resolve(zipPath);
+ });
+
+ archive.on('error', (err) => reject(err));
+ archive.pipe(output);
+
+ // Add screenshots
+ const screenshotsDir = path.join(clientDir, 'screenshots');
+ if (fs.existsSync(screenshotsDir)) {
+ archive.directory(screenshotsDir, 'screenshots');
+ }
+
+ // Create compliance packet text file
+ const packetText = generatePacketText(config);
+ archive.append(packetText, { name: 'compliance-packet.txt' });
+
+ // Create a formatted HTML version
+ const packetHtml = generatePacketHtml(config);
+ archive.append(packetHtml, { name: 'compliance-packet.html' });
+
+ // Add config
+ archive.append(JSON.stringify(config, null, 2), { name: 'config.json' });
+
+ archive.finalize();
+ });
+}
+
+function generatePacketText(config) {
+ const p = config.a2pPacket;
+ const siteUrl = config.siteUrl;
+
+ return `═══════════════════════════════════════════════════════
+A2P 10DLC COMPLIANCE PACKET
+${config.businessName}
+Generated: ${new Date().toLocaleDateString()}
+═══════════════════════════════════════════════════════
+
+CAMPAIGN DESCRIPTION
+─────────────────────
+${p.campaignDescription}
+
+SAMPLE MESSAGES
+─────────────────────
+Message 1:
+${p.sampleMessages[0]}
+
+Message 2:
+${p.sampleMessages[1]}
+
+Message 3:
+${p.sampleMessages[2]}
+
+OPT-IN FLOW DESCRIPTION
+─────────────────────
+${p.optInFlowDescription}
+
+Website URL: ${siteUrl}
+Contact/Opt-in Page: ${siteUrl}/contact
+Privacy Policy: ${siteUrl}/privacy-policy
+Terms of Service: ${siteUrl}/terms
+
+OPT-IN CONFIRMATION MESSAGE
+─────────────────────
+${p.optInConfirmation}
+
+OPT-OUT CONFIRMATION MESSAGE
+─────────────────────
+${p.optOutConfirmation}
+
+HELP MESSAGE
+─────────────────────
+${p.helpMessage}
+
+KEYWORDS
+─────────────────────
+Opt-in Keywords: ${p.optInKeywords}
+Opt-out Keywords: ${p.optOutKeywords}
+Help Keywords: ${p.helpKeywords}
+
+CONSENT TEXT (on website form)
+─────────────────────
+${p.consentText}
+
+SCREENSHOTS INCLUDED
+─────────────────────
+1. homepage.png - Full homepage screenshot
+2. contact-optin.png - Opt-in form with consent checkbox
+3. privacy-policy.png - Privacy Policy page
+4. terms-of-service.png - Terms of Service page
+
+═══════════════════════════════════════════════════════
+Generated by A2P Compliance Wizard
+═══════════════════════════════════════════════════════
+`;
+}
+
+function generatePacketHtml(config) {
+ const p = config.a2pPacket;
+ const siteUrl = config.siteUrl;
+
+ return `
+
+
+
+ A2P Compliance Packet - ${config.businessName}
+
+
+
+ A2P 10DLC Compliance Packet
+ ${config.businessName} · Generated ${new Date().toLocaleDateString()}
+
+ Campaign Description
+
+
+ Sample Messages
+ ${p.sampleMessages[0]}
+ ${p.sampleMessages[1]}
+ ${p.sampleMessages[2]}
+
+ Opt-In Flow Description
+ ${p.optInFlowDescription}
+
+ Website URLs
+
+
Homepage
+
Contact/Opt-in
+
Privacy Policy
+
Terms of Service
+
+
+ Opt-In Confirmation Message
+ ${p.optInConfirmation}
+
+ Opt-Out Confirmation Message
+ ${p.optOutConfirmation}
+
+ Help Message
+ ${p.helpMessage}
+
+ Keywords
+
+
Opt-in
${p.optInKeywords}
+
Opt-out
${p.optOutKeywords}
+
Help
${p.helpKeywords}
+
+
+ Consent Text
+
+
+`;
+}
+
+module.exports = { assemblePacket };
diff --git a/a2p-wizard-rebuild/services/screenshot.js b/a2p-wizard-rebuild/services/screenshot.js
new file mode 100644
index 0000000..7bdafe0
--- /dev/null
+++ b/a2p-wizard-rebuild/services/screenshot.js
@@ -0,0 +1,75 @@
+const { chromium } = require('playwright');
+const path = require('path');
+const fs = require('fs');
+
+const BASE_URL = `http://localhost:${process.env.PORT || 8899}`;
+// Screenshots always hit localhost, even when site is publicly accessible via subdomain
+
+async function takeScreenshots(clientId, clientDir) {
+ const screenshotsDir = path.join(clientDir, 'screenshots');
+ fs.mkdirSync(screenshotsDir, { recursive: true });
+
+ let browser;
+ try {
+ browser = await chromium.launch({
+ headless: true,
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
+ });
+ const context = await browser.newContext({
+ viewport: { width: 1440, height: 900 },
+ deviceScaleFactor: 2,
+ });
+
+ const pages = [
+ {
+ url: `${BASE_URL}/sites/${clientId}/`,
+ filename: 'homepage.png',
+ fullPage: true,
+ },
+ {
+ url: `${BASE_URL}/sites/${clientId}/contact`,
+ filename: 'contact-optin.png',
+ fullPage: true,
+ },
+ {
+ url: `${BASE_URL}/sites/${clientId}/privacy-policy`,
+ filename: 'privacy-policy.png',
+ fullPage: true,
+ },
+ {
+ url: `${BASE_URL}/sites/${clientId}/terms`,
+ filename: 'terms-of-service.png',
+ fullPage: true,
+ },
+ ];
+
+ for (const pageConfig of pages) {
+ try {
+ const page = await context.newPage();
+ await page.goto(pageConfig.url, {
+ waitUntil: 'networkidle',
+ timeout: 15000
+ });
+
+ // Small delay for any CSS animations to settle
+ await page.waitForTimeout(500);
+
+ await page.screenshot({
+ path: path.join(screenshotsDir, pageConfig.filename),
+ fullPage: pageConfig.fullPage,
+ });
+
+ await page.close();
+ console.log(` → Screenshot: ${pageConfig.filename}`);
+ } catch (err) {
+ console.error(` ⚠ Failed screenshot ${pageConfig.filename}:`, err.message);
+ }
+ }
+
+ await context.close();
+ } finally {
+ if (browser) await browser.close();
+ }
+}
+
+module.exports = { takeScreenshots };
diff --git a/a2p-wizard-rebuild/services/site-builder.js b/a2p-wizard-rebuild/services/site-builder.js
new file mode 100644
index 0000000..6ffcb0c
--- /dev/null
+++ b/a2p-wizard-rebuild/services/site-builder.js
@@ -0,0 +1,71 @@
+const ejs = require('ejs');
+const fs = require('fs');
+const path = require('path');
+
+const TEMPLATES_DIR = path.join(__dirname, '..', 'templates', 'site');
+
+async function buildSite(config, clientDir) {
+ const siteDir = path.join(clientDir, 'site');
+
+ // Ensure site directory exists
+ fs.mkdirSync(siteDir, { recursive: true });
+
+ // Template data
+ const data = {
+ businessName: config.businessName,
+ businessAddress: config.businessAddress,
+ businessEmail: config.businessEmail,
+ businessPhone: config.businessPhone,
+ businessDescription: config.businessDescription,
+ logoUrl: config.logoUrl || null,
+ clientId: config.clientId,
+ siteUrl: config.siteUrl,
+ website: config.website,
+ a2pPacket: config.a2pPacket,
+ colorScheme: config.colorScheme || {
+ primary: '#2563EB',
+ primaryDark: '#1D4ED8',
+ primaryLight: '#EFF6FF',
+ accent: '#F59E0B'
+ },
+ year: new Date().getFullYear(),
+ // When served via subdomain, baseUrl is empty (root-relative links)
+ // When served via /sites/{id}/, baseUrl has the prefix
+ // We build with empty baseUrl for subdomain-first approach
+ baseUrl: ''
+ };
+
+ // Render each page
+ const pages = [
+ { template: 'index.ejs', output: 'index.html' },
+ { template: 'contact.ejs', output: 'contact.html' },
+ { template: 'privacy-policy.ejs', output: 'privacy-policy.html' },
+ { template: 'terms.ejs', output: 'terms.html' },
+ ];
+
+ for (const page of pages) {
+ const templatePath = path.join(TEMPLATES_DIR, page.template);
+ const template = fs.readFileSync(templatePath, 'utf8');
+ const html = ejs.render(template, data, { filename: templatePath });
+ fs.writeFileSync(path.join(siteDir, page.output), html);
+ }
+
+ // Copy the site CSS
+ const cssSource = path.join(__dirname, '..', 'public', 'css', 'site.css');
+ const assetsDir = path.join(siteDir, 'assets');
+ fs.mkdirSync(assetsDir, { recursive: true });
+ fs.copyFileSync(cssSource, path.join(assetsDir, 'site.css'));
+
+ // If there's a logo, copy it to the site assets
+ if (config.logoUrl) {
+ const logoSource = path.join(__dirname, '..', config.logoUrl);
+ if (fs.existsSync(logoSource)) {
+ const ext = path.extname(logoSource);
+ fs.copyFileSync(logoSource, path.join(assetsDir, `logo${ext}`));
+ }
+ }
+
+ console.log(` → Built ${pages.length} pages in ${siteDir}`);
+}
+
+module.exports = { buildSite };
diff --git a/a2p-wizard-rebuild/templates/form.ejs b/a2p-wizard-rebuild/templates/form.ejs
new file mode 100644
index 0000000..16f6c1e
--- /dev/null
+++ b/a2p-wizard-rebuild/templates/form.ejs
@@ -0,0 +1,181 @@
+
+
+
+
+
+ A2P Compliance Wizard — Generate Compliant Websites & Packets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Building Your Compliance Package
+
Preparing your request...
+
+
+
+
+
Generating AI content
+
+
+
+
Building website pages
+
+
+
+
+
Assembling compliance packet
+
+
+
+
+
+
+
+
+
+
+
diff --git a/a2p-wizard-rebuild/templates/packet/results.ejs b/a2p-wizard-rebuild/templates/packet/results.ejs
new file mode 100644
index 0000000..560045e
--- /dev/null
+++ b/a2p-wizard-rebuild/templates/packet/results.ejs
@@ -0,0 +1,624 @@
+
+
+
+
+
+ Compliance Packet — <%= config.businessName %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<%= config.siteUrl %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Campaign Description
+
<%= config.a2pPacket.campaignDescription %>
+
+
+
+
+
Opt-In Flow Description
+
<%= config.a2pPacket.optInFlowDescription %>
+
+
+
+
+
+
+
+
+
+ <% config.a2pPacket.sampleMessages.forEach((msg, i) => { %>
+
+
Sample Message <%= i + 1 %>
+
<%= msg %>
+
+
+ <% }); %>
+
+
+
+
+
+
+
+
+
Opt-In Confirmation
+
<%= config.a2pPacket.optInConfirmation %>
+
+
+
+
+
Opt-Out Confirmation
+
<%= config.a2pPacket.optOutConfirmation %>
+
+
+
+
+
Help Message
+
<%= config.a2pPacket.helpMessage %>
+
+
+
+
+
Consent Text (website form)
+
<%= config.a2pPacket.consentText %>
+
+
+
+
+
+
Opt-In Keywords
+
<%= config.a2pPacket.optInKeywords %>
+
+
+
Opt-Out Keywords
+
<%= config.a2pPacket.optOutKeywords %>
+
+
+
Help Keywords
+
<%= config.a2pPacket.helpKeywords %>
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+

+
+
+
+

+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/a2p-wizard-rebuild/templates/site/contact.ejs b/a2p-wizard-rebuild/templates/site/contact.ejs
new file mode 100644
index 0000000..bdd0ec0
--- /dev/null
+++ b/a2p-wizard-rebuild/templates/site/contact.ejs
@@ -0,0 +1,125 @@
+
+
+
+
+
+ Contact Us — <%= businessName %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/a2p-wizard-rebuild/templates/site/index.ejs b/a2p-wizard-rebuild/templates/site/index.ejs
new file mode 100644
index 0000000..ea18cdf
--- /dev/null
+++ b/a2p-wizard-rebuild/templates/site/index.ejs
@@ -0,0 +1,172 @@
+
+
+
+
+
+ <%= businessName %> — <%= website.heroTitle %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <% website.painpoints.forEach((pp) => { %>
+
+
+ <%- getIcon(pp.icon) %>
+
+
<%= pp.title %>
+
<%= pp.description %>
+
+ <% }); %>
+
+
+
+
+
+
+
+
+
+ <% website.howItWorks.forEach((step) => { %>
+
+
<%= step.step %>
+
<%= step.title %>
+
<%= step.description %>
+
+ <% }); %>
+
+
+
+
+
+
+
+
+
+ <% website.faqItems.forEach((faq) => { %>
+
+
+
+
+ <% }); %>
+
+
+
+
+
+
+
+
Ready to Get Started?
+
Reach out today and discover how we can help your business thrive.
+
+ Contact Us Today
+
+
+
+
+
+
+
+
+
+
+<%
+function getIcon(name) {
+ const icons = {
+ shield: '',
+ clock: '',
+ chart: '',
+ heart: '',
+ star: '',
+ zap: '',
+ target: '',
+ users: '',
+ check: '',
+ phone: '',
+ };
+ return icons[name] || icons.star;
+}
+%>
diff --git a/a2p-wizard-rebuild/templates/site/privacy-policy.ejs b/a2p-wizard-rebuild/templates/site/privacy-policy.ejs
new file mode 100644
index 0000000..e6c3f88
--- /dev/null
+++ b/a2p-wizard-rebuild/templates/site/privacy-policy.ejs
@@ -0,0 +1,68 @@
+
+
+
+
+
+ Privacy Policy — <%= businessName %>
+
+
+
+
+
+
+
+
+
+
+
+
Privacy Policy
+
Last updated: <%= new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) %> · <%= businessName %>
+
+ <%- website.privacyPolicy %>
+
+
+
+
+
+
+
+
+
diff --git a/a2p-wizard-rebuild/templates/site/terms.ejs b/a2p-wizard-rebuild/templates/site/terms.ejs
new file mode 100644
index 0000000..5e9ab9d
--- /dev/null
+++ b/a2p-wizard-rebuild/templates/site/terms.ejs
@@ -0,0 +1,68 @@
+
+
+
+
+
+ Terms of Service — <%= businessName %>
+
+
+
+
+
+
+
+
+
+
+
+
Terms of Service
+
Last updated: <%= new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) %> · <%= businessName %>
+
+ <%- website.termsOfService %>
+
+
+
+
+
+
+
+
+
diff --git a/closebot-sms/BUILD_PLAN.md b/closebot-sms/BUILD_PLAN.md
deleted file mode 100644
index acff1e0..0000000
--- a/closebot-sms/BUILD_PLAN.md
+++ /dev/null
@@ -1,665 +0,0 @@
-# CloseBot SMS — Definitive Build Plan
-
-> Unified app: Twilio native SMS + CloseBot AI bots. One dashboard to rule them all.
-
----
-
-## 1. TECH STACK
-
-| Layer | Technology | Why |
-|---|---|---|
-| **Frontend** | Next.js 14 (App Router) + Tailwind CSS + shadcn/ui | Dark-mode-first, server components, fast |
-| **Backend** | Next.js API routes (Edge-compatible) | Same deployment, zero CORS |
-| **Database** | SQLite via better-sqlite3 (dev) → Turso/LibSQL (prod) | State tracking, conversation history, routing config |
-| **Real-time** | Server-Sent Events (SSE) | Live conversation updates on dashboard |
-| **SMS** | Twilio Node SDK (`twilio`) | Send/receive SMS, delivery status |
-| **AI Bots** | CloseBot API (direct HTTP) | Webhook Source for inbound, API for bots/leads/metrics |
-| **Auth** | NextAuth.js with credentials provider | Simple login, protect all routes |
-| **Deploy** | Railway / Fly.io / Vercel | Needs persistent process for webhooks |
-
----
-
-## 2. SYSTEM ARCHITECTURE
-
-```
-┌─────────────┐ ┌──────────────────────────┐ ┌─────────────┐
-│ Customer │────>│ Twilio (SMS) │────>│ CloseBot │
-│ Phone │<────│ │<────│ SMS App │
-└─────────────┘ └──────────────────────────┘ └──────┬──────┘
- │
- ┌──────────────────────────┐ │
- │ CloseBot API │<───────────┘
- │ (Webhook Source) │────────────┐
- └──────────────────────────┘ │
- v
- ┌──────────────┐
- │ SQLite DB │
- │ (state/logs) │
- └──────────────┘
-```
-
-**Message Flow — Inbound:**
-1. Customer sends SMS to Twilio number
-2. Twilio POSTs to `POST /api/webhooks/twilio/inbound`
-3. App looks up routing: which Twilio number → which CloseBot bot/source
-4. App sends inbound event to CloseBot via `POST /webhook/event/{sourceId}` with:
- - `type: "message"`
- - `contactId` (phone number as unique identifier)
- - `message` (SMS body)
- - `state` (JSON: `{ twilioNumber, phoneFrom, messageSid }`)
-5. CloseBot processes through bot flow
-6. CloseBot POSTs response to our webhook retrieval URL: `POST /api/webhooks/closebot/response`
-7. App receives response, extracts `state` to get the Twilio number + customer phone
-8. App sends SMS via Twilio API
-9. App logs everything to SQLite + broadcasts via SSE for live dashboard
-
-**Message Flow — Manual Override (from Conversations view):**
-1. User types message in the conversation UI
-2. App sends SMS directly via Twilio (bypassing CloseBot)
-3. Logs to SQLite with `source: "manual"` flag
-
----
-
-## 3. DATABASE SCHEMA
-
-```sql
--- Routes: Twilio number → CloseBot bot mapping
-CREATE TABLE routes (
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(8)))),
- twilio_number TEXT NOT NULL UNIQUE,
- twilio_number_sid TEXT,
- closebot_source_id TEXT NOT NULL,
- closebot_bot_id TEXT,
- bot_name TEXT,
- greeting_message TEXT,
- after_hours_reply TEXT,
- business_hours_json TEXT, -- {"mon": {"start": "09:00", "end": "17:00"}, ...}
- max_concurrent INTEGER DEFAULT 50,
- active BOOLEAN DEFAULT TRUE,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
-);
-
--- Contacts: phone numbers we've interacted with
-CREATE TABLE contacts (
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(8)))),
- phone TEXT NOT NULL UNIQUE,
- name TEXT,
- email TEXT,
- status TEXT DEFAULT 'new', -- new, active, qualified, booked, closed, cold
- closebot_lead_id TEXT,
- assigned_bot_id TEXT,
- assigned_route_id TEXT REFERENCES routes(id),
- tags TEXT, -- JSON array
- fields_json TEXT, -- collected fields from CloseBot
- first_contact DATETIME DEFAULT CURRENT_TIMESTAMP,
- last_contact DATETIME DEFAULT CURRENT_TIMESTAMP,
- message_count INTEGER DEFAULT 0
-);
-
--- Messages: every SMS in/out
-CREATE TABLE messages (
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(8)))),
- contact_id TEXT REFERENCES contacts(id),
- route_id TEXT REFERENCES routes(id),
- direction TEXT NOT NULL, -- 'inbound' | 'outbound'
- source TEXT NOT NULL, -- 'customer' | 'bot' | 'manual'
- body TEXT NOT NULL,
- twilio_sid TEXT,
- twilio_status TEXT, -- queued, sent, delivered, failed, undelivered
- closebot_message_id TEXT,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
-);
-
--- Settings: app-wide config
-CREATE TABLE settings (
- key TEXT PRIMARY KEY,
- value TEXT NOT NULL,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
-);
-
--- Events: activity log for dashboard feed
-CREATE TABLE events (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- type TEXT NOT NULL, -- 'message_in', 'message_out', 'booking', 'new_lead', 'status_change'
- contact_id TEXT REFERENCES contacts(id),
- route_id TEXT REFERENCES routes(id),
- data_json TEXT,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
-);
-```
-
----
-
-## 4. API ROUTES
-
-### Webhook Endpoints (public, Twilio/CloseBot call these)
-| Route | Method | Purpose |
-|---|---|---|
-| `/api/webhooks/twilio/inbound` | POST | Receive inbound SMS from Twilio |
-| `/api/webhooks/twilio/status` | POST | Receive delivery status updates |
-| `/api/webhooks/closebot/response` | POST | Receive CloseBot bot responses |
-
-### Internal API (authenticated, frontend calls these)
-| Route | Method | Purpose |
-|---|---|---|
-| `/api/dashboard/stats` | GET | Active convos, messages today, bookings, response rate |
-| `/api/dashboard/activity` | GET | Recent activity feed (SSE endpoint) |
-| `/api/conversations` | GET | List conversations with pagination/filters |
-| `/api/conversations/[contactId]` | GET | Get single conversation with messages |
-| `/api/conversations/[contactId]/send` | POST | Send manual SMS message |
-| `/api/bots` | GET | List CloseBot bots (proxied from CloseBot API) |
-| `/api/bots/[id]` | GET | Get bot details + metrics |
-| `/api/contacts` | GET | List/search contacts with filters |
-| `/api/contacts/[id]` | GET/PUT/DELETE | Contact CRUD |
-| `/api/contacts/export` | GET | Export CSV |
-| `/api/analytics/overview` | GET | Summary metrics with date range |
-| `/api/analytics/messages` | GET | Message volume time series |
-| `/api/analytics/bots` | GET | Per-bot conversation counts |
-| `/api/analytics/outcomes` | GET | Outcome distribution |
-| `/api/analytics/leaderboard` | GET | Top performing bots |
-| `/api/routes` | GET/POST | List routes, create new route |
-| `/api/routes/[id]` | GET/PUT/DELETE | Route CRUD |
-| `/api/settings` | GET/PUT | App settings (Twilio creds, CloseBot key) |
-| `/api/settings/test-connection` | POST | Test Twilio + CloseBot connections |
-
----
-
-## 5. PAGE-BY-PAGE UI SPEC
-
-### 5A. DASHBOARD (`/`)
-**Matches mockup: closebot-sms-dashboard.png**
-
-**Layout:**
-- Left sidebar (240px): Logo "CloseBot SMS" + 6 nav items with icons
-- Top bar: Global search input
-- Main content area
-
-**Components:**
-1. **Stat Cards Row** — 4 cards in a grid
- - Active Conversations (chat bubble icon, count from DB)
- - Messages Today (envelope icon, count from DB where date = today)
- - Bookings Made (calendar icon, from CloseBot metrics API)
- - Response Rate (trending icon, calculated: bot_responses / inbound_messages * 100)
- - Each card: dark glass-morphism background (`bg-slate-800/50 backdrop-blur`), cyan border glow, icon top-left, label + big number
-
-2. **Real-time Activity Table**
- - Columns: SMS (phone number), Bot Name, Badges (status pill), Timestamp
- - Status pills: `active` (green), `pending` (yellow), `closed` (gray)
- - Auto-updates via SSE — new rows slide in at top
- - Click row → navigate to conversation
-
-**Data sources:**
-- Stats: `GET /api/dashboard/stats` → queries SQLite counts
-- Activity: `GET /api/dashboard/activity` → SSE stream from events table
-
----
-
-### 5B. CONVERSATIONS (`/conversations`)
-**Matches mockup: closebot-sms-conversations.png**
-
-**Layout:** Two-panel split (left 380px list, right flexible chat)
-
-**Left Panel — Conversation List:**
-- Search bar with filter icon + settings icon
-- "All Filters" dropdown (by status, bot, date range)
-- Conversation rows: avatar (generated from initials), name, phone, last message preview (truncated), timestamp, unread badge (cyan circle with count)
-- Selected conversation highlighted with left cyan border
-- Sorted by most recent message
-
-**Right Panel — Chat Thread:**
-- Header: avatar, name, phone number, "CloseBot SMS AI" badge, status dot + "Active" label
-- Message bubbles:
- - Inbound (customer): dark gray background (`bg-slate-700`), left-aligned with avatar
- - Outbound (bot): cyan/blue gradient background, right-aligned
- - Each bubble shows text, no timestamps on individual messages (clean look)
-- Bottom: text input "Type a message..." with cyan send button
- - Sending from here = manual override, bypasses CloseBot
-
-**Data sources:**
-- List: `GET /api/conversations?search=&status=&bot=&page=`
-- Messages: `GET /api/conversations/[contactId]`
-- Send: `POST /api/conversations/[contactId]/send`
-- Live updates: SSE for new messages appended to thread
-
----
-
-### 5C. BOTS (`/bots`)
-**Matches mockup: closebot-sms-bots.png**
-
-**Layout:** Grid of bot cards (3 columns on desktop)
-
-**Top Bar:**
-- "Bot Management" heading
-- Search bots input + filter icon + sort icon
-- "+ Create New Bot" button (cyan, calls CloseBot create_bot_with_ai)
-
-**Bot Cards:**
-- Card header: bot name in bold, gradient top border (different colors per bot)
-- Fields: Status toggle (Active/Inactive), Twilio # (from route mapping), Messages count, Conversion % (from CloseBot metrics), Last Active
-- Inactive bots appear dimmed/grayed
-- Click expand chevron → expanded card shows:
- - Connected Sources list
- - Recent Performance sparkline (mini chart, last 7 days)
- - "Edit Flow" button (opens CloseBot dashboard in new tab)
-
-**Data sources:**
-- Bots list: `GET /api/bots` → proxies CloseBot `GET /bot` + merges route data from SQLite
-- Metrics: CloseBot `GET /botMetric/agencySummary`
-- Toggle active: `PUT /api/routes/[id]` to enable/disable route
-
----
-
-### 5D. CONTACTS (`/contacts`)
-**Matches mockup: closebot-sms-contacts.png**
-
-**Layout:** Full-width data table with optional slide-out detail panel
-
-**Top Bar:**
-- Search input
-- Filter dropdowns: "Filter by Status" (hot/warm/cold/new), "Filter by Bot", date range picker
-- "Import Contacts" button (cyan outline) + "Export CSV" button (cyan outline)
-
-**Table Columns:**
-- Name (bold)
-- Phone Number
-- Assigned Bot
-- Status (colored dot: red=Hot Lead, orange=Warm, gray=Cold + text label)
-- Messages Exchanged (number)
-- Last Contact (relative time: "2 hours ago", "Yesterday")
-- Tags (colored pills)
-
-**Slide-out Detail Panel (on row click):**
-- Contact name (large, bold)
-- Phone, Email, Company fields
-- "Conversation Summary" section — AI-generated from CloseBot
- - Bullet points: "Product Interest", "Budget Discussion", etc.
-- "Field Values Collected" — mini table of CloseBot-collected fields (Company Size, Industry, Priority)
-- Action buttons: "View Full History" + "Send Message" (cyan)
-
-**Data sources:**
-- Table: `GET /api/contacts?search=&status=&bot=&dateFrom=&dateTo=&page=&limit=`
-- Detail: `GET /api/contacts/[id]`
-- Export: `GET /api/contacts/export` → CSV download
-- Import: `POST /api/contacts/import` (CSV upload, creates contacts + optionally sends first message)
-
----
-
-### 5E. ANALYTICS (`/analytics`)
-**Matches mockup: closebot-sms-analytics.png**
-
-**Layout:** Scrollable dashboard with multiple chart sections
-
-**Top Bar:**
-- "Analytics" heading
-- Tab navigation: Dashboard, Conversations, Bots, Contacts, Settings
-- Date range picker dropdown (top right): "Last 30 Days (Oct 1 - Oct 30, 2024)"
-
-**Section 1 — Stat Cards (4 across):**
-- Total Conversations (large number + sparkline + % change vs last period)
-- Booking Rate (percentage + sparkline + change)
-- Avg Response Time (duration format "1m 45s" + sparkline + change)
-- Customer Satisfaction (rating "4.8/5" + sparkline + change)
-- Each card has colored gradient background (red→pink, green, blue, purple)
-
-**Section 2 — Messages Over Time (full width):**
-- Line chart with dual lines: Inbound (blue) vs Outbound (green)
-- X-axis: dates over selected range
-- Y-axis: message count
-- Tooltip on hover showing exact values
-- Chart library: Recharts (React-native, works with Next.js)
-
-**Section 3 — Two charts side by side:**
-- Left: "Conversations by Bot" — horizontal bar chart with colored bars per bot
-- Right: "Outcome Distribution" — donut chart with segments (Booked 40%, Qualified 30%, Dropped 18%, Pending 12%), total in center
-
-**Section 4 — Top Performing Bots:**
-- Mini leaderboard table: Rank, Bot Name (with icon), Conversations, Booking Rate, CSAT Score
-- Sorted by booking rate descending
-
-**Data sources:**
-- Stats: `GET /api/analytics/overview?start=&end=`
-- Messages chart: `GET /api/analytics/messages?start=&end=&resolution=daily`
-- By bot: `GET /api/analytics/bots?start=&end=`
-- Outcomes: `GET /api/analytics/outcomes?start=&end=`
-- Leaderboard: `GET /api/analytics/leaderboard?start=&end=`
-- All backed by SQLite queries (message counts, conversation outcomes) + CloseBot metrics API for bookings/CSAT
-
----
-
-### 5F. ROUTING (`/routing`)
-**Matches mockup: closebot-sms-routing.png**
-
-**Layout:** Three-column visual routing display
-
-**Top Bar:**
-- Breadcrumb: "Routing / Phone Number Routing"
-- Top nav tabs: Dashboard, Routing, Bots, Analytics, Settings
-- "+ Add New Route" button (top right)
-- Search bar + filter icon
-
-**Three Columns:**
-- **Left: Twilio Phone Numbers** — list of phone number cards with phone icon
-- **Center: Routing** — animated connection lines (CSS/SVG) linking numbers to bots
-- **Right: CloseBot Bots & Sources** — bot cards with: name, source badge (WEB_LEAD, EMAIL_INQUIRY, etc.), Active/Paused toggle, message count badge, "Configure" button
-
-**Expanded Route Configuration (on Configure click):**
-- Modal/drawer overlaying the route:
- - Greeting Message Override (textarea, optional)
- - After-Hours Auto-Reply Text (textarea)
- - Business Hours Schedule — day-of-week toggle grid (Mon-Sun), time range "Mon-Fri: 9:00 AM - 5:00 PM", "Add Exception" link
- - Max Concurrent Conversations — slider (1-100) with current value displayed
- - Save + Close Configuration buttons
-
-**Data sources:**
-- Routes: `GET /api/routes` → SQLite routes table
-- Twilio numbers: fetched from Twilio API on settings save, cached
-- Bots: `GET /api/bots` → CloseBot API
-- Update: `PUT /api/routes/[id]` with config JSON
-
----
-
-### 5G. SETTINGS (`/settings`)
-**Matches mockup: closebot-sms-settings.png**
-
-**Layout:** Two-column card layout
-
-**Cards:**
-1. **Twilio Connection** — Account SID input (masked), Auth Token input (masked), "Connection Status" indicator (green dot + "Connected")
-2. **CloseBot Connection** — API Key input (masked), Webhook Source ID (display), "Connection Status" indicator
-3. **Phone Numbers** — list of Twilio numbers with assigned bot + toggle switch
-4. **Notifications** — toggles: Email Alerts, SMS Delivery Failures, New Lead Alerts
-5. **Webhook URLs** — read-only display of:
- - Inbound URL (the URL you give to Twilio)
- - Response URL (the URL you give to CloseBot Webhook Source)
- - Copy buttons next to each
-
-**Data sources:**
-- Settings: `GET/PUT /api/settings`
-- Test: `POST /api/settings/test-connection` → pings both APIs
-
----
-
-## 6. FILE STRUCTURE
-
-```
-closebot-sms/
-├── package.json
-├── next.config.js
-├── tailwind.config.ts
-├── tsconfig.json
-├── .env.local.example
-├── BUILD_PLAN.md
-├── README.md
-├── prisma/ (or drizzle/)
-│ └── schema.sql
-├── src/
-│ ├── app/
-│ │ ├── layout.tsx # Root layout with sidebar
-│ │ ├── page.tsx # Dashboard
-│ │ ├── conversations/
-│ │ │ └── page.tsx # Conversations split view
-│ │ ├── bots/
-│ │ │ └── page.tsx # Bot grid
-│ │ ├── contacts/
-│ │ │ └── page.tsx # Contacts table
-│ │ ├── analytics/
-│ │ │ └── page.tsx # Analytics dashboard
-│ │ ├── routing/
-│ │ │ └── page.tsx # Phone number routing
-│ │ ├── settings/
-│ │ │ └── page.tsx # Settings/config
-│ │ └── api/
-│ │ ├── webhooks/
-│ │ │ ├── twilio/
-│ │ │ │ ├── inbound/route.ts
-│ │ │ │ └── status/route.ts
-│ │ │ └── closebot/
-│ │ │ └── response/route.ts
-│ │ ├── dashboard/
-│ │ │ ├── stats/route.ts
-│ │ │ └── activity/route.ts # SSE
-│ │ ├── conversations/
-│ │ │ ├── route.ts
-│ │ │ └── [contactId]/
-│ │ │ ├── route.ts
-│ │ │ └── send/route.ts
-│ │ ├── bots/
-│ │ │ ├── route.ts
-│ │ │ └── [id]/route.ts
-│ │ ├── contacts/
-│ │ │ ├── route.ts
-│ │ │ ├── export/route.ts
-│ │ │ └── [id]/route.ts
-│ │ ├── analytics/
-│ │ │ ├── overview/route.ts
-│ │ │ ├── messages/route.ts
-│ │ │ ├── bots/route.ts
-│ │ │ ├── outcomes/route.ts
-│ │ │ └── leaderboard/route.ts
-│ │ ├── routes/
-│ │ │ ├── route.ts
-│ │ │ └── [id]/route.ts
-│ │ └── settings/
-│ │ ├── route.ts
-│ │ └── test-connection/route.ts
-│ ├── components/
-│ │ ├── layout/
-│ │ │ ├── sidebar.tsx
-│ │ │ ├── topbar.tsx
-│ │ │ └── nav-item.tsx
-│ │ ├── dashboard/
-│ │ │ ├── stat-card.tsx
-│ │ │ └── activity-feed.tsx
-│ │ ├── conversations/
-│ │ │ ├── conversation-list.tsx
-│ │ │ ├── conversation-item.tsx
-│ │ │ ├── chat-thread.tsx
-│ │ │ ├── message-bubble.tsx
-│ │ │ └── chat-input.tsx
-│ │ ├── bots/
-│ │ │ ├── bot-grid.tsx
-│ │ │ └── bot-card.tsx
-│ │ ├── contacts/
-│ │ │ ├── contacts-table.tsx
-│ │ │ ├── contact-detail-panel.tsx
-│ │ │ └── contact-filters.tsx
-│ │ ├── analytics/
-│ │ │ ├── stat-card-sparkline.tsx
-│ │ │ ├── messages-chart.tsx
-│ │ │ ├── bots-bar-chart.tsx
-│ │ │ ├── outcome-donut.tsx
-│ │ │ └── bot-leaderboard.tsx
-│ │ ├── routing/
-│ │ │ ├── routing-view.tsx
-│ │ │ ├── phone-number-card.tsx
-│ │ │ ├── bot-route-card.tsx
-│ │ │ ├── connection-lines.tsx # SVG animated lines
-│ │ │ └── route-config-modal.tsx
-│ │ └── settings/
-│ │ ├── twilio-card.tsx
-│ │ ├── closebot-card.tsx
-│ │ ├── phone-numbers-card.tsx
-│ │ ├── notifications-card.tsx
-│ │ └── webhook-urls-card.tsx
-│ ├── lib/
-│ │ ├── db.ts # SQLite connection + queries
-│ │ ├── twilio.ts # Twilio client wrapper
-│ │ ├── closebot.ts # CloseBot API client (reuse from MCP server)
-│ │ ├── sse.ts # SSE broadcast utility
-│ │ └── utils.ts # Formatters, helpers
-│ └── styles/
-│ └── globals.css # Tailwind base + custom dark theme
-```
-
----
-
-## 7. DESIGN SYSTEM
-
-**Colors (from mockups):**
-```css
---bg-primary: #0f1729; /* deepest navy */
---bg-secondary: #1a2332; /* card backgrounds */
---bg-tertiary: #1e293b; /* elevated surfaces */
---bg-hover: #2d3748; /* table row hover */
---text-primary: #e2e8f0; /* main text */
---text-secondary: #94a3b8; /* muted text */
---text-muted: #64748b; /* timestamps, labels */
---accent-cyan: #22d3ee; /* primary accent */
---accent-blue: #3b82f6; /* secondary accent */
---accent-green: #22c55e; /* success, active */
---accent-yellow: #eab308; /* warning, pending */
---accent-red: #ef4444; /* error, hot lead */
---accent-orange: #f97316; /* warm */
---accent-purple: #a855f7; /* bot cards */
---border: #334155; /* subtle borders */
---border-glow: rgba(34, 211, 238, 0.2); /* cyan card glow */
-```
-
-**Typography:**
-- Font: `Inter` (system-ui fallback)
-- Headings: 600-700 weight
-- Body: 400 weight
-- Monospace numbers: `font-variant-numeric: tabular-nums`
-
-**Card Style:**
-```css
-.card {
- background: rgba(30, 41, 59, 0.5);
- backdrop-filter: blur(12px);
- border: 1px solid rgba(51, 65, 85, 0.5);
- border-radius: 12px;
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
-}
-```
-
-**Status Badges:**
-- Active: `bg-green-500/20 text-green-400 border-green-500/30`
-- Pending: `bg-yellow-500/20 text-yellow-400 border-yellow-500/30`
-- Closed: `bg-gray-500/20 text-gray-400 border-gray-500/30`
-
----
-
-## 8. BUILD PHASES
-
-### Phase 1 — Foundation (Day 1)
-- [ ] Next.js project scaffold with Tailwind + shadcn/ui
-- [ ] SQLite schema + db.ts with all queries
-- [ ] Global layout with sidebar navigation (matches dashboard mockup)
-- [ ] Dark theme CSS variables
-- [ ] Environment config (.env.local)
-- [ ] Twilio client wrapper
-- [ ] CloseBot API client (port from closebot-mcp)
-
-### Phase 2 — Core Webhook Bridge (Day 1-2)
-- [ ] `POST /api/webhooks/twilio/inbound` — receive SMS, forward to CloseBot
-- [ ] `POST /api/webhooks/closebot/response` — receive bot reply, send via Twilio
-- [ ] `POST /api/webhooks/twilio/status` — delivery status updates
-- [ ] Contact auto-creation on first inbound
-- [ ] Message logging to SQLite
-- [ ] Route lookup logic (Twilio number → CloseBot source)
-- [ ] Business hours check + after-hours auto-reply
-- [ ] State passthrough (phone + route info in CloseBot state field)
-
-### Phase 3 — Dashboard + Real-time (Day 2)
-- [ ] Dashboard page with 4 stat cards
-- [ ] Real-time activity feed (SSE)
-- [ ] Activity table with status badges
-- [ ] Auto-refresh stats
-
-### Phase 4 — Conversations (Day 2-3)
-- [ ] Conversation list with search/filter
-- [ ] Chat thread with message bubbles
-- [ ] Manual message send
-- [ ] Unread badges
-- [ ] Live message updates via SSE
-
-### Phase 5 — Bots + Contacts (Day 3)
-- [ ] Bot grid with cards from CloseBot API
-- [ ] Bot status toggle (enable/disable route)
-- [ ] Contacts table with all filters
-- [ ] Slide-out detail panel
-- [ ] CSV export
-
-### Phase 6 — Analytics (Day 3-4)
-- [ ] Stat cards with sparklines
-- [ ] Messages over time chart (Recharts)
-- [ ] Conversations by bot bar chart
-- [ ] Outcome distribution donut chart
-- [ ] Bot leaderboard table
-- [ ] Date range picker
-
-### Phase 7 — Routing + Settings (Day 4)
-- [ ] Phone number routing view with visual connections
-- [ ] Route config modal (business hours, greeting, max concurrent)
-- [ ] Settings page with credential cards
-- [ ] Connection testing
-- [ ] Webhook URL display with copy
-
-### Phase 8 — Polish (Day 4-5)
-- [ ] Loading states + skeletons
-- [ ] Error handling + toast notifications
-- [ ] Mobile responsive adjustments
-- [ ] Auth gate (simple login)
-- [ ] README with deploy instructions
-
----
-
-## 9. EXTERNAL DEPENDENCIES
-
-### npm packages:
-```json
-{
- "dependencies": {
- "next": "^14.2",
- "react": "^18.3",
- "twilio": "^5.0",
- "better-sqlite3": "^11.0",
- "recharts": "^2.12",
- "lucide-react": "^0.400",
- "@radix-ui/react-dialog": "^1.0",
- "@radix-ui/react-dropdown-menu": "^2.0",
- "@radix-ui/react-toggle": "^1.0",
- "@radix-ui/react-slider": "^1.0",
- "class-variance-authority": "^0.7",
- "clsx": "^2.1",
- "tailwind-merge": "^2.3",
- "next-auth": "^4.24"
- }
-}
-```
-
-### Environment Variables:
-```env
-# Twilio
-TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-TWILIO_AUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-
-# CloseBot
-CLOSEBOT_API_KEY=cb_xxxxxxxxxxxxxxxx
-
-# App
-NEXTAUTH_SECRET=random-secret-here
-NEXTAUTH_URL=https://your-domain.com
-APP_URL=https://your-domain.com # Used to construct webhook URLs
-
-# Database
-DATABASE_PATH=./data/closebot-sms.db
-```
-
----
-
-## 10. WHAT MAKES THIS DIFFERENT
-
-1. **No GHL required** — CloseBot's Webhook Source is the channel, Twilio is the pipe
-2. **Multi-tenant ready** — one app, many Twilio numbers, many bots
-3. **Human takeover** — click into any convo and type a manual reply
-4. **Business hours built-in** — auto-reply after hours, resume bot in morning
-5. **Full audit trail** — every message logged with direction, source, status
-6. **Real-time everything** — SSE for live dashboard, no polling
-7. **CloseBot metrics integrated** — booking rates, leaderboards, CSAT from their API
-8. **$0.0079/SMS** — Twilio pricing, no GHL middleman markup
-
----
-
-*Plan version: 1.0 | Last updated: 2026-02-06*
diff --git a/closebot-sms/app/data/closebot-sms.db b/closebot-sms/app/data/closebot-sms.db
deleted file mode 100644
index db7a745..0000000
Binary files a/closebot-sms/app/data/closebot-sms.db and /dev/null differ
diff --git a/closebot-sms/app/data/closebot-sms.db-shm b/closebot-sms/app/data/closebot-sms.db-shm
deleted file mode 100644
index ae80714..0000000
Binary files a/closebot-sms/app/data/closebot-sms.db-shm and /dev/null differ
diff --git a/closebot-sms/app/data/closebot-sms.db-wal b/closebot-sms/app/data/closebot-sms.db-wal
deleted file mode 100644
index a36309c..0000000
Binary files a/closebot-sms/app/data/closebot-sms.db-wal and /dev/null differ
diff --git a/closebot-sms/app/next-env.d.ts b/closebot-sms/app/next-env.d.ts
deleted file mode 100644
index 40c3d68..0000000
--- a/closebot-sms/app/next-env.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-///
-///
-
-// NOTE: This file should not be edited
-// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
diff --git a/closebot-sms/app/next.config.js b/closebot-sms/app/next.config.js
deleted file mode 100644
index d6b435e..0000000
--- a/closebot-sms/app/next.config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/** @type {import('next').NextConfig} */
-const nextConfig = {
- experimental: {
- serverComponentsExternalPackages: ['better-sqlite3'],
- },
-};
-
-module.exports = nextConfig;
diff --git a/closebot-sms/app/package.json b/closebot-sms/app/package.json
deleted file mode 100644
index f063af3..0000000
--- a/closebot-sms/app/package.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "name": "closebot-sms",
- "version": "1.0.0",
- "private": true,
- "scripts": {
- "dev": "next dev",
- "build": "next build",
- "start": "next start",
- "lint": "next lint"
- },
- "dependencies": {
- "better-sqlite3": "^11.0.0",
- "class-variance-authority": "^0.7.0",
- "clsx": "^2.1.0",
- "handlebars": "^4.7.8",
- "lucide-react": "^0.400.0",
- "next": "^14.2.0",
- "react": "^18.3.0",
- "react-dom": "^18.3.0",
- "recharts": "^2.12.0",
- "tailwind-merge": "^2.3.0",
- "twilio": "^5.0.0"
- },
- "devDependencies": {
- "@types/better-sqlite3": "^7.6.0",
- "@types/node": "^20.0.0",
- "@types/react": "^18.3.0",
- "@types/react-dom": "^18.3.0",
- "autoprefixer": "^10.4.0",
- "postcss": "^8.4.0",
- "tailwindcss": "^3.4.0",
- "typescript": "^5.4.0"
- }
-}
diff --git a/closebot-sms/app/postcss.config.js b/closebot-sms/app/postcss.config.js
deleted file mode 100644
index 12a703d..0000000
--- a/closebot-sms/app/postcss.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = {
- plugins: {
- tailwindcss: {},
- autoprefixer: {},
- },
-};
diff --git a/closebot-sms/app/src/app/a2p/page.tsx b/closebot-sms/app/src/app/a2p/page.tsx
deleted file mode 100644
index c755652..0000000
--- a/closebot-sms/app/src/app/a2p/page.tsx
+++ /dev/null
@@ -1,349 +0,0 @@
-'use client';
-
-import { useEffect, useState, useCallback } from 'react';
-import Link from 'next/link';
-import {
- Shield,
- Plus,
- RefreshCw,
- Clock,
- CheckCircle2,
- XCircle,
- AlertTriangle,
- MoreVertical,
- Eye,
- RotateCcw,
- Trash2,
- FileText,
-} from 'lucide-react';
-import { cn } from '@/lib/utils';
-import type { SubmissionStatus } from '@/lib/a2p-types';
-import { formatStatus, getStatusColor, formatUseCase } from '@/lib/a2p-types';
-
-interface A2PRegistration {
- id: string;
- business_name: string;
- status: SubmissionStatus;
- brand_trust_score: number | null;
- failure_reason: string | null;
- attempt_count: number;
- created_at: string;
- updated_at: string;
- input: {
- business: { businessName: string };
- campaign: { useCase: string };
- };
-}
-
-interface A2PStats {
- total: number;
- pending: number;
- approved: number;
- failed: number;
-}
-
-function StatusBadge({ status }: { status: SubmissionStatus }) {
- const color = getStatusColor(status);
- const colorMap: Record = {
- green: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',
- red: 'bg-red-500/10 text-red-400 border-red-500/20',
- yellow: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20',
- blue: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
- amber: 'bg-amber-500/10 text-amber-400 border-amber-500/20',
- orange: 'bg-orange-500/10 text-orange-400 border-orange-500/20',
- slate: 'bg-slate-500/10 text-slate-400 border-slate-500/20',
- };
-
- return (
-
- {formatStatus(status)}
-
- );
-}
-
-export default function A2PPage() {
- const [registrations, setRegistrations] = useState([]);
- const [stats, setStats] = useState({ total: 0, pending: 0, approved: 0, failed: 0 });
- const [loading, setLoading] = useState(true);
- const [actionMenuId, setActionMenuId] = useState(null);
-
- const fetchData = useCallback(async () => {
- try {
- setLoading(true);
- const res = await fetch('/api/a2p?stats=true');
- const data = await res.json();
- setRegistrations(data.registrations || []);
- setStats(data.stats || { total: 0, pending: 0, approved: 0, failed: 0 });
- } catch (err) {
- console.error('Failed to fetch A2P data:', err);
- } finally {
- setLoading(false);
- }
- }, []);
-
- useEffect(() => { fetchData(); }, [fetchData]);
-
- async function handleRetry(id: string) {
- try {
- await fetch(`/api/a2p/${id}/retry`, { method: 'POST' });
- setActionMenuId(null);
- fetchData();
- } catch (err) {
- console.error('Retry failed:', err);
- }
- }
-
- async function handleDelete(id: string) {
- if (!confirm('Are you sure you want to delete this registration?')) return;
- try {
- await fetch(`/api/a2p/${id}`, { method: 'DELETE' });
- setActionMenuId(null);
- fetchData();
- } catch (err) {
- console.error('Delete failed:', err);
- }
- }
-
- const statCards = [
- {
- label: 'Total Registrations',
- value: stats.total,
- icon: FileText,
- color: 'text-cyan-400',
- bgColor: 'bg-cyan-500/10',
- borderColor: 'border-cyan-500/20',
- },
- {
- label: 'Pending',
- value: stats.pending,
- icon: Clock,
- color: 'text-yellow-400',
- bgColor: 'bg-yellow-500/10',
- borderColor: 'border-yellow-500/20',
- },
- {
- label: 'Approved',
- value: stats.approved,
- icon: CheckCircle2,
- color: 'text-emerald-400',
- bgColor: 'bg-emerald-500/10',
- borderColor: 'border-emerald-500/20',
- },
- {
- label: 'Failed',
- value: stats.failed,
- icon: XCircle,
- color: 'text-red-400',
- bgColor: 'bg-red-500/10',
- borderColor: 'border-red-500/20',
- },
- ];
-
- return (
-
- {/* Header */}
-
-
-
-
-
-
-
-
A2P Registration
-
- Manage 10DLC brand & campaign registrations
-
-
-
-
-
-
-
-
- New Registration
-
-
-
-
- {/* Stats Cards */}
-
- {statCards.map((card) => {
- const Icon = card.icon;
- return (
-
-
-
-
{card.label}
-
{card.value}
-
-
-
-
-
-
- );
- })}
-
-
- {/* Registration Table */}
-
-
-
All Registrations
-
-
- {loading ? (
-
-
- Loading registrations...
-
- ) : registrations.length === 0 ? (
-
-
-
-
-
No registrations yet
-
- Create your first A2P 10DLC registration to start sending compliant business messages.
-
-
-
- Start Registration
-
-
- ) : (
-
-
-
-
- |
- Business Name
- |
-
- Status
- |
-
- Brand Score
- |
-
- Use Case
- |
-
- Created
- |
-
- Actions
- |
-
-
-
- {registrations.map((reg) => (
-
- |
-
- {reg.business_name}
-
- |
-
-
- {reg.failure_reason && (
-
-
- {reg.failure_reason}
-
- )}
- |
-
- {reg.brand_trust_score != null ? (
- = 75 ? 'text-emerald-400' :
- reg.brand_trust_score >= 50 ? 'text-yellow-400' : 'text-red-400'
- )}>
- {reg.brand_trust_score}
-
- ) : (
- —
- )}
- |
-
-
- {reg.input?.campaign?.useCase
- ? formatUseCase(reg.input.campaign.useCase as Parameters[0])
- : '—'}
-
- |
-
-
- {new Date(reg.created_at).toLocaleDateString()}
-
- |
-
-
-
- {actionMenuId === reg.id && (
-
- setActionMenuId(null)}
- >
-
- View Details
-
- {['brand_failed', 'campaign_failed', 'manual_review'].includes(reg.status) && (
-
- )}
-
-
- )}
-
- |
-
- ))}
-
-
-
- )}
-
-
- );
-}
diff --git a/closebot-sms/app/src/app/a2p/register/page.tsx b/closebot-sms/app/src/app/a2p/register/page.tsx
deleted file mode 100644
index 507de1b..0000000
--- a/closebot-sms/app/src/app/a2p/register/page.tsx
+++ /dev/null
@@ -1,995 +0,0 @@
-'use client';
-
-import { useState, useEffect, useCallback } from 'react';
-import { useRouter } from 'next/navigation';
-import Link from 'next/link';
-import {
- ArrowLeft,
- ArrowRight,
- Building2,
- User,
- MapPin,
- MessageSquare,
- CheckCircle,
- Plus,
- Trash2,
- Shield,
- Loader2,
-} from 'lucide-react';
-import { cn } from '@/lib/utils';
-import type {
- BusinessInfo,
- AuthorizedRep,
- BusinessAddress,
- CampaignInfo,
- BusinessType,
- BusinessIndustry,
- RegistrationIdentifier,
- JobPosition,
- CampaignUseCase,
- OptInType,
- CompanyType,
-} from '@/lib/a2p-types';
-import {
- BUSINESS_TYPE_OPTIONS,
- INDUSTRY_OPTIONS,
- JOB_POSITION_OPTIONS,
- USE_CASE_OPTIONS,
- OPT_IN_TYPE_OPTIONS,
- COMPANY_TYPE_OPTIONS,
- REGISTRATION_ID_OPTIONS,
- formatIndustry,
- formatUseCase,
- formatStatus,
-} from '@/lib/a2p-types';
-
-const STORAGE_KEY = 'closebot_a2p_wizard_draft';
-
-const STEPS = [
- { label: 'Business Info', icon: Building2 },
- { label: 'Authorized Rep', icon: User },
- { label: 'Address', icon: MapPin },
- { label: 'Campaign', icon: MessageSquare },
- { label: 'Review & Submit', icon: CheckCircle },
-];
-
-// Default form state
-function getDefaultBusiness(): BusinessInfo {
- return {
- businessName: '',
- businessType: 'Corporation',
- businessIndustry: 'TECHNOLOGY',
- registrationIdentifier: 'EIN',
- registrationNumber: '',
- websiteUrl: '',
- socialMediaUrls: [],
- businessIdentity: 'direct_customer',
- regionsOfOperation: ['USA_AND_CANADA'],
- companyType: 'private',
- };
-}
-
-function getDefaultRep(): AuthorizedRep {
- return {
- firstName: '',
- lastName: '',
- businessTitle: '',
- jobPosition: 'CEO',
- phoneNumber: '',
- email: '',
- };
-}
-
-function getDefaultAddress(): BusinessAddress {
- return {
- customerName: '',
- street: '',
- streetSecondary: '',
- city: '',
- region: '',
- postalCode: '',
- isoCountry: 'US',
- };
-}
-
-function getDefaultCampaign(): CampaignInfo {
- return {
- useCase: 'MARKETING',
- description: '',
- sampleMessages: [''],
- messageFlow: '',
- optInType: 'WEB_FORM',
- optInMessage: '',
- optOutMessage: 'You have been unsubscribed and will no longer receive messages. Reply START to re-subscribe.',
- helpMessage: 'Reply STOP to unsubscribe. For support, contact us at our website.',
- hasEmbeddedLinks: false,
- hasEmbeddedPhone: false,
- };
-}
-
-// Shared form components
-function FormLabel({ children, required }: { children: React.ReactNode; required?: boolean }) {
- return (
-
- );
-}
-
-function FormInput({
- value,
- onChange,
- placeholder,
- type = 'text',
- required,
-}: {
- value: string;
- onChange: (v: string) => void;
- placeholder?: string;
- type?: string;
- required?: boolean;
-}) {
- return (
- onChange(e.target.value)}
- placeholder={placeholder}
- required={required}
- className="w-full rounded-lg border border-slate-700/50 bg-slate-800/50 px-4 py-2.5 text-sm text-white placeholder-slate-500 focus:border-cyan-500/50 focus:outline-none focus:ring-2 focus:ring-cyan-500/20 transition-colors"
- />
- );
-}
-
-function FormSelect({
- value,
- onChange,
- options,
- formatLabel,
-}: {
- value: T;
- onChange: (v: T) => void;
- options: T[];
- formatLabel?: (v: T) => string;
-}) {
- return (
-
- );
-}
-
-function FormTextarea({
- value,
- onChange,
- placeholder,
- rows = 3,
-}: {
- value: string;
- onChange: (v: string) => void;
- placeholder?: string;
- rows?: number;
-}) {
- return (
-