# Analytics & User Feedback — Implementation Plan

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

**Goal:** Add team analytics, personal analytics, flow analytics dashboards and a two-tier feedback system (step thumbs up/down + flow CSAT 1-5) to ResolutionFlow.

**Architecture:** Live aggregation queries against existing PostgreSQL tables plus one new `session_ratings` table. Four new API endpoint groups (`/analytics/team`, `/analytics/me`, `/analytics/flows/{id}`, `/sessions/{id}/rate`). Recharts for time-series visualization. Step feedback reuses existing `step_ratings` table with simplified thumbs-only input.

**Tech Stack:** Python FastAPI, SQLAlchemy 2.0 async, Alembic, React 19, TypeScript, Recharts, Tailwind CSS v3

---

## Reference: Design Document

See `docs/plans/2026-02-15-analytics-feedback-design.md` for full design rationale and endpoint response shapes.

## Reference: Existing Code

- Step rating endpoints already exist: `backend/app/api/endpoints/steps.py:427-618` (POST/PUT/DELETE `/steps/{id}/rate` + aggregation helper)
- Session model: `backend/app/models/session.py` — has `outcome`, `completed_at`, `started_at`, `tree_id`, `user_id` columns
- Tree model: `backend/app/models/tree.py` — has `usage_count`, `account_id`
- Session does NOT have `account_id` — must join through `trees.account_id` or `users.account_id`
- Latest migration: `038_remove_workspace_system.py`
- Route registration: `backend/app/api/router.py` — import module + `api_router.include_router()`
- Frontend routes: `frontend/src/router.tsx` — lazy imports + Suspense wrappers
- Sidebar nav: `frontend/src/components/layout/Sidebar.tsx:119-153`
- Permissions: `frontend/src/hooks/usePermissions.ts` — `isSuperAdmin`, `isAccountOwner`, `effectiveRole`

---

## Task 1: Database Migration

**Files:**
- Create: `backend/alembic/versions/039_add_session_ratings_and_analytics_indexes.py`

**Step 1: Generate migration**

```bash
cd backend
```

Create migration file manually (autogenerate won't pick up indexes properly):

```python
"""Add session_ratings table and analytics indexes

Revision ID: 039
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID

revision = '039_add_session_ratings_and_analytics_indexes'
down_revision = '038_remove_workspace_system'

def upgrade():
    # New session_ratings table
    op.create_table(
        'session_ratings',
        sa.Column('id', UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), primary_key=True),
        sa.Column('session_id', UUID(as_uuid=True), sa.ForeignKey('sessions.id', ondelete='CASCADE'), nullable=False),
        sa.Column('user_id', UUID(as_uuid=True), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False),
        sa.Column('tree_id', UUID(as_uuid=True), sa.ForeignKey('trees.id', ondelete='CASCADE'), nullable=False),
        sa.Column('account_id', UUID(as_uuid=True), sa.ForeignKey('accounts.id', ondelete='CASCADE'), nullable=False),
        sa.Column('rating', sa.Integer, nullable=False),
        sa.Column('comment', sa.String(500), nullable=True),
        sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False),
        sa.CheckConstraint('rating >= 1 AND rating <= 5', name='ck_session_ratings_rating_range'),
        sa.UniqueConstraint('session_id', name='uq_session_ratings_session_id'),
    )

    # Analytics indexes
    op.create_index('ix_session_ratings_tree_created', 'session_ratings', ['tree_id', 'created_at'])
    op.create_index('ix_session_ratings_account_created', 'session_ratings', ['account_id', 'created_at'])
    op.create_index('ix_sessions_completed', 'sessions', ['completed_at'])
    op.create_index('ix_step_ratings_step_helpful', 'step_ratings', ['step_id', 'was_helpful'])

    # Make step_ratings.rating column nullable (thumbs-only mode)
    op.alter_column('step_ratings', 'rating', nullable=True)

    # Drop old unique constraint on step_ratings (step_id, user_id) and replace with (step_id, user_id, session_id)
    op.drop_constraint('uq_step_rating_per_user', 'step_ratings', type_='unique')
    op.create_unique_constraint('uq_step_rating_per_user_session', 'step_ratings', ['step_id', 'user_id', 'session_id'])

def downgrade():
    op.drop_constraint('uq_step_rating_per_user_session', 'step_ratings', type_='unique')
    op.create_unique_constraint('uq_step_rating_per_user', 'step_ratings', ['step_id', 'user_id'])
    op.alter_column('step_ratings', 'rating', nullable=False)
    op.drop_index('ix_step_ratings_step_helpful', 'step_ratings')
    op.drop_index('ix_sessions_completed', 'sessions')
    op.drop_index('ix_session_ratings_account_created', 'session_ratings')
    op.drop_index('ix_session_ratings_tree_created', 'session_ratings')
    op.drop_table('session_ratings')
```

**Step 2: Run the migration**

```bash
cd backend && alembic upgrade head
```

Expected: Migration applies successfully with no errors.

**Step 3: Verify table exists**

```bash
docker exec -it patherly_postgres psql -U postgres -d patherly -c "\d session_ratings"
```

Expected: Table with columns id, session_id, user_id, tree_id, account_id, rating, comment, created_at.

**Step 4: Commit**

```bash
git add backend/alembic/versions/039_*
git commit -m "feat: add session_ratings table and analytics indexes"
```

---

## Task 2: SessionRating Model + Schemas

**Files:**
- Create: `backend/app/models/session_rating.py`
- Modify: `backend/app/models/__init__.py`
- Create: `backend/app/schemas/analytics.py`
- Modify: `backend/app/schemas/__init__.py` (if it exists as a barrel export)

**Step 1: Create the SessionRating model**

Create `backend/app/models/session_rating.py`:

```python
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, CheckConstraint, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.core.database import Base


class SessionRating(Base):
    __tablename__ = "session_ratings"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    session_id = Column(UUID(as_uuid=True), ForeignKey("sessions.id", ondelete="CASCADE"), nullable=False)
    user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
    tree_id = Column(UUID(as_uuid=True), ForeignKey("trees.id", ondelete="CASCADE"), nullable=False)
    account_id = Column(UUID(as_uuid=True), ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False)
    rating = Column(Integer, nullable=False)
    comment = Column(String(500), nullable=True)
    created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)

    __table_args__ = (
        CheckConstraint("rating >= 1 AND rating <= 5", name="ck_session_ratings_rating_range"),
        UniqueConstraint("session_id", name="uq_session_ratings_session_id"),
    )

    # Relationships
    session = relationship("Session", foreign_keys=[session_id])
    user = relationship("User", foreign_keys=[user_id])
    tree = relationship("Tree", foreign_keys=[tree_id])
```

**Step 2: Register in models/__init__.py**

Add to `backend/app/models/__init__.py`:

```python
from .session_rating import SessionRating
```

And add `"SessionRating"` to the `__all__` list.

**Step 3: Create analytics schemas**

Create `backend/app/schemas/analytics.py`:

```python
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field


# --- Session Rating Schemas ---

class SessionRatingCreate(BaseModel):
    rating: int = Field(..., ge=1, le=5)
    comment: Optional[str] = Field(None, max_length=500)


class SessionRatingResponse(BaseModel):
    id: str
    session_id: str
    rating: int
    comment: Optional[str]
    created_at: datetime

    model_config = {"from_attributes": True}


class FlowRatingItem(BaseModel):
    rating: int
    comment: Optional[str]
    user_name: Optional[str]
    created_at: datetime


# --- Step Feedback Schema ---

class StepFeedbackCreate(BaseModel):
    session_id: str
    was_helpful: bool


# --- Analytics Response Schemas ---

class OutcomeBreakdown(BaseModel):
    resolved: int = 0
    escalated: int = 0
    workaround: int = 0
    unresolved: int = 0


class AnalyticsSummary(BaseModel):
    total_sessions: int
    completed_sessions: int
    completion_rate: float
    avg_duration_minutes: float
    outcome_breakdown: OutcomeBreakdown


class TimeSeriesPoint(BaseModel):
    date: str
    sessions: int = 0
    resolved: int = 0
    escalated: int = 0
    workaround: int = 0
    unresolved: int = 0


class TopFlow(BaseModel):
    tree_id: str
    name: str
    sessions: int
    completion_rate: float
    avg_duration_minutes: float
    avg_csat: Optional[float] = None


class TopEngineer(BaseModel):
    user_id: str
    name: str
    sessions: int
    completion_rate: float
    avg_duration_minutes: float


class TeamAnalyticsResponse(BaseModel):
    summary: AnalyticsSummary
    time_series: list[TimeSeriesPoint]
    top_flows: list[TopFlow]
    top_engineers: list[TopEngineer]


class PersonalAnalyticsResponse(BaseModel):
    summary: AnalyticsSummary
    time_series: list[TimeSeriesPoint]
    top_flows: list[TopFlow]


class StepFeedbackSummary(BaseModel):
    node_id: str
    node_title: str
    helpful_yes: int
    helpful_no: int
    helpful_rate: float


class FlowAnalyticsResponse(BaseModel):
    summary: AnalyticsSummary
    avg_csat: Optional[float]
    total_ratings: int
    time_series: list[TimeSeriesPoint]
    step_feedback: list[StepFeedbackSummary]
    recent_comments: list[FlowRatingItem]
```

**Step 4: Verify import works**

```bash
cd backend && python -c "from app.models.session_rating import SessionRating; print('OK')"
```

**Step 5: Commit**

```bash
git add backend/app/models/session_rating.py backend/app/models/__init__.py backend/app/schemas/analytics.py
git commit -m "feat: add SessionRating model and analytics schemas"
```

---

## Task 3: Session Rating Endpoint

**Files:**
- Create: `backend/app/api/endpoints/ratings.py`
- Modify: `backend/app/api/router.py`
- Create: `backend/tests/test_ratings.py`

**Step 1: Write tests**

Create `backend/tests/test_ratings.py`:

```python
import pytest
from httpx import AsyncClient

pytestmark = pytest.mark.asyncio


async def test_rate_session_success(client: AsyncClient, auth_headers: dict, completed_session_id: str):
    """Rate a completed session with CSAT score."""
    response = await client.post(
        f"/api/v1/sessions/{completed_session_id}/rate",
        json={"rating": 4, "comment": "Very helpful flow"},
        headers=auth_headers,
    )
    assert response.status_code == 201
    data = response.json()
    assert data["rating"] == 4
    assert data["comment"] == "Very helpful flow"


async def test_rate_session_duplicate(client: AsyncClient, auth_headers: dict, completed_session_id: str):
    """Cannot rate same session twice."""
    await client.post(
        f"/api/v1/sessions/{completed_session_id}/rate",
        json={"rating": 4},
        headers=auth_headers,
    )
    response = await client.post(
        f"/api/v1/sessions/{completed_session_id}/rate",
        json={"rating": 5},
        headers=auth_headers,
    )
    assert response.status_code == 409


async def test_rate_session_invalid_rating(client: AsyncClient, auth_headers: dict, completed_session_id: str):
    """Rating must be 1-5."""
    response = await client.post(
        f"/api/v1/sessions/{completed_session_id}/rate",
        json={"rating": 6},
        headers=auth_headers,
    )
    assert response.status_code == 422


async def test_rate_incomplete_session(client: AsyncClient, auth_headers: dict, active_session_id: str):
    """Cannot rate a session that hasn't been completed."""
    response = await client.post(
        f"/api/v1/sessions/{active_session_id}/rate",
        json={"rating": 4},
        headers=auth_headers,
    )
    assert response.status_code == 400
```

**Step 2: Run tests to verify they fail**

```bash
cd backend && python -m pytest tests/test_ratings.py -v --override-ini="addopts="
```

Expected: FAIL (endpoint doesn't exist yet).

**Step 3: Implement the endpoint**

Create `backend/app/api/endpoints/ratings.py`:

```python
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.database import get_db
from app.api.deps import get_current_active_user
from app.models import User, Session, SessionRating
from app.schemas.analytics import SessionRatingCreate, SessionRatingResponse

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


@router.post("/sessions/{session_id}/rate", response_model=SessionRatingResponse, status_code=status.HTTP_201_CREATED)
async def rate_session(
    session_id: UUID,
    data: SessionRatingCreate,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_active_user),
):
    """Submit a CSAT rating (1-5) for a completed session."""
    # Verify session exists and belongs to user
    result = await db.execute(select(Session).where(Session.id == session_id))
    session = result.scalar_one_or_none()
    if not session:
        raise HTTPException(status_code=404, detail="Session not found")
    if session.user_id != current_user.id:
        raise HTTPException(status_code=403, detail="Not your session")
    if not session.completed_at:
        raise HTTPException(status_code=400, detail="Session not completed yet")

    # Check for duplicate
    existing = await db.execute(
        select(SessionRating).where(SessionRating.session_id == session_id)
    )
    if existing.scalar_one_or_none():
        raise HTTPException(status_code=409, detail="Session already rated")

    rating = SessionRating(
        session_id=session_id,
        user_id=current_user.id,
        tree_id=session.tree_id,
        account_id=current_user.account_id,
        rating=data.rating,
        comment=data.comment,
    )
    db.add(rating)
    await db.commit()
    await db.refresh(rating)

    return SessionRatingResponse(
        id=str(rating.id),
        session_id=str(rating.session_id),
        rating=rating.rating,
        comment=rating.comment,
        created_at=rating.created_at,
    )
```

**Step 4: Register the route**

In `backend/app/api/router.py`, add:

```python
from app.api.endpoints import ratings
```

And:

```python
api_router.include_router(ratings.router)
```

**Step 5: Run tests to verify they pass**

```bash
cd backend && python -m pytest tests/test_ratings.py -v --override-ini="addopts="
```

Note: Tests may need fixture adjustments (`completed_session_id`, `active_session_id`). Create appropriate fixtures in `conftest.py` if they don't exist — a completed session is one where `completed_at` is set and `outcome` is populated.

**Step 6: Commit**

```bash
git add backend/app/api/endpoints/ratings.py backend/app/api/router.py backend/tests/test_ratings.py
git commit -m "feat: add session CSAT rating endpoint"
```

---

## Task 4: Step Feedback Endpoint

**Files:**
- Modify: `backend/app/api/endpoints/ratings.py`
- Add tests to: `backend/tests/test_ratings.py`

**Step 1: Add step feedback tests**

Append to `backend/tests/test_ratings.py`:

```python
async def test_step_feedback_thumbs_up(client: AsyncClient, auth_headers: dict, step_id: str, session_id: str):
    """Submit thumbs-up feedback for a step."""
    response = await client.post(
        f"/api/v1/steps/{step_id}/feedback",
        json={"session_id": session_id, "was_helpful": True},
        headers=auth_headers,
    )
    assert response.status_code == 201
    assert response.json()["was_helpful"] is True


async def test_step_feedback_toggle(client: AsyncClient, auth_headers: dict, step_id: str, session_id: str):
    """Submitting again for same step+session updates the rating."""
    await client.post(
        f"/api/v1/steps/{step_id}/feedback",
        json={"session_id": session_id, "was_helpful": True},
        headers=auth_headers,
    )
    response = await client.post(
        f"/api/v1/steps/{step_id}/feedback",
        json={"session_id": session_id, "was_helpful": False},
        headers=auth_headers,
    )
    assert response.status_code == 200
    assert response.json()["was_helpful"] is False
```

**Step 2: Implement the endpoint**

Add to `backend/app/api/endpoints/ratings.py`:

```python
from app.models import StepLibrary, StepRating
from app.schemas.analytics import StepFeedbackCreate


@router.post("/steps/{step_id}/feedback", status_code=status.HTTP_201_CREATED)
async def submit_step_feedback(
    step_id: UUID,
    data: StepFeedbackCreate,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_active_user),
):
    """Submit thumbs up/down feedback for a step used in a session."""
    # Verify step exists
    result = await db.execute(select(StepLibrary).where(StepLibrary.id == step_id))
    step = result.scalar_one_or_none()
    if not step:
        raise HTTPException(status_code=404, detail="Step not found")

    session_uuid = UUID(data.session_id)

    # Check for existing feedback for this step+user+session
    existing_result = await db.execute(
        select(StepRating).where(
            StepRating.step_id == step_id,
            StepRating.user_id == current_user.id,
            StepRating.session_id == session_uuid,
        )
    )
    existing = existing_result.scalar_one_or_none()

    if existing:
        existing.was_helpful = data.was_helpful
        status_code = 200
    else:
        rating = StepRating(
            step_id=step_id,
            user_id=current_user.id,
            session_id=session_uuid,
            was_helpful=data.was_helpful,
        )
        db.add(rating)
        status_code = 201

    # Update aggregates on step_library
    await _update_step_helpful_counts(db, step_id)
    await db.commit()

    return {"step_id": str(step_id), "was_helpful": data.was_helpful, "status": "created" if status_code == 201 else "updated"}


async def _update_step_helpful_counts(db: AsyncSession, step_id: UUID):
    """Recalculate helpful_yes and helpful_no on step_library."""
    from sqlalchemy import func
    yes_count = await db.execute(
        select(func.count()).where(StepRating.step_id == step_id, StepRating.was_helpful == True)
    )
    no_count = await db.execute(
        select(func.count()).where(StepRating.step_id == step_id, StepRating.was_helpful == False)
    )
    await db.execute(
        StepLibrary.__table__.update()
        .where(StepLibrary.id == step_id)
        .values(helpful_yes=yes_count.scalar(), helpful_no=no_count.scalar())
    )
```

**Step 3: Run tests**

```bash
cd backend && python -m pytest tests/test_ratings.py -v --override-ini="addopts="
```

**Step 4: Commit**

```bash
git add backend/app/api/endpoints/ratings.py backend/tests/test_ratings.py
git commit -m "feat: add step thumbs up/down feedback endpoint"
```

---

## Task 5: Team Analytics Endpoint

**Files:**
- Create: `backend/app/api/endpoints/analytics.py`
- Modify: `backend/app/api/router.py`
- Create: `backend/tests/test_analytics.py`

**Step 1: Write tests**

Create `backend/tests/test_analytics.py`:

```python
import pytest
from httpx import AsyncClient

pytestmark = pytest.mark.asyncio


async def test_team_analytics_success(client: AsyncClient, admin_auth_headers: dict):
    """Team admin can access team analytics."""
    response = await client.get(
        "/api/v1/analytics/team?period=30d",
        headers=admin_auth_headers,
    )
    assert response.status_code == 200
    data = response.json()
    assert "summary" in data
    assert "time_series" in data
    assert "top_flows" in data
    assert "top_engineers" in data
    assert "total_sessions" in data["summary"]
    assert "completion_rate" in data["summary"]


async def test_team_analytics_forbidden_for_engineer(client: AsyncClient, auth_headers: dict):
    """Regular engineers cannot access team analytics."""
    response = await client.get(
        "/api/v1/analytics/team",
        headers=auth_headers,
    )
    assert response.status_code == 403


async def test_personal_analytics_success(client: AsyncClient, auth_headers: dict):
    """Any user can access personal analytics."""
    response = await client.get(
        "/api/v1/analytics/me?period=30d",
        headers=auth_headers,
    )
    assert response.status_code == 200
    data = response.json()
    assert "summary" in data
    assert "time_series" in data
    assert "top_flows" in data


async def test_flow_analytics_success(client: AsyncClient, auth_headers: dict, tree_id: str):
    """Can access analytics for a visible flow."""
    response = await client.get(
        f"/api/v1/analytics/flows/{tree_id}",
        headers=auth_headers,
    )
    assert response.status_code == 200
    data = response.json()
    assert "summary" in data
    assert "step_feedback" in data
    assert "recent_comments" in data
```

**Step 2: Implement analytics endpoint**

Create `backend/app/api/endpoints/analytics.py`:

```python
from datetime import datetime, timezone, timedelta
from uuid import UUID
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import select, func, case, and_, cast, Date
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.database import get_db
from app.api.deps import get_current_active_user
from app.models import User, Session, Tree, SessionRating, StepRating
from app.schemas.analytics import (
    TeamAnalyticsResponse, PersonalAnalyticsResponse, FlowAnalyticsResponse,
    AnalyticsSummary, OutcomeBreakdown, TimeSeriesPoint,
    TopFlow, TopEngineer, StepFeedbackSummary, FlowRatingItem,
)

router = APIRouter(prefix="/analytics", tags=["analytics"])


def _get_period_start(period: str) -> datetime:
    days = {"7d": 7, "30d": 30, "90d": 90}.get(period, 30)
    return datetime.now(timezone.utc) - timedelta(days=days)


async def _build_summary(db: AsyncSession, base_filter) -> AnalyticsSummary:
    """Build analytics summary from a filtered session query."""
    # Total and completed counts
    total_q = await db.execute(select(func.count()).select_from(Session).where(*base_filter))
    total = total_q.scalar() or 0

    completed_q = await db.execute(
        select(func.count()).select_from(Session).where(*base_filter, Session.completed_at.isnot(None))
    )
    completed = completed_q.scalar() or 0

    # Avg duration (minutes)
    duration_q = await db.execute(
        select(
            func.avg(func.extract('epoch', Session.completed_at - Session.started_at) / 60)
        ).where(*base_filter, Session.completed_at.isnot(None))
    )
    avg_duration = round(float(duration_q.scalar() or 0), 1)

    # Outcome breakdown
    outcome_q = await db.execute(
        select(Session.outcome, func.count()).where(
            *base_filter, Session.completed_at.isnot(None), Session.outcome.isnot(None)
        ).group_by(Session.outcome)
    )
    outcomes = dict(outcome_q.all())

    return AnalyticsSummary(
        total_sessions=total,
        completed_sessions=completed,
        completion_rate=round(completed / total, 3) if total > 0 else 0,
        avg_duration_minutes=avg_duration,
        outcome_breakdown=OutcomeBreakdown(
            resolved=outcomes.get("resolved", 0),
            escalated=outcomes.get("escalated", 0),
            workaround=outcomes.get("workaround", 0),
            unresolved=outcomes.get("unresolved", 0),
        ),
    )


async def _build_time_series(db: AsyncSession, base_filter, period_start: datetime) -> list[TimeSeriesPoint]:
    """Build daily time-series of session counts by outcome."""
    rows = await db.execute(
        select(
            cast(Session.started_at, Date).label("date"),
            func.count().label("sessions"),
            func.count().filter(Session.outcome == "resolved").label("resolved"),
            func.count().filter(Session.outcome == "escalated").label("escalated"),
            func.count().filter(Session.outcome == "workaround").label("workaround"),
            func.count().filter(Session.outcome == "unresolved").label("unresolved"),
        ).where(*base_filter, Session.started_at >= period_start)
        .group_by(cast(Session.started_at, Date))
        .order_by(cast(Session.started_at, Date))
    )
    return [
        TimeSeriesPoint(
            date=str(row.date), sessions=row.sessions,
            resolved=row.resolved, escalated=row.escalated,
            workaround=row.workaround, unresolved=row.unresolved,
        )
        for row in rows.all()
    ]


@router.get("/team", response_model=TeamAnalyticsResponse)
async def get_team_analytics(
    period: str = Query("30d", pattern="^(7d|30d|90d)$"),
    engineer_id: Optional[UUID] = None,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_active_user),
):
    """Team analytics — team_admin or super_admin only."""
    if not (current_user.is_team_admin or current_user.role == "super_admin"):
        raise HTTPException(status_code=403, detail="Team admin access required")

    period_start = _get_period_start(period)
    base_filter = [
        Session.started_at >= period_start,
        Tree.account_id == current_user.account_id,
    ]
    if engineer_id:
        base_filter.append(Session.user_id == engineer_id)

    # Need to join Session to Tree for account_id scoping
    # Adjust queries to use join
    summary = await _build_summary_with_join(db, base_filter, period_start)
    time_series = await _build_time_series_with_join(db, base_filter, period_start)

    # Top flows
    top_flows_q = await db.execute(
        select(
            Tree.id, Tree.name,
            func.count(Session.id).label("sessions"),
            (func.count().filter(Session.completed_at.isnot(None)) * 1.0 / func.count()).label("completion_rate"),
            func.avg(func.extract('epoch', Session.completed_at - Session.started_at) / 60).label("avg_duration"),
        )
        .join(Tree, Session.tree_id == Tree.id)
        .where(Session.started_at >= period_start, Tree.account_id == current_user.account_id)
        .group_by(Tree.id, Tree.name)
        .order_by(func.count(Session.id).desc())
        .limit(10)
    )
    top_flows = [
        TopFlow(
            tree_id=str(row.id), name=row.name, sessions=row.sessions,
            completion_rate=round(float(row.completion_rate or 0), 3),
            avg_duration_minutes=round(float(row.avg_duration or 0), 1),
        )
        for row in top_flows_q.all()
    ]

    # Top engineers
    top_engineers_q = await db.execute(
        select(
            User.id, User.name,
            func.count(Session.id).label("sessions"),
            (func.count().filter(Session.completed_at.isnot(None)) * 1.0 / func.count()).label("completion_rate"),
            func.avg(func.extract('epoch', Session.completed_at - Session.started_at) / 60).label("avg_duration"),
        )
        .join(User, Session.user_id == User.id)
        .join(Tree, Session.tree_id == Tree.id)
        .where(Session.started_at >= period_start, Tree.account_id == current_user.account_id)
        .group_by(User.id, User.name)
        .order_by(func.count(Session.id).desc())
        .limit(10)
    )
    top_engineers = [
        TopEngineer(
            user_id=str(row.id), name=row.name or "Unknown", sessions=row.sessions,
            completion_rate=round(float(row.completion_rate or 0), 3),
            avg_duration_minutes=round(float(row.avg_duration or 0), 1),
        )
        for row in top_engineers_q.all()
    ]

    return TeamAnalyticsResponse(
        summary=summary, time_series=time_series,
        top_flows=top_flows, top_engineers=top_engineers,
    )


@router.get("/me", response_model=PersonalAnalyticsResponse)
async def get_personal_analytics(
    period: str = Query("30d", pattern="^(7d|30d|90d)$"),
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_active_user),
):
    """Personal analytics — any authenticated user."""
    period_start = _get_period_start(period)
    base_filter = [Session.started_at >= period_start, Session.user_id == current_user.id]

    summary = await _build_summary(db, base_filter)
    time_series = await _build_time_series(db, base_filter, period_start)

    # My top flows
    top_flows_q = await db.execute(
        select(
            Tree.id, Tree.name,
            func.count(Session.id).label("sessions"),
            (func.count().filter(Session.completed_at.isnot(None)) * 1.0 / func.count()).label("completion_rate"),
            func.avg(func.extract('epoch', Session.completed_at - Session.started_at) / 60).label("avg_duration"),
        )
        .join(Tree, Session.tree_id == Tree.id)
        .where(Session.started_at >= period_start, Session.user_id == current_user.id)
        .group_by(Tree.id, Tree.name)
        .order_by(func.count(Session.id).desc())
        .limit(10)
    )
    top_flows = [
        TopFlow(
            tree_id=str(row.id), name=row.name, sessions=row.sessions,
            completion_rate=round(float(row.completion_rate or 0), 3),
            avg_duration_minutes=round(float(row.avg_duration or 0), 1),
        )
        for row in top_flows_q.all()
    ]

    return PersonalAnalyticsResponse(
        summary=summary, time_series=time_series, top_flows=top_flows,
    )


@router.get("/flows/{tree_id}", response_model=FlowAnalyticsResponse)
async def get_flow_analytics(
    tree_id: UUID,
    period: str = Query("30d", pattern="^(7d|30d|90d)$"),
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_active_user),
):
    """Analytics for a specific flow."""
    # Verify tree exists and user can view it
    result = await db.execute(select(Tree).where(Tree.id == tree_id))
    tree = result.scalar_one_or_none()
    if not tree:
        raise HTTPException(status_code=404, detail="Flow not found")

    period_start = _get_period_start(period)
    base_filter = [Session.started_at >= period_start, Session.tree_id == tree_id]

    summary = await _build_summary(db, base_filter)
    time_series = await _build_time_series(db, base_filter, period_start)

    # CSAT stats
    csat_q = await db.execute(
        select(func.avg(SessionRating.rating), func.count())
        .where(SessionRating.tree_id == tree_id, SessionRating.created_at >= period_start)
    )
    csat_row = csat_q.one()
    avg_csat = round(float(csat_row[0]), 1) if csat_row[0] else None
    total_ratings = csat_row[1]

    # Step feedback (thumbs) — aggregate by node_id from decisions JSONB
    # This requires joining step_ratings with session decisions
    # For v1, return step_library-level feedback if steps are from library
    step_feedback: list[StepFeedbackSummary] = []

    # Recent comments
    comments_q = await db.execute(
        select(SessionRating.rating, SessionRating.comment, User.name, SessionRating.created_at)
        .join(User, SessionRating.user_id == User.id)
        .where(
            SessionRating.tree_id == tree_id,
            SessionRating.comment.isnot(None),
            SessionRating.comment != "",
        )
        .order_by(SessionRating.created_at.desc())
        .limit(10)
    )
    recent_comments = [
        FlowRatingItem(
            rating=row.rating, comment=row.comment,
            user_name=row.name, created_at=row.created_at,
        )
        for row in comments_q.all()
    ]

    return FlowAnalyticsResponse(
        summary=summary, avg_csat=avg_csat, total_ratings=total_ratings,
        time_series=time_series, step_feedback=step_feedback,
        recent_comments=recent_comments,
    )
```

Note: The `_build_summary_with_join` and `_build_time_series_with_join` helper functions need to be implemented with proper Session-Tree joins for account scoping. The implementing engineer should refactor `_build_summary` and `_build_time_series` to optionally accept a join condition.

**Step 3: Register route**

In `backend/app/api/router.py`:

```python
from app.api.endpoints import analytics
api_router.include_router(analytics.router)
```

**Step 4: Run tests**

```bash
cd backend && python -m pytest tests/test_analytics.py -v --override-ini="addopts="
```

**Step 5: Commit**

```bash
git add backend/app/api/endpoints/analytics.py backend/app/api/router.py backend/tests/test_analytics.py
git commit -m "feat: add team, personal, and flow analytics endpoints"
```

---

## Task 6: Frontend Setup — Recharts, Types, API Client

**Files:**
- Modify: `frontend/package.json` (install recharts)
- Create: `frontend/src/types/analytics.ts`
- Modify: `frontend/src/types/index.ts`
- Create: `frontend/src/api/analytics.ts`
- Modify: `frontend/src/api/index.ts`
- Modify: `frontend/src/api/sessions.ts`

**Step 1: Install Recharts**

```bash
cd frontend && npm install recharts
```

**Step 2: Create analytics types**

Create `frontend/src/types/analytics.ts`:

```typescript
export interface OutcomeBreakdown {
  resolved: number
  escalated: number
  workaround: number
  unresolved: number
}

export interface AnalyticsSummary {
  total_sessions: number
  completed_sessions: number
  completion_rate: number
  avg_duration_minutes: number
  outcome_breakdown: OutcomeBreakdown
}

export interface TimeSeriesPoint {
  date: string
  sessions: number
  resolved: number
  escalated: number
  workaround: number
  unresolved: number
}

export interface TopFlow {
  tree_id: string
  name: string
  sessions: number
  completion_rate: number
  avg_duration_minutes: number
  avg_csat?: number
}

export interface TopEngineer {
  user_id: string
  name: string
  sessions: number
  completion_rate: number
  avg_duration_minutes: number
}

export interface TeamAnalyticsResponse {
  summary: AnalyticsSummary
  time_series: TimeSeriesPoint[]
  top_flows: TopFlow[]
  top_engineers: TopEngineer[]
}

export interface PersonalAnalyticsResponse {
  summary: AnalyticsSummary
  time_series: TimeSeriesPoint[]
  top_flows: TopFlow[]
}

export interface StepFeedbackSummary {
  node_id: string
  node_title: string
  helpful_yes: number
  helpful_no: number
  helpful_rate: number
}

export interface FlowRatingItem {
  rating: number
  comment?: string
  user_name?: string
  created_at: string
}

export interface FlowAnalyticsResponse {
  summary: AnalyticsSummary
  avg_csat?: number
  total_ratings: number
  time_series: TimeSeriesPoint[]
  step_feedback: StepFeedbackSummary[]
  recent_comments: FlowRatingItem[]
}

export type AnalyticsPeriod = '7d' | '30d' | '90d'
```

**Step 3: Export from types/index.ts**

Add to `frontend/src/types/index.ts`:

```typescript
export type * from './analytics'
```

**Step 4: Create analytics API client**

Create `frontend/src/api/analytics.ts`:

```typescript
import { apiClient } from './client'
import type {
  TeamAnalyticsResponse,
  PersonalAnalyticsResponse,
  FlowAnalyticsResponse,
  AnalyticsPeriod,
} from '@/types'

export const analyticsApi = {
  async getTeamAnalytics(period: AnalyticsPeriod = '30d', engineerId?: string): Promise<TeamAnalyticsResponse> {
    const params: Record<string, string> = { period }
    if (engineerId) params.engineer_id = engineerId
    const response = await apiClient.get<TeamAnalyticsResponse>('/analytics/team', { params })
    return response.data
  },

  async getPersonalAnalytics(period: AnalyticsPeriod = '30d'): Promise<PersonalAnalyticsResponse> {
    const response = await apiClient.get<PersonalAnalyticsResponse>('/analytics/me', { params: { period } })
    return response.data
  },

  async getFlowAnalytics(treeId: string, period: AnalyticsPeriod = '30d'): Promise<FlowAnalyticsResponse> {
    const response = await apiClient.get<FlowAnalyticsResponse>(`/analytics/flows/${treeId}`, { params: { period } })
    return response.data
  },

  async rateSession(sessionId: string, rating: number, comment?: string): Promise<void> {
    await apiClient.post(`/sessions/${sessionId}/rate`, { rating, comment })
  },

  async submitStepFeedback(stepId: string, sessionId: string, wasHelpful: boolean): Promise<void> {
    await apiClient.post(`/steps/${stepId}/feedback`, { session_id: sessionId, was_helpful: wasHelpful })
  },
}
```

**Step 5: Export from api/index.ts**

Add to `frontend/src/api/index.ts`:

```typescript
export { analyticsApi } from './analytics'
```

**Step 6: Verify build**

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

**Step 7: Commit**

```bash
git add frontend/package.json frontend/package-lock.json frontend/src/types/analytics.ts frontend/src/types/index.ts frontend/src/api/analytics.ts frontend/src/api/index.ts
git commit -m "feat: add recharts, analytics types, and API client"
```

---

## Task 7: Inline Step Feedback Component

**Files:**
- Create: `frontend/src/components/session/StepFeedback.tsx`
- Modify: `frontend/src/pages/TreeNavigationPage.tsx`
- Modify: `frontend/src/pages/ProceduralNavigationPage.tsx` (same pattern)

**Step 1: Create StepFeedback component**

Create `frontend/src/components/session/StepFeedback.tsx`:

```tsx
import { useState, useEffect } from 'react'
import { ThumbsUp, ThumbsDown } from 'lucide-react'
import { analyticsApi } from '@/api'
import { cn } from '@/lib/utils'

interface StepFeedbackProps {
  stepId: string
  sessionId: string
}

const HINT_KEY = 'rf-step-feedback-hint-dismissed'

export function StepFeedback({ stepId, sessionId }: StepFeedbackProps) {
  const [feedback, setFeedback] = useState<boolean | null>(null)
  const [submitting, setSubmitting] = useState(false)
  const [showHint, setShowHint] = useState(false)

  useEffect(() => {
    if (!localStorage.getItem(HINT_KEY)) {
      setShowHint(true)
    }
  }, [])

  const handleFeedback = async (wasHelpful: boolean) => {
    if (submitting) return
    setSubmitting(true)
    try {
      const newValue = feedback === wasHelpful ? null : wasHelpful
      if (newValue !== null) {
        await analyticsApi.submitStepFeedback(stepId, sessionId, newValue)
      }
      setFeedback(newValue)
      if (showHint) {
        setShowHint(false)
        localStorage.setItem(HINT_KEY, '1')
      }
    } catch {
      // Silently fail — feedback is non-critical
    } finally {
      setSubmitting(false)
    }
  }

  return (
    <div className="flex items-center gap-2">
      {showHint && (
        <span className="text-xs text-muted-foreground">Rate this step to help improve flows</span>
      )}
      <button
        onClick={() => handleFeedback(true)}
        disabled={submitting}
        className={cn(
          'rounded-md p-1.5 transition-colors',
          feedback === true
            ? 'text-emerald-400 bg-emerald-400/10'
            : 'text-muted-foreground hover:text-emerald-400 hover:bg-accent'
        )}
        title="Helpful"
      >
        <ThumbsUp size={14} />
      </button>
      <button
        onClick={() => handleFeedback(false)}
        disabled={submitting}
        className={cn(
          'rounded-md p-1.5 transition-colors',
          feedback === false
            ? 'text-red-400 bg-red-400/10'
            : 'text-muted-foreground hover:text-red-400 hover:bg-accent'
        )}
        title="Not helpful"
      >
        <ThumbsDown size={14} />
      </button>
    </div>
  )
}
```

**Step 2: Integrate into TreeNavigationPage**

In `TreeNavigationPage.tsx`, find where step content is rendered (after the step description/help_text, before decision options or continue button). Add:

```tsx
import { StepFeedback } from '@/components/session/StepFeedback'

// Inside the step rendering, after content and before options:
{currentNode && session && (
  <div className="mt-3 flex justify-end">
    <StepFeedback stepId={currentNode.id} sessionId={session.id} />
  </div>
)}
```

Apply the same pattern to `ProceduralNavigationPage.tsx` for procedural flow steps.

**Step 3: Verify build**

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

**Step 4: Commit**

```bash
git add frontend/src/components/session/StepFeedback.tsx frontend/src/pages/TreeNavigationPage.tsx frontend/src/pages/ProceduralNavigationPage.tsx
git commit -m "feat: add inline step thumbs up/down feedback during sessions"
```

---

## Task 8: CSAT Modal Component

**Files:**
- Create: `frontend/src/components/session/CSATModal.tsx`
- Modify: `frontend/src/pages/TreeNavigationPage.tsx`
- Modify: `frontend/src/pages/ProceduralNavigationPage.tsx`

**Step 1: Create CSATModal**

Create `frontend/src/components/session/CSATModal.tsx`:

```tsx
import { useState } from 'react'
import { Star } from 'lucide-react'
import { Modal } from '@/components/common/Modal'
import { analyticsApi } from '@/api'
import { cn } from '@/lib/utils'

interface CSATModalProps {
  isOpen: boolean
  onClose: () => void
  sessionId: string
}

const RATED_SESSIONS_KEY = 'rf-rated-sessions'

function getRatedSessions(): string[] {
  try {
    return JSON.parse(localStorage.getItem(RATED_SESSIONS_KEY) || '[]')
  } catch {
    return []
  }
}

function markSessionRated(sessionId: string) {
  const rated = getRatedSessions()
  rated.push(sessionId)
  localStorage.setItem(RATED_SESSIONS_KEY, JSON.stringify(rated.slice(-100)))
}

export function hasBeenRated(sessionId: string): boolean {
  return getRatedSessions().includes(sessionId)
}

export function CSATModal({ isOpen, onClose, sessionId }: CSATModalProps) {
  const [rating, setRating] = useState(0)
  const [hoveredRating, setHoveredRating] = useState(0)
  const [comment, setComment] = useState('')
  const [submitting, setSubmitting] = useState(false)

  const handleSubmit = async () => {
    if (rating === 0 || submitting) return
    setSubmitting(true)
    try {
      await analyticsApi.rateSession(sessionId, rating, comment || undefined)
      markSessionRated(sessionId)
      onClose()
    } catch {
      // Silently fail
      onClose()
    } finally {
      setSubmitting(false)
    }
  }

  const handleSkip = () => {
    markSessionRated(sessionId)
    onClose()
  }

  return (
    <Modal isOpen={isOpen} onClose={handleSkip} title="How was this flow?">
      <div className="space-y-4">
        <p className="text-sm text-muted-foreground">
          Your feedback helps flow authors improve troubleshooting paths.
        </p>

        {/* Star rating */}
        <div className="flex items-center justify-center gap-1">
          {[1, 2, 3, 4, 5].map((value) => (
            <button
              key={value}
              onClick={() => setRating(value)}
              onMouseEnter={() => setHoveredRating(value)}
              onMouseLeave={() => setHoveredRating(0)}
              className="p-1 transition-colors"
            >
              <Star
                size={28}
                className={cn(
                  'transition-colors',
                  (hoveredRating || rating) >= value
                    ? 'fill-yellow-400 text-yellow-400'
                    : 'fill-none text-muted-foreground'
                )}
              />
            </button>
          ))}
        </div>

        {/* Comment */}
        <textarea
          value={comment}
          onChange={(e) => setComment(e.target.value)}
          placeholder="Any comments? (optional)"
          maxLength={500}
          rows={3}
          className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20 resize-none"
        />

        {/* Actions */}
        <div className="flex items-center justify-between">
          <button onClick={handleSkip} className="text-sm text-muted-foreground hover:text-foreground transition-colors">
            Skip
          </button>
          <button
            onClick={handleSubmit}
            disabled={rating === 0 || submitting}
            className="rounded-lg bg-gradient-brand px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-primary/20 hover:opacity-90 transition-opacity disabled:opacity-50"
          >
            {submitting ? 'Submitting...' : 'Submit'}
          </button>
        </div>
      </div>
    </Modal>
  )
}
```

**Step 2: Integrate into session completion flow**

In `TreeNavigationPage.tsx`, after the `SessionOutcomeModal` submit handler resolves:

```tsx
import { CSATModal, hasBeenRated } from '@/components/session/CSATModal'

// Add state
const [csatOpen, setCsatOpen] = useState(false)

// In the outcome submit handler, after successful completion:
const handleSubmitOutcome = async (data) => {
  await sessionsApi.complete(session.id, data)
  // ... existing logic ...
  if (!hasBeenRated(session.id)) {
    setCsatOpen(true)
  }
}

// In JSX, after SessionOutcomeModal:
{session && (
  <CSATModal
    isOpen={csatOpen}
    onClose={() => setCsatOpen(false)}
    sessionId={session.id}
  />
)}
```

Apply the same pattern to `ProceduralNavigationPage.tsx`.

**Step 3: Verify build**

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

**Step 4: Commit**

```bash
git add frontend/src/components/session/CSATModal.tsx frontend/src/pages/TreeNavigationPage.tsx frontend/src/pages/ProceduralNavigationPage.tsx
git commit -m "feat: add CSAT rating modal after session completion"
```

---

## Task 9: Team Analytics Page

**Files:**
- Create: `frontend/src/pages/TeamAnalyticsPage.tsx`
- Modify: `frontend/src/router.tsx`
- Modify: `frontend/src/components/layout/Sidebar.tsx`

**Step 1: Create the page**

Create `frontend/src/pages/TeamAnalyticsPage.tsx`:

Build a page with:
- Header: "Team Analytics" + period dropdown (`<select>` with 7d/30d/90d)
- Stat cards row: Total Sessions, Completion Rate (%), Avg Duration, Active Engineers
- Time-series area chart using Recharts `<AreaChart>` with stacked outcomes (resolved=emerald, escalated=red, workaround=yellow, unresolved=slate)
- Two-column layout below:
  - Left: Top Flows table (name, sessions, completion rate, avg duration)
  - Right: Top Engineers table (name, sessions, completion rate, avg duration)

Use the `analyticsApi.getTeamAnalytics()` method. Loading state with `<Loader2>` spinner.

Guard with permissions: if not team_admin/super_admin, show "You don't have access to team analytics" message with a link to `/analytics/me`.

**Recharts imports needed:**

```tsx
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
```

**Chart color constants:**

```tsx
const CHART_COLORS = {
  resolved: '#34d399',
  escalated: '#f87171',
  workaround: '#fbbf24',
  unresolved: '#94a3b8',
}
```

**Step 2: Add route**

In `frontend/src/router.tsx`, add lazy import and route:

```tsx
const TeamAnalyticsPage = lazy(() => import('@/pages/TeamAnalyticsPage'))

// Inside AppLayout children:
{ path: 'analytics', element: <Suspense fallback={<PageLoader />}><TeamAnalyticsPage /></Suspense> },
```

**Step 3: Add nav item**

In `frontend/src/components/layout/Sidebar.tsx`, add between Sessions and Exports (in both collapsed and expanded branches):

```tsx
import { BarChart3 } from 'lucide-react'

<NavItem href="/analytics" icon={BarChart3} label="Analytics" />
```

**Step 4: Verify build**

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

**Step 5: Commit**

```bash
git add frontend/src/pages/TeamAnalyticsPage.tsx frontend/src/router.tsx frontend/src/components/layout/Sidebar.tsx
git commit -m "feat: add Team Analytics page with charts and leaderboards"
```

---

## Task 10: Personal Analytics Page

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

**Step 1: Create the page**

Create `frontend/src/pages/MyAnalyticsPage.tsx`:

Similar structure to TeamAnalyticsPage but:
- Header: "My Analytics"
- Stat cards: My Sessions, My Completion Rate, My Avg Duration, My Outcomes
- Sessions-per-day line chart (Recharts `<LineChart>`)
- Two-column layout:
  - Left: My Top Flows table
  - Right: Outcome distribution donut chart (Recharts `<PieChart>` with `<Pie>`)

Use `analyticsApi.getPersonalAnalytics()`. No permission guard — any user can access.

**Step 2: Add route**

```tsx
const MyAnalyticsPage = lazy(() => import('@/pages/MyAnalyticsPage'))

{ path: 'analytics/me', element: <Suspense fallback={<PageLoader />}><MyAnalyticsPage /></Suspense> },
```

**Step 3: Update TeamAnalyticsPage** — add a link/tab to switch between Team and Personal views.

**Step 4: Verify build**

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

**Step 5: Commit**

```bash
git add frontend/src/pages/MyAnalyticsPage.tsx frontend/src/router.tsx
git commit -m "feat: add My Analytics page with personal stats and charts"
```

---

## Task 11: Flow Analytics Panel

**Files:**
- Create: `frontend/src/components/analytics/FlowAnalyticsPanel.tsx`
- Modify: `frontend/src/pages/TreeEditorPage.tsx` or tree detail view

**Step 1: Create the panel**

Create `frontend/src/components/analytics/FlowAnalyticsPanel.tsx`:

A collapsible or tabbed panel showing:
- Summary stat row: Usage, Completion Rate, Avg Duration, CSAT score
- Mini area chart for sessions-over-time (compact, 200px height)
- Step feedback table: step name, helpful rate bar (green fill proportional to %), thumbs counts
- Recent comments list: star rating, comment text, user name, date

Use `analyticsApi.getFlowAnalytics(treeId)`.

**Step 2: Integrate into tree views**

Add as a tab or expandable section in the tree editor or tree detail page. The exact integration point depends on the current page structure — look for a natural place to add an "Analytics" tab alongside existing content.

**Step 3: Verify build**

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

**Step 4: Commit**

```bash
git add frontend/src/components/analytics/FlowAnalyticsPanel.tsx frontend/src/pages/TreeEditorPage.tsx
git commit -m "feat: add Flow Analytics panel with step feedback and CSAT"
```

---

## Task 12: Final Integration & Polish

**Step 1: Run full backend test suite**

```bash
cd backend && python -m pytest --override-ini="addopts=" -v
```

Fix any failures.

**Step 2: Run frontend build**

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

Fix any type errors.

**Step 3: Test end-to-end manually**

1. Start a session, verify inline thumbs appear on steps
2. Complete a session, verify CSAT modal appears
3. Submit a rating, verify it persists
4. Visit `/analytics` as team admin, verify charts render
5. Visit `/analytics/me` as engineer, verify personal stats

**Step 4: Final commit**

```bash
git add -A
git commit -m "fix: analytics integration polish and test fixes"
```
