Files
resolutionflow/docs/plans/2026-02-18-feedback-form-implementation.md
chihlasm e995316d41 docs: revise feedback implementation plan with enhancements
Adds: DB persistence, feedback type helper text, confirmation
email to submitter, and TODO breadcrumbs for post-session prompt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 17:27:14 -05:00

36 KiB

Feedback Form Implementation Plan

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

Goal: Add a feedback form page where logged-in users can submit feedback that gets stored in the database and emailed to a configurable address via the existing Resend infrastructure. Includes confirmation email to the submitter, helper text on feedback types, and TODO breadcrumbs for future post-session contextual feedback.

Architecture: New POST /feedback backend endpoint validates input, writes to a feedback table, sends an HTML notification email to the configured address, and fires a confirmation email back to the submitter. Email failures do NOT prevent the DB write from succeeding. Frontend is a single FeedbackPage.tsx form page with a custom feedback type selector (with descriptions) accessible from the sidebar nav and account settings.

Tech Stack: FastAPI + Pydantic + Alembic (backend), React + TypeScript + Tailwind (frontend), Resend (email delivery), slowapi (rate limiting)

Design doc: docs/plans/2026-02-18-feedback-form-design.md


Task 1: Backend Schema

Files:

  • Create: backend/app/schemas/feedback.py

Step 1: Create the Pydantic schema

from enum import Enum
from pydantic import BaseModel, EmailStr, Field


class FeedbackType(str, Enum):
    BUG_REPORT = "Bug Report"
    FEATURE_REQUEST = "Feature Request"
    USABILITY_ISSUE = "Usability Issue"
    DOCUMENTATION = "Documentation"
    GENERAL = "General Feedback"


class FeedbackSubmission(BaseModel):
    email: EmailStr
    feedback_type: FeedbackType
    message: str = Field(..., min_length=10, max_length=5000)


class FeedbackResponse(BaseModel):
    success: bool
    message: str

Step 2: Commit

git add backend/app/schemas/feedback.py
git commit -m "feat: add feedback submission schema"

Task 2: Database Model & Migration

Files:

  • Create: backend/app/models/feedback.py
  • Modify: backend/app/models/__init__.py
  • Create: Alembic migration (manual)

Step 1: Create the SQLAlchemy model

Create backend/app/models/feedback.py:

import uuid
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy import String, Text, DateTime, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import UUID
from app.core.database import Base


class Feedback(Base):
    __tablename__ = "feedback"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    account_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("accounts.id", ondelete="SET NULL"), nullable=True)
    user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=False)
    email: Mapped[str] = mapped_column(String(255), nullable=False)
    feedback_type: Mapped[str] = mapped_column(String(50), nullable=False)
    message: Mapped[str] = mapped_column(Text, nullable=False)
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))

Step 2: Register the model

In backend/app/models/__init__.py, add the import and export:

Add import:

from .feedback import Feedback

Add "Feedback" to the __all__ list.

Step 3: Create the migration manually

Run:

cd backend && alembic revision -m "add feedback table"

Then edit the generated migration file:

"""add feedback table

Revision ID: <auto-generated>
Revises: 7e00fa3c75c9
Create Date: 2026-02-18

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = '<auto-generated>'
down_revision = '7e00fa3c75c9'
branch_labels = None
depends_on = None


def upgrade() -> None:
    op.create_table(
        'feedback',
        sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True),
        sa.Column('account_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('accounts.id', ondelete='SET NULL'), nullable=True),
        sa.Column('user_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('users.id', ondelete='SET NULL'), nullable=False),
        sa.Column('email', sa.String(255), nullable=False),
        sa.Column('feedback_type', sa.String(50), nullable=False),
        sa.Column('message', sa.Text(), nullable=False),
        sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
    )
    op.create_index('ix_feedback_account_id', 'feedback', ['account_id'])
    op.create_index('ix_feedback_user_id', 'feedback', ['user_id'])
    op.create_index('ix_feedback_created_at', 'feedback', ['created_at'])


def downgrade() -> None:
    op.drop_index('ix_feedback_created_at', table_name='feedback')
    op.drop_index('ix_feedback_user_id', table_name='feedback')
    op.drop_index('ix_feedback_account_id', table_name='feedback')
    op.drop_table('feedback')

Step 4: Run the migration

cd backend && alembic upgrade head

Step 5: Commit

git add backend/app/models/feedback.py backend/app/models/__init__.py backend/alembic/versions/*feedback*.py
git commit -m "feat: add feedback database model and migration"

Task 3: Config — Add FEEDBACK_EMAIL

Files:

  • Modify: backend/app/core/config.py

Step 1: Add the FEEDBACK_EMAIL setting

In backend/app/core/config.py, add this line in the Settings class after the FROM_EMAIL line (line 57):

    FEEDBACK_EMAIL: Optional[str] = None

Step 2: Commit

git add backend/app/core/config.py
git commit -m "feat: add FEEDBACK_EMAIL config setting"

Task 4: Email Service — Add feedback emails

Files:

  • Modify: backend/app/core/email.py

Step 1: Add the send_feedback_email method (admin notification)

Add this method to the EmailService class (after send_account_invite_email, before the helper functions):

    @staticmethod
    async def send_feedback_email(
        to_email: str,
        reply_to_email: str,
        feedback_type: str,
        message: str,
        user_email: str,
        account_name: str | None = None,
        account_code: str | None = None,
    ) -> bool:
        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

            date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
            code_suffix = f" — {account_code}" if account_code else ""
            subject = f"[ResolutionFlow Feedback] {feedback_type}{date_str}{code_suffix}"

            html = _render_feedback_html(
                feedback_type=feedback_type,
                message=message,
                user_email=user_email,
                account_name=account_name,
                account_code=account_code,
            )

            resend.Emails.send(
                {
                    "from": settings.FROM_EMAIL,
                    "to": [to_email],
                    "reply_to": reply_to_email,
                    "subject": subject,
                    "html": html,
                }
            )
            logger.info("Feedback email sent from %s (type: %s)", user_email, feedback_type)
            return True

        except Exception:
            logger.exception("Failed to send feedback email from %s", user_email)
            return False

Step 2: Add the send_feedback_confirmation_email method (user confirmation)

Add this method right after send_feedback_email:

    @staticmethod
    async def send_feedback_confirmation_email(
        to_email: str,
        feedback_type: str,
        message_preview: str,
    ) -> bool:
        """Send a thank-you confirmation to the feedback submitter. Fire-and-forget."""
        if not settings.email_enabled:
            logger.warning("Confirmation email not sent — RESEND_API_KEY not configured")
            return False

        try:
            import resend

            resend.api_key = settings.RESEND_API_KEY

            subject = "Thanks for your feedback — ResolutionFlow"

            html = _render_feedback_confirmation_html(
                feedback_type=feedback_type,
                message_preview=message_preview,
            )

            resend.Emails.send(
                {
                    "from": settings.FROM_EMAIL,
                    "to": [to_email],
                    "subject": subject,
                    "html": html,
                }
            )
            logger.info("Feedback confirmation email sent to %s", to_email)
            return True

        except Exception:
            logger.exception("Failed to send feedback confirmation to %s", to_email)
            return False

Step 3: Add the HTML renderers

Add these functions at the bottom of the file (after the other _render_* functions):

def _render_feedback_html(
    feedback_type: str,
    message: str,
    user_email: str,
    account_name: str | None,
    account_code: str | None,
) -> str:
    from datetime import datetime, timezone
    import html

    date_str = datetime.now(timezone.utc).strftime("%B %d, %Y")
    safe_message = html.escape(message).replace("\n", "<br>")

    account_line = ""
    if account_name and account_code:
        account_line = f"""
        <tr><td style="padding:0 40px 8px;">
          <p style="margin:0;color:#a0a0a0;font-size:14px;">
            <strong style="color:#e0e0e0;">Account:</strong> {html.escape(account_name)} ({html.escape(account_code)})
          </p>
        </td></tr>"""

    return f"""<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
<body style="margin:0;padding:0;background:#000;font-family:'Inter',Helvetica,Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#000;padding:40px 0;">
<tr><td align="center">
<table width="560" cellpadding="0" cellspacing="0" style="background:#111;border:1px solid rgba(255,255,255,0.06);border-radius:16px;">
  <tr><td style="padding:40px 40px 24px;text-align:center;">
    <h1 style="margin:0;color:#fff;font-size:24px;font-weight:600;">ResolutionFlow Feedback</h1>
  </td></tr>
  <tr><td style="padding:0 40px 8px;">
    <p style="margin:0;color:#a0a0a0;font-size:14px;">
      <strong style="color:#e0e0e0;">Type:</strong> {html.escape(feedback_type)}
    </p>
  </td></tr>
  <tr><td style="padding:0 40px 8px;">
    <p style="margin:0;color:#a0a0a0;font-size:14px;">
      <strong style="color:#e0e0e0;">From:</strong> {html.escape(user_email)}
    </p>
  </td></tr>
  {account_line}
  <tr><td style="padding:0 40px 8px;">
    <p style="margin:0;color:#a0a0a0;font-size:14px;">
      <strong style="color:#e0e0e0;">Date:</strong> {date_str}
    </p>
  </td></tr>
  <tr><td style="padding:16px 40px 0;">
    <div style="border-top:1px solid rgba(255,255,255,0.06);padding-top:16px;">
      <p style="margin:0;color:#e0e0e0;font-size:15px;line-height:1.7;">{safe_message}</p>
    </div>
  </td></tr>
  <tr><td style="padding:24px 40px 32px;">
    <p style="margin:0;color:#666;font-size:12px;text-align:center;">
      Reply directly to this email to respond to the user.
    </p>
  </td></tr>
</table>
</td></tr>
</table>
</body></html>"""


def _render_feedback_confirmation_html(
    feedback_type: str,
    message_preview: str,
) -> str:
    import html

    safe_preview = html.escape(message_preview)

    return f"""<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
<body style="margin:0;padding:0;background:#000;font-family:'Inter',Helvetica,Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#000;padding:40px 0;">
<tr><td align="center">
<table width="560" cellpadding="0" cellspacing="0" style="background:#111;border:1px solid rgba(255,255,255,0.06);border-radius:16px;">
  <tr><td style="padding:40px 40px 24px;text-align:center;">
    <h1 style="margin:0;color:#fff;font-size:24px;font-weight:600;">ResolutionFlow</h1>
    <p style="margin:8px 0 0;color:#a0a0a0;font-size:14px;">Thanks for your feedback!</p>
  </td></tr>
  <tr><td style="padding:0 40px 24px;">
    <p style="margin:0;color:#e0e0e0;font-size:16px;line-height:1.6;">
      We've received your <strong style="color:#fff;">{html.escape(feedback_type)}</strong> and our team will review it shortly.
    </p>
  </td></tr>
  <tr><td style="padding:0 40px 24px;">
    <div style="background:#000;border:1px solid rgba(255,255,255,0.1);border-radius:12px;padding:16px 20px;">
      <p style="margin:0 0 4px;color:#a0a0a0;font-size:12px;text-transform:uppercase;letter-spacing:1px;">Your feedback</p>
      <p style="margin:0;color:#e0e0e0;font-size:14px;line-height:1.5;font-style:italic;">"{safe_preview}"</p>
    </div>
  </td></tr>
  <tr><td style="padding:0 40px 32px;">
    <p style="margin:0;color:#666;font-size:12px;text-align:center;">
      If we need more details, we'll reach out to you directly.
    </p>
  </td></tr>
</table>
</td></tr>
</table>
</body></html>"""

Step 4: Commit

git add backend/app/core/email.py
git commit -m "feat: add feedback notification and confirmation emails to EmailService"

Task 5: Backend Endpoint

Files:

  • Create: backend/app/api/endpoints/feedback.py
  • Modify: backend/app/api/router.py

Step 1: Create the endpoint

Create backend/app/api/endpoints/feedback.py:

import logging
from typing import Annotated

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

from app.api.deps import get_current_active_user
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.user import User
from app.models.account import Account
from app.models.feedback import Feedback
from app.schemas.feedback import FeedbackSubmission, FeedbackResponse

logger = logging.getLogger(__name__)

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

# TODO: Post-session contextual feedback prompt — when building the post-session
# feedback flow, reuse this endpoint by adding optional session_id/tree_id fields
# to FeedbackSubmission. The Feedback model and email infrastructure are already
# in place. See design doc for details.


@router.post("/feedback", response_model=FeedbackResponse)
@limiter.limit("1/minute")
async def submit_feedback(
    request: Request,
    data: FeedbackSubmission,
    current_user: Annotated[User, Depends(get_current_active_user)],
    db: Annotated[AsyncSession, Depends(get_db)],
):
    """Submit user feedback. Saves to DB and sends notification email."""
    if not settings.FEEDBACK_EMAIL:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail="Feedback submission is not configured",
        )

    # Get account info for the email
    account_name = None
    account_code = None
    if current_user.account_id:
        result = await db.execute(
            select(Account).where(Account.id == current_user.account_id)
        )
        account = result.scalar_one_or_none()
        if account:
            account_name = account.name
            account_code = account.display_code

    # Always persist to DB first — email failure should not lose feedback
    feedback_record = Feedback(
        account_id=current_user.account_id,
        user_id=current_user.id,
        email=data.email,
        feedback_type=data.feedback_type.value,
        message=data.message,
    )
    db.add(feedback_record)
    await db.commit()

    # Send notification email to admin (best-effort)
    sent = await EmailService.send_feedback_email(
        to_email=settings.FEEDBACK_EMAIL,
        reply_to_email=data.email,
        feedback_type=data.feedback_type.value,
        message=data.message,
        user_email=current_user.email,
        account_name=account_name,
        account_code=account_code,
    )

    if not sent:
        logger.warning("Feedback saved to DB but notification email failed for user %s", current_user.email)

    # Send confirmation email to submitter (fire-and-forget)
    message_preview = data.message[:100] + ("..." if len(data.message) > 100 else "")
    await EmailService.send_feedback_confirmation_email(
        to_email=data.email,
        feedback_type=data.feedback_type.value,
        message_preview=message_preview,
    )

    return FeedbackResponse(success=True, message="Thank you! Your feedback has been submitted.")

Step 2: Register the router

In backend/app/api/router.py, add the import and include:

Add to imports (after the existing import lines):

from app.api.endpoints import feedback

Add at the end of the router registrations:

api_router.include_router(feedback.router)

Step 3: Commit

git add backend/app/api/endpoints/feedback.py backend/app/api/router.py
git commit -m "feat: add POST /feedback endpoint with DB persistence and dual emails"

Task 6: Backend Tests

Files:

  • Create: backend/tests/test_feedback.py

Step 1: Write the tests

These tests use the project's existing client and auth_headers fixtures from conftest.py.

import pytest
from unittest.mock import patch, AsyncMock


@pytest.mark.asyncio
async def test_submit_feedback(client, auth_headers):
    """Test successful feedback submission — saves to DB and sends emails."""
    with patch("app.api.endpoints.feedback.settings") as mock_settings, \
         patch("app.api.endpoints.feedback.EmailService") as mock_email:
        mock_settings.FEEDBACK_EMAIL = "support@test.com"
        mock_email.send_feedback_email = AsyncMock(return_value=True)
        mock_email.send_feedback_confirmation_email = AsyncMock(return_value=True)

        response = await client.post(
            "/api/v1/feedback",
            json={
                "email": "test@example.com",
                "feedback_type": "Bug Report",
                "message": "Something is broken in the tree editor when I try to save.",
            },
            headers=auth_headers,
        )

    assert response.status_code == 200
    data = response.json()
    assert data["success"] is True
    assert "submitted" in data["message"].lower()

    # Verify both emails were called
    mock_email.send_feedback_email.assert_called_once()
    mock_email.send_feedback_confirmation_email.assert_called_once()


@pytest.mark.asyncio
async def test_submit_feedback_saves_to_db_even_if_email_fails(client, auth_headers, test_db):
    """Test that feedback is persisted even when email sending fails."""
    from sqlalchemy import select, func
    from app.models.feedback import Feedback

    with patch("app.api.endpoints.feedback.settings") as mock_settings, \
         patch("app.api.endpoints.feedback.EmailService") as mock_email:
        mock_settings.FEEDBACK_EMAIL = "support@test.com"
        mock_email.send_feedback_email = AsyncMock(return_value=False)
        mock_email.send_feedback_confirmation_email = AsyncMock(return_value=False)

        response = await client.post(
            "/api/v1/feedback",
            json={
                "email": "test@example.com",
                "feedback_type": "Feature Request",
                "message": "Please add dark mode to the export preview screen.",
            },
            headers=auth_headers,
        )

    # Should still succeed — DB write happened
    assert response.status_code == 200
    assert response.json()["success"] is True

    # Verify it was saved to the database
    result = await test_db.execute(select(func.count()).select_from(Feedback))
    count = result.scalar()
    assert count >= 1


@pytest.mark.asyncio
async def test_submit_feedback_not_configured(client, auth_headers):
    """Test 503 when FEEDBACK_EMAIL is not set."""
    with patch("app.api.endpoints.feedback.settings") as mock_settings:
        mock_settings.FEEDBACK_EMAIL = None

        response = await client.post(
            "/api/v1/feedback",
            json={
                "email": "test@example.com",
                "feedback_type": "General Feedback",
                "message": "This is a general feedback message for testing.",
            },
            headers=auth_headers,
        )

    assert response.status_code == 503


@pytest.mark.asyncio
async def test_submit_feedback_validation_message_too_short(client, auth_headers):
    """Test validation — message too short."""
    response = await client.post(
        "/api/v1/feedback",
        json={
            "email": "test@example.com",
            "feedback_type": "Bug Report",
            "message": "short",
        },
        headers=auth_headers,
    )

    assert response.status_code == 422


@pytest.mark.asyncio
async def test_submit_feedback_invalid_type(client, auth_headers):
    """Test validation — invalid feedback type."""
    response = await client.post(
        "/api/v1/feedback",
        json={
            "email": "test@example.com",
            "feedback_type": "Invalid Type",
            "message": "This should fail because the type is invalid.",
        },
        headers=auth_headers,
    )

    assert response.status_code == 422


@pytest.mark.asyncio
async def test_submit_feedback_requires_auth(client):
    """Test that unauthenticated requests are rejected."""
    response = await client.post(
        "/api/v1/feedback",
        json={
            "email": "anon@example.com",
            "feedback_type": "General Feedback",
            "message": "This should fail because I'm not logged in.",
        },
    )
    assert response.status_code == 401

Step 2: Run the tests

cd backend && pytest tests/test_feedback.py -v --override-ini="addopts="

Expected: All 6 tests pass. If any fail, debug and fix before proceeding.

Step 3: Commit

git add backend/tests/test_feedback.py
git commit -m "test: add feedback endpoint tests including DB persistence"

Task 7: Frontend API Client

Files:

  • Create: frontend/src/api/feedback.ts
  • Modify: frontend/src/api/index.ts

Step 1: Create the API module

Create frontend/src/api/feedback.ts:

import { apiClient } from './client'

export interface FeedbackSubmission {
  email: string
  feedback_type: string
  message: string
}

export interface FeedbackResponse {
  success: boolean
  message: string
}

export const feedbackApi = {
  submit: async (data: FeedbackSubmission): Promise<FeedbackResponse> => {
    const { data: response } = await apiClient.post('/feedback', data)
    return response
  },
}

export default feedbackApi

Step 2: Export from index

In frontend/src/api/index.ts, add at the end:

export { default as feedbackApi } from './feedback'

Step 3: Commit

git add frontend/src/api/feedback.ts frontend/src/api/index.ts
git commit -m "feat: add feedback API client"

Task 8: Frontend Page

Files:

  • Create: frontend/src/pages/FeedbackPage.tsx

Step 1: Create the page component

This version uses a custom feedback type selector with helper/description text instead of a plain <select>, so users can distinguish between types like "Usability Issue" vs "Bug Report."

import { useState } from 'react'
import { MessageSquareText, Send, CheckCircle2, ChevronDown } from 'lucide-react'
import { useAuthStore } from '@/store/authStore'
import { feedbackApi } from '@/api'
import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast'

// TODO: Post-session contextual feedback prompt — after completing a troubleshooting
// session, show a subtle inline prompt like "How was this flow? Quick feedback →"
// that opens a lightweight version of this form pre-tagged with tree/session context.

const FEEDBACK_TYPES = [
  { value: 'Bug Report', description: 'Something is broken or not working as expected' },
  { value: 'Feature Request', description: "An idea for something new you'd like to see" },
  { value: 'Usability Issue', description: 'Something works but is confusing or hard to use' },
  { value: 'Documentation', description: 'Feedback on help docs, tooltips, or in-app guidance' },
  { value: 'General Feedback', description: 'Anything else — thoughts, impressions, suggestions' },
] as const

export function FeedbackPage() {
  const user = useAuthStore(s => s.user)

  const [email, setEmail] = useState(user?.email ?? '')
  const [feedbackType, setFeedbackType] = useState<string>('')
  const [message, setMessage] = useState('')
  const [isSubmitting, setIsSubmitting] = useState(false)
  const [submitted, setSubmitted] = useState(false)
  const [typeDropdownOpen, setTypeDropdownOpen] = useState(false)

  const canSubmit = email.trim() && feedbackType && message.trim().length >= 10

  const selectedType = FEEDBACK_TYPES.find(t => t.value === feedbackType)

  const handleSelectType = (value: string) => {
    setFeedbackType(value)
    setTypeDropdownOpen(false)
  }

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    if (!canSubmit || isSubmitting) return

    setIsSubmitting(true)
    try {
      const response = await feedbackApi.submit({
        email: email.trim(),
        feedback_type: feedbackType,
        message: message.trim(),
      })
      if (response.success) {
        setSubmitted(true)
        setFeedbackType('')
        setMessage('')
      }
    } catch (err: unknown) {
      const error = err as { response?: { data?: { detail?: string } } }
      toast.error(error.response?.data?.detail || 'Failed to submit feedback. Please try again.')
    } finally {
      setIsSubmitting(false)
    }
  }

  const handleNewFeedback = () => {
    setSubmitted(false)
    setEmail(user?.email ?? '')
  }

  return (
    <div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
      {/* Page header */}
      <div className="mb-8">
        <div className="flex items-center gap-3">
          <MessageSquareText className="h-8 w-8 text-muted-foreground" />
          <h1 className="text-2xl font-bold text-foreground sm:text-3xl">Send Feedback</h1>
        </div>
        <p className="mt-2 text-muted-foreground">
          Help us improve ResolutionFlow. Report bugs, request features, or share your thoughts.
        </p>
      </div>

      <div className="max-w-2xl">
        {submitted ? (
          <div className="bg-card border border-border rounded-xl p-8 text-center">
            <CheckCircle2 className="mx-auto h-12 w-12 text-green-500 mb-4" />
            <h2 className="text-xl font-semibold text-foreground mb-2">Thank you for your feedback!</h2>
            <p className="text-muted-foreground mb-6">
              We've received your submission and will review it shortly. Check your email for a confirmation.
            </p>
            <button
              onClick={handleNewFeedback}
              className="bg-gradient-brand text-white font-medium px-6 py-2.5 rounded-lg shadow-lg shadow-primary/20 hover:opacity-90 transition-opacity"
            >
              Send More Feedback
            </button>
          </div>
        ) : (
          <form onSubmit={handleSubmit} className="bg-card border border-border rounded-xl p-4 sm:p-6 space-y-5">
            {/* Email */}
            <div>
              <label htmlFor="feedback-email" className="block text-sm font-medium text-foreground mb-1.5">
                Email Address
              </label>
              <input
                id="feedback-email"
                type="email"
                value={email}
                onChange={e => setEmail(e.target.value)}
                placeholder="your@email.com"
                required
                className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary/20 focus:outline-none"
              />
              <p className="mt-1 text-xs text-muted-foreground">We'll reply to this address if we need more details.</p>
            </div>

            {/* Feedback Type — custom selector with descriptions */}
            <div>
              <label className="block text-sm font-medium text-foreground mb-1.5">
                Feedback Type
              </label>
              <div className="relative">
                <button
                  type="button"
                  onClick={() => setTypeDropdownOpen(!typeDropdownOpen)}
                  className={cn(
                    "w-full rounded-lg border border-border bg-card px-3 py-2 text-left flex items-center justify-between focus:border-primary focus:ring-1 focus:ring-primary/20 focus:outline-none",
                    feedbackType ? "text-foreground" : "text-muted-foreground"
                  )}
                >
                  <span>{selectedType?.value ?? 'Select a type...'}</span>
                  <ChevronDown size={16} className={cn("transition-transform", typeDropdownOpen && "rotate-180")} />
                </button>
                {typeDropdownOpen && (
                  <div className="absolute z-10 mt-1 w-full rounded-lg border border-border bg-card shadow-lg overflow-hidden">
                    {FEEDBACK_TYPES.map(type => (
                      <button
                        key={type.value}
                        type="button"
                        onClick={() => handleSelectType(type.value)}
                        className={cn(
                          "w-full text-left px-3 py-2.5 hover:bg-accent transition-colors",
                          feedbackType === type.value && "bg-accent"
                        )}
                      >
                        <div className="text-sm font-medium text-foreground">{type.value}</div>
                        <div className="text-xs text-muted-foreground mt-0.5">{type.description}</div>
                      </button>
                    ))}
                  </div>
                )}
              </div>
            </div>

            {/* Message */}
            <div>
              <label htmlFor="feedback-message" className="block text-sm font-medium text-foreground mb-1.5">
                Your Feedback
              </label>
              <textarea
                id="feedback-message"
                value={message}
                onChange={e => setMessage(e.target.value)}
                placeholder="Describe your feedback in detail..."
                required
                minLength={10}
                rows={6}
                className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary/20 focus:outline-none resize-y"
              />
              <p className="mt-1 text-xs text-muted-foreground">
                {message.trim().length < 10
                  ? `Minimum 10 characters (${message.trim().length}/10)`
                  : `${message.trim().length} characters`}
              </p>
            </div>

            {/* Submit */}
            <div className="pt-2">
              <button
                type="submit"
                disabled={!canSubmit || isSubmitting}
                className={cn(
                  "flex items-center gap-2 rounded-lg px-6 py-2.5 font-medium text-white shadow-lg shadow-primary/20 transition-opacity",
                  canSubmit && !isSubmitting
                    ? "bg-gradient-brand hover:opacity-90"
                    : "bg-gradient-brand opacity-50 cursor-not-allowed"
                )}
              >
                <Send size={16} />
                {isSubmitting ? 'Sending...' : 'Submit Feedback'}
              </button>
            </div>
          </form>
        )}
      </div>
    </div>
  )
}

export default FeedbackPage

Step 2: Commit

git add frontend/src/pages/FeedbackPage.tsx
git commit -m "feat: add FeedbackPage with custom feedback type selector"

Task 9: Router & Navigation

Files:

  • Modify: frontend/src/router.tsx
  • Modify: frontend/src/components/layout/Sidebar.tsx
  • Modify: frontend/src/pages/AccountSettingsPage.tsx

Step 1: Add the route

In frontend/src/router.tsx:

Add to the lazy imports section (after the MyAnalyticsPage import, around line 32):

const FeedbackPage = lazy(() => import('@/pages/FeedbackPage'))

Add as a child of the '/' route, after the analytics/me route (around line 228, before the admin routes comment):

      {
        path: 'feedback',
        element: (
          <Suspense fallback={<PageLoader />}>
            <FeedbackPage />
          </Suspense>
        ),
      },

Step 2: Add the sidebar nav item

In frontend/src/components/layout/Sidebar.tsx:

Add MessageSquareText to the lucide-react import (line 3):

import { LayoutGrid, Box, PenLine, Clock, FileText, Bookmark, BarChart3, Users, Settings, PanelLeftClose, PanelLeftOpen, MessageSquareText } from 'lucide-react'

In the collapsed sidebar section (around line 147, after the Analytics NavItem):

            <NavItem href="/feedback" icon={MessageSquareText} label="Feedback" collapsed />

In the expanded sidebar section, add in the footer area (around line 202, before the Team NavItem):

            <NavItem href="/feedback" icon={MessageSquareText} label="Feedback" />

Step 3: Add the account settings link card

In frontend/src/pages/AccountSettingsPage.tsx:

Add MessageSquareText to the lucide-react import.

Add a link card after the last existing link card (after the Target Lists card), outside any permission guard:

        {/* Feedback Link (all users) */}
        <Link
          to="/feedback"
          className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
        >
          <div className="flex items-center gap-3">
            <MessageSquareText className="h-5 w-5 text-muted-foreground" />
            <div>
              <h2 className="text-lg font-semibold text-foreground">Send Feedback</h2>
              <p className="text-sm text-muted-foreground">Report bugs, request features, or share your thoughts</p>
            </div>
          </div>
          <span className="text-muted-foreground group-hover:text-foreground transition-colors">&rarr;</span>
        </Link>

Step 4: Commit

git add frontend/src/router.tsx frontend/src/components/layout/Sidebar.tsx frontend/src/pages/AccountSettingsPage.tsx
git commit -m "feat: add feedback route, sidebar nav item, and account link card"

Task 10: Build Verification

Step 1: Run the frontend build

cd frontend && npm run build

Expected: Build succeeds with no errors. TypeScript type-checking is stricter in the build than tsc --noEmit.

Step 2: Run the backend tests

cd backend && pytest tests/test_feedback.py -v --override-ini="addopts="

Expected: All 6 tests pass.

Step 3: Set FEEDBACK_EMAIL in .env for local testing

Add to backend/.env:

FEEDBACK_EMAIL=your-email@example.com

Step 4: Manual smoke test

  1. Start backend: cd backend && uvicorn app.main:app --reload
  2. Start frontend: cd frontend && npm run dev
  3. Log in as any test user
  4. Navigate to /feedback via sidebar
  5. Verify form loads with email pre-filled
  6. Click the feedback type dropdown — verify descriptions appear under each option
  7. Submit feedback — verify success state with confirmation email note
  8. Check email inbox for: (a) admin notification email, (b) user confirmation email
  9. Verify feedback row in DB: docker exec -it patherly_postgres psql -U postgres -d patherly -c "SELECT * FROM feedback;"
  10. Verify the account settings page shows the "Send Feedback" link card

Task 11: Final Verification

Step 1: Run full backend test suite

cd backend && pytest --override-ini="addopts="

Expected: All tests pass, no regressions.

Step 2: Final frontend build check

cd frontend && npm run build

Expected: Clean build.