# 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 = "
".join(f"• {item}" for item in answer) else: answer_display = str(answer) rows_html += f""" {q_id} {answer_display} """ html = f"""

FlowPilot Survey Response

From: {name_display} • {date_str}

{rows_html}

This response has been saved to the survey_responses table.

""" 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: , errorElement: , }, ``` **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