Move completed plan docs to docs/plans/archive/. Add survey migration 046 and reference HTML/plan files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
537 lines
18 KiB
Markdown
537 lines
18 KiB
Markdown
# 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} • {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
|