# FlowPilot Survey — Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Build a public-facing survey page at `/survey` that collects FlowPilot research responses from senior MSP engineers and emails the results to Michael. The survey is unauthenticated (no login required), uses the existing ResolutionFlow design system, and has a backend endpoint to receive and email submissions via the existing Resend infrastructure.

**Architecture:** Static React page (public route, no auth) → `POST /api/v1/survey/submit` (unauthenticated, rate-limited) → saves to DB → sends notification email via existing `EmailService` + Resend. No confirmation email to respondent (they may not provide an email).

**Design doc reference:** The survey content and question structure is defined in the HTML file at `frontend/public/survey-reference.html` (copy the uploaded `survey.html` there for reference). The page must be rebuilt as a proper React component using the existing design system — NOT the inline-styled HTML.

**Tech Stack:** FastAPI backend, SQLAlchemy 2.0 (async), Alembic, Pydantic v2. React 19, TypeScript, Tailwind CSS, shadcn/ui frontend.

---

## Reference File

Before starting, copy the survey HTML reference file into the project:

```bash
cp /path/to/survey.html docs/reference/flowpilot-survey-reference.html
```

This file contains the exact questions, options, types, and flow. Use it as the source of truth for survey content. The visual design should NOT copy the inline styles — instead, use the ResolutionFlow design system (Tailwind + shadcn/ui + brand tokens).

---

## Phase 1: Backend

### Task 1: Database Model — `SurveyResponse`

**Files:**
- Create: `backend/app/models/survey_response.py`
- Modify: `backend/app/models/__init__.py` (add import + `__all__` entry)

**Step 1: Create the model file**

```python
# backend/app/models/survey_response.py
"""FlowPilot survey response storage."""
import uuid
from datetime import datetime, timezone

from sqlalchemy import Column, DateTime, String, Text
from sqlalchemy.dialects.postgresql import JSONB, UUID

from app.core.database import Base


class SurveyResponse(Base):
    __tablename__ = "survey_responses"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    respondent_name = Column(String(255), nullable=True)  # Optional — we may already know who they are
    responses = Column(JSONB, nullable=False)  # Full survey answers as JSON
    ip_address = Column(String(45), nullable=True)  # For rate limiting / dedup
    user_agent = Column(Text, nullable=True)  # Browser info
    created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
```

**Step 2: Add to models `__init__.py`**

Add import and `__all__` entry following the existing pattern in the file.

**Step 3: Commit**

```bash
git add backend/app/models/survey_response.py backend/app/models/__init__.py
git commit -m "feat: add SurveyResponse model for FlowPilot research"
```

---

### Task 2: Database Migration

**Files:**
- Create: new Alembic migration

**Step 1: Generate migration**

```bash
cd backend
alembic revision --autogenerate -m "add survey_responses table"
```

**Step 2: Review the generated migration** — verify it creates `survey_responses` with the correct columns.

**Step 3: Run migration**

```bash
alembic upgrade head
```

**Step 4: Commit**

```bash
git add backend/alembic/versions/
git commit -m "feat: add survey_responses migration"
```

---

### Task 3: Pydantic Schema

**Files:**
- Create: `backend/app/schemas/survey.py`

**Step 1: Create schema**

```python
# backend/app/schemas/survey.py
"""Schemas for FlowPilot survey submission."""
from typing import Any, Optional
from pydantic import BaseModel, Field


class SurveySubmission(BaseModel):
    """Incoming survey response from the public form."""
    respondent_name: Optional[str] = Field(None, max_length=255)
    responses: dict[str, Any] = Field(
        ...,
        description="Question ID → answer mapping. Values can be strings, lists, or numbers.",
    )


class SurveySubmissionResponse(BaseModel):
    """Response after successful submission."""
    message: str = "Thank you for your response!"
    id: str
```

**Step 2: Commit**

```bash
git add backend/app/schemas/survey.py
git commit -m "feat: add survey submission schemas"
```

---

### Task 4: API Endpoint

**Files:**
- Create: `backend/app/api/endpoints/survey.py`
- Modify: `backend/app/api/router.py` (register the new router)

**Step 1: Create the endpoint**

```python
# backend/app/api/endpoints/survey.py
"""Public survey submission endpoint. No authentication required."""
import logging
from typing import Annotated

from fastapi import APIRouter, Depends, Request
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.config import settings
from app.core.database import get_db
from app.core.email import EmailService
from app.core.rate_limit import limiter
from app.models.survey_response import SurveyResponse
from app.schemas.survey import SurveySubmission, SurveySubmissionResponse

logger = logging.getLogger(__name__)

router = APIRouter(tags=["survey"])


@router.post("/survey/submit", response_model=SurveySubmissionResponse)
@limiter.limit("3/hour")
async def submit_survey(
    request: Request,
    data: SurveySubmission,
    db: Annotated[AsyncSession, Depends(get_db)],
):
    """Accept a public survey submission. No auth required.

    Rate limited to 3/hour per IP to prevent spam.
    Saves to DB and sends notification email to configured address.
    """
    # Get client info
    ip = request.client.host if request.client else None
    ua = request.headers.get("user-agent", "")

    # Save to database
    response = SurveyResponse(
        respondent_name=data.respondent_name,
        responses=data.responses,
        ip_address=ip,
        user_agent=ua,
    )
    db.add(response)
    await db.flush()

    # Send notification email (fire-and-forget, don't fail the request)
    try:
        await EmailService.send_survey_notification_email(
            to_email=settings.FEEDBACK_EMAIL,  # Reuse existing feedback email config
            respondent_name=data.respondent_name,
            responses=data.responses,
        )
    except Exception:
        logger.exception("Failed to send survey notification email")

    return SurveySubmissionResponse(id=str(response.id))
```

**Step 2: Register in router.py**

Add to `backend/app/api/router.py` following the existing pattern. Import and include the survey router. **IMPORTANT:** This endpoint does NOT use `get_current_active_user` — it's public. Make sure the route is included without any auth dependency prefix.

The survey route should be registered similarly to how the auth routes are registered (no auth prefix), e.g.:
```python
from app.api.endpoints import survey
api_router.include_router(survey.router, prefix="")
```

**Step 3: Commit**

```bash
git add backend/app/api/endpoints/survey.py backend/app/api/router.py
git commit -m "feat: add public survey submission endpoint with rate limiting"
```

---

### Task 5: Email Notification

**Files:**
- Modify: `backend/app/core/email.py` (add new static method to `EmailService`)

**Step 1: Add `send_survey_notification_email` to `EmailService`**

Follow the exact same pattern as `send_feedback_email()` — it's a notification to Michael with the response content. Use the same dark-themed HTML template style as existing emails.

```python
@staticmethod
async def send_survey_notification_email(
    to_email: str,
    respondent_name: str | None,
    responses: dict,
) -> bool:
    """Send survey response notification. Fire-and-forget."""
    if not settings.email_enabled:
        logger.warning("Email not sent — RESEND_API_KEY not configured")
        return False

    try:
        import resend
        from datetime import datetime, timezone

        resend.api_key = settings.RESEND_API_KEY

        name_display = respondent_name or "Anonymous"
        date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
        subject = f"[FlowPilot Survey] New Response from {name_display} — {date_str}"

        # Build response summary as HTML
        rows_html = ""
        for q_id, answer in responses.items():
            if isinstance(answer, list):
                answer_display = "<br>".join(f"• {item}" for item in answer)
            else:
                answer_display = str(answer)
            rows_html += f"""
            <tr>
                <td style="padding: 10px 14px; border-bottom: 1px solid #27272a; color: #818cf8; font-weight: 600; vertical-align: top; width: 120px; font-size: 13px;">{q_id}</td>
                <td style="padding: 10px 14px; border-bottom: 1px solid #27272a; color: #e4e4e7; font-size: 14px; white-space: pre-wrap;">{answer_display}</td>
            </tr>"""

        html = f"""<!DOCTYPE html>
<html><body style="margin:0; padding:0; background-color:#09090b; font-family:Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#09090b; padding:40px 20px;">
<tr><td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color:#18181b; border-radius:12px; border:1px solid #27272a;">
  <tr><td style="padding:32px 32px 20px;">
    <h1 style="margin:0 0 4px; font-size:20px; color:#f4f4f5;">FlowPilot Survey Response</h1>
    <p style="margin:0; font-size:13px; color:#71717a;">From: {name_display} &bull; {date_str}</p>
  </td></tr>
  <tr><td style="padding:0 32px 32px;">
    <table width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #27272a; border-radius:8px; overflow:hidden;">
      {rows_html}
    </table>
  </td></tr>
  <tr><td style="padding:0 32px 24px;">
    <p style="margin:0; font-size:12px; color:#52525b;">This response has been saved to the survey_responses table.</p>
  </td></tr>
</table>
</td></tr>
</table>
</body></html>"""

        resend.Emails.send({
            "from": settings.FROM_EMAIL,
            "to": [to_email],
            "subject": subject,
            "html": html,
        })
        logger.info("Survey notification sent for respondent: %s", name_display)
        return True

    except Exception:
        logger.exception("Failed to send survey notification email")
        return False
```

**Step 2: Commit**

```bash
git add backend/app/core/email.py
git commit -m "feat: add survey notification email to EmailService"
```

---

### Task 6: CORS — Allow Public Survey Endpoint

**Files:**
- Verify: `backend/app/main.py` CORS configuration

The survey endpoint needs to accept POST requests from the frontend. Since both frontend and backend are on the same domain (`resolutionflow.com` / `api.resolutionflow.com`), this should already work with existing CORS config. **Verify** that the existing CORS middleware allows the frontend origin. No changes should be needed, but confirm.

---

## Phase 2: Frontend

### Task 7: Survey Page Component

**Files:**
- Create: `frontend/src/pages/SurveyPage.tsx`

**CRITICAL:** This is a public page. It must NOT import or depend on `authStore`, `ProtectedRoute`, or any authenticated API client. It uses its own direct `fetch` or a standalone axios call to `POST /api/v1/survey/submit`.

**Design requirements:**
- Use the existing ResolutionFlow design system: Tailwind CSS classes, shadcn/ui components (`Card`, `Button`, `Label`, `Textarea`, `Slider`, `Badge`, `Progress`)
- Dark theme by default (the page won't inherit app shell's theme toggle — hardcode `dark` class on root or use dark-only styles)
- Fonts: Plus Jakarta Sans for headings, Inter for body (already in `index.html`)
- Brand gradient: `bg-gradient-brand` or `from-[#818cf8] to-[#a78bfa]`
- Use `bg-background`, `bg-card`, `text-foreground`, `text-muted-foreground`, `border-border` tokens
- Responsive (works on mobile — engineers might fill this out on their phone)

**Component structure:**

```
SurveyPage
├── SurveyHero (title, description, stats)
├── SurveyProgress (step dots + progress bar)
├── SurveySlide (rendered per active slide)
│   ├── MultipleChoice (single select)
│   ├── MultipleChoiceMulti (multi select)
│   ├── RangeSlider
│   ├── TextAnswer (textarea)
│   ├── DragRank (drag-to-reorder list)
│   └── ScenarioBox (ticket simulation display)
├── SlideNavigation (back/next buttons)
└── SurveyComplete (thank you + submit)
```

**Question data:** Extract the `SLIDES` array from the reference HTML file (`docs/reference/flowpilot-survey-reference.html`). Port it to a TypeScript constant with proper types:

```typescript
type QuestionType = 'mc' | 'mc-multi' | 'range' | 'text' | 'rank';

interface SurveyQuestion {
  id: string;
  type: QuestionType;
  num: string;
  text: string;
  hint?: string;
  options?: string[];       // for mc, mc-multi
  items?: string[];         // for rank
  min?: number;             // for range
  max?: number;
  step?: number;
  suffix?: string;
  low_label?: string;
  high_label?: string;
}

interface SurveySlide {
  id: string;
  questions: SurveyQuestion[];
  scenario?: {
    title: string;
    symptom: string;
    details: string;
  };
}
```

**State management:** Use local React state (`useState` / `useReducer`) — do NOT create a Zustand store for this. It's a self-contained page.

**Submission flow:**
1. User clicks "Submit" on last slide
2. Show loading state on button
3. POST to `${VITE_API_URL}/api/v1/survey/submit` with `{ respondent_name: null, responses: answersObject }`
4. On success → show completion screen
5. On error → show error toast, keep form populated for retry
6. Also offer "Copy to Clipboard" as fallback if submission fails

**The drag-and-drop ranking component:**
- Use native HTML5 drag events (same approach as the reference HTML)
- Support touch events for mobile
- Show numbered positions that update on reorder
- Use `border-border` for item borders, `bg-accent` for drag-over state

**Step 1: Create `frontend/src/pages/SurveyPage.tsx`**

Build the complete page as a single file component (it's a standalone page, not reused elsewhere). Aim for ~400-600 lines. Use shadcn components where they fit (Button, Card, Progress, Textarea, Slider, Badge), custom Tailwind for the rest.

**Step 2: Verify build**

```bash
cd frontend && npm run build
```

**Step 3: Commit**

```bash
git add frontend/src/pages/SurveyPage.tsx
git commit -m "feat: add SurveyPage component for FlowPilot research"
```

---

### Task 8: Public Route

**Files:**
- Modify: `frontend/src/router.tsx`

**Step 1: Add public route for `/survey`**

Add as a top-level route (same level as `/login`, `/register`, `/share/:shareToken` — NOT inside the `ProtectedRoute` wrapper):

```typescript
import SurveyPage from '@/pages/SurveyPage'

// ... in the router array, at the top level alongside login/register:
{
  path: '/survey',
  element: <SurveyPage />,
  errorElement: <RouteError />,
},
```

**IMPORTANT:** This route must be OUTSIDE the `ProtectedRoute` parent. It must not require authentication.

**Step 2: Verify build**

```bash
cd frontend && npm run build
```

**Step 3: Verify route works**

Navigate to `http://localhost:5173/survey` — should render the survey without any login redirect.

**Step 4: Commit**

```bash
git add frontend/src/router.tsx
git commit -m "feat: add public /survey route"
```

---

### Task 9: Nginx Catch-All Verification

**Files:**
- Verify: `frontend/nginx.conf`

The existing nginx config should already have a `try_files $uri $uri/ /index.html` catch-all that sends unknown paths to the React router. Verify that `/survey` will be caught by this rule and served correctly. No changes should be needed.

---

## Phase 3: Polish & Testing

### Task 10: Backend Test

**Files:**
- Create: `backend/tests/test_survey.py`

Write a test that:
1. POSTs a valid survey submission to `/api/v1/survey/submit` (no auth headers)
2. Verifies 200 response with `id` field
3. Verifies the response was saved to the database
4. Tests rate limiting (4th request within an hour returns 429)
5. Tests validation (empty `responses` dict returns 422)

Follow the existing test patterns in `backend/tests/test_auth.py`.

**Step 1: Create test file**

**Step 2: Run tests**

```bash
cd backend && pytest tests/test_survey.py -v
```

**Step 3: Commit**

```bash
git add backend/tests/test_survey.py
git commit -m "test: add survey submission endpoint tests"
```

---

### Task 11: Mobile Responsiveness Check

Verify the survey page renders correctly at these breakpoints:
- Desktop: 1024px+
- Tablet: 768px
- Mobile: 375px

The drag-and-drop ranking must work on touch devices. If shadcn's `Slider` component doesn't render well on mobile, use a custom range input with Tailwind styling.

---

## Summary

| Phase | Tasks | New Files | Modified Files |
|-------|-------|-----------|----------------|
| Backend | Model, migration, schema, endpoint, email | `models/survey_response.py`, `schemas/survey.py`, `api/endpoints/survey.py`, migration | `models/__init__.py`, `api/router.py`, `core/email.py` |
| Frontend | Survey page, public route | `pages/SurveyPage.tsx` | `router.tsx` |
| Testing | Backend test, mobile check | `tests/test_survey.py` | — |

**Total estimated effort:** 3-4 hours for Claude Code execution.

**Key constraints:**
- `/survey` route is PUBLIC — no authentication
- Rate limit: 3 submissions per hour per IP
- Email notification uses existing Resend + `FEEDBACK_EMAIL` config
- Design uses existing Tailwind/shadcn design system, NOT the inline CSS from the reference HTML
- Must work on mobile (engineers may fill this out on their phone)
- The reference HTML at `docs/reference/flowpilot-survey-reference.html` is the source of truth for question content
