diff --git a/backend/app/api/endpoints/sidebar.py b/backend/app/api/endpoints/sidebar.py new file mode 100644 index 00000000..a473433b --- /dev/null +++ b/backend/app/api/endpoints/sidebar.py @@ -0,0 +1,178 @@ +"""Sidebar stats and activity feed endpoint.""" + +from datetime import datetime, timedelta, timezone + +from fastapi import APIRouter, Depends, Query +from sqlalchemy import func, select, and_ +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.session import Session +from app.models.tree import Tree +from app.models.user import User +from app.schemas.sidebar import ( + SidebarActiveSession, + SidebarRecentSession, + SidebarStatsResponse, + SidebarTreeCounts, +) + +router = APIRouter(prefix="/sessions", tags=["sidebar"]) + + +@router.get("/sidebar-stats", response_model=SidebarStatsResponse) +async def get_sidebar_stats( + tz_offset: int = Query(..., description="Client UTC offset in minutes (e.g. -300 for EST)"), + current_user: User = Depends(get_current_active_user), + db: AsyncSession = Depends(get_db), +) -> SidebarStatsResponse: + """Get sidebar stats and activity feed for the current user. + + Returns daily stats (resolved count, active count, time in session) + and lists of active + recently completed sessions. + """ + # Compute "today" start in user's timezone, then convert to UTC + now_utc = datetime.now(timezone.utc) + user_tz = timezone(timedelta(minutes=-tz_offset)) + now_local = now_utc.astimezone(user_tz) + today_start_local = now_local.replace(hour=0, minute=0, second=0, microsecond=0) + today_start_utc = today_start_local.astimezone(timezone.utc) + + user_filter = Session.user_id == current_user.id + + # --- Resolved today --- + resolved_result = await db.execute( + select(func.count()).where( + and_( + user_filter, + Session.completed_at >= today_start_utc, + Session.outcome == "resolved", + ) + ) + ) + resolved_today = resolved_result.scalar() or 0 + + # --- Active count (all time, not just today) --- + active_result = await db.execute( + select(func.count()).where( + and_( + user_filter, + Session.started_at.isnot(None), + Session.completed_at.is_(None), + ) + ) + ) + active_count = active_result.scalar() or 0 + + # --- Completed session minutes today (active session time computed client-side) --- + duration_expr = func.extract( + "epoch", + Session.completed_at - Session.started_at, + ) / 60.0 + duration_result = await db.execute( + select(func.coalesce(func.sum(duration_expr), 0)).where( + and_( + user_filter, + Session.started_at.isnot(None), + Session.completed_at.isnot(None), + Session.started_at >= today_start_utc, + ) + ) + ) + total_minutes = int(duration_result.scalar() or 0) + + # --- Active sessions (max 5, most recent first) --- + active_sessions_result = await db.execute( + select( + Session.id, + Session.tree_id, + Session.started_at, + Session.ticket_number, + Session.psa_ticket_id, + Session.tree_snapshot["name"].as_string().label("tree_name"), + Session.tree_snapshot["tree_type"].as_string().label("tree_type"), + ) + .where( + and_( + user_filter, + Session.started_at.isnot(None), + Session.completed_at.is_(None), + ) + ) + .order_by(Session.started_at.desc()) + .limit(5) + ) + active_sessions = [ + SidebarActiveSession( + session_id=row.id, + tree_name=row.tree_name or "Unknown Flow", + tree_id=row.tree_id, + tree_type=row.tree_type or "troubleshooting", + started_at=row.started_at, + ticket_number=row.ticket_number, + psa_ticket_id=row.psa_ticket_id, + ) + for row in active_sessions_result.all() + ] + + # --- Recent completions (max 3, completed today, most recent first) --- + recent_result = await db.execute( + select( + Session.id, + Session.tree_id, + Session.completed_at, + Session.tree_snapshot["name"].as_string().label("tree_name"), + Session.tree_snapshot["tree_type"].as_string().label("tree_type"), + ) + .where( + and_( + user_filter, + Session.completed_at.isnot(None), + Session.completed_at >= today_start_utc, + ) + ) + .order_by(Session.completed_at.desc()) + .limit(3) + ) + recent_completions = [ + SidebarRecentSession( + session_id=row.id, + tree_name=row.tree_name or "Unknown Flow", + tree_id=row.tree_id, + tree_type=row.tree_type or "troubleshooting", + completed_at=row.completed_at, + ) + for row in recent_result.all() + ] + + # --- Tree counts (for All Flows sub-items) --- + tree_counts_result = await db.execute( + select( + func.count().label("total"), + func.count().filter(Tree.tree_type == "troubleshooting").label("troubleshooting"), + func.count().filter(Tree.tree_type == "procedural").label("procedural"), + func.count().filter(Tree.tree_type == "maintenance").label("maintenance"), + ).where( + and_( + Tree.account_id == current_user.account_id, + Tree.is_active.is_(True), + Tree.deleted_at.is_(None), + ) + ) + ) + tc = tree_counts_result.one() + + return SidebarStatsResponse( + resolved_today=resolved_today, + active_count=active_count, + total_session_minutes_today=total_minutes, + tree_counts=SidebarTreeCounts( + total=tc.total, + troubleshooting=tc.troubleshooting, + procedural=tc.procedural, + maintenance=tc.maintenance, + ), + active_sessions=active_sessions, + recent_completions=recent_completions, + ) diff --git a/backend/app/api/router.py b/backend/app/api/router.py index 0e656178..3bc9afde 100644 --- a/backend/app/api/router.py +++ b/backend/app/api/router.py @@ -1,5 +1,5 @@ from fastapi import APIRouter -from app.api.endpoints import auth, trees, sessions, invite, categories, tags, folders, step_categories, steps, admin, accounts, webhooks, shares, shared, tree_markdown +from app.api.endpoints import auth, trees, sessions, sidebar, invite, categories, tags, folders, step_categories, steps, admin, accounts, webhooks, shares, shared, tree_markdown from app.api.endpoints import admin_dashboard, admin_audit, admin_plan_limits, admin_feature_flags, admin_settings, admin_categories from app.api.endpoints import ratings, analytics from app.api.endpoints import target_lists @@ -23,6 +23,7 @@ api_router = APIRouter() api_router.include_router(auth.router) api_router.include_router(trees.router) +api_router.include_router(sidebar.router) api_router.include_router(sessions.router) api_router.include_router(invite.router) api_router.include_router(categories.router) diff --git a/backend/app/schemas/sidebar.py b/backend/app/schemas/sidebar.py new file mode 100644 index 00000000..95d92291 --- /dev/null +++ b/backend/app/schemas/sidebar.py @@ -0,0 +1,45 @@ +"""Schemas for sidebar stats endpoint.""" + +from datetime import datetime +from typing import Optional +from uuid import UUID + +from pydantic import BaseModel + + +class SidebarActiveSession(BaseModel): + """An active or paused session for the activity feed.""" + session_id: UUID + tree_name: str + tree_id: UUID + tree_type: str + started_at: datetime + ticket_number: Optional[str] = None + psa_ticket_id: Optional[str] = None + + +class SidebarRecentSession(BaseModel): + """A recently completed session for the activity feed.""" + session_id: UUID + tree_name: str + tree_id: UUID + tree_type: str + completed_at: datetime + + +class SidebarTreeCounts(BaseModel): + """Tree counts for All Flows sub-items.""" + total: int + troubleshooting: int + procedural: int + maintenance: int + + +class SidebarStatsResponse(BaseModel): + """Response for GET /sessions/sidebar-stats.""" + resolved_today: int + active_count: int + total_session_minutes_today: int + tree_counts: SidebarTreeCounts + active_sessions: list[SidebarActiveSession] + recent_completions: list[SidebarRecentSession] diff --git a/backend/tests/test_sidebar_stats.py b/backend/tests/test_sidebar_stats.py new file mode 100644 index 00000000..dc5ec100 --- /dev/null +++ b/backend/tests/test_sidebar_stats.py @@ -0,0 +1,142 @@ +"""Integration tests for sidebar stats endpoint.""" + +import pytest +from httpx import AsyncClient + + +class TestSidebarStats: + """Tests for GET /sessions/sidebar-stats.""" + + @pytest.mark.asyncio + async def test_sidebar_stats_no_sessions( + self, client: AsyncClient, auth_headers: dict + ): + """Empty stats when user has no sessions.""" + response = await client.get( + "/api/v1/sessions/sidebar-stats?tz_offset=0", + headers=auth_headers, + ) + assert response.status_code == 200 + data = response.json() + assert data["resolved_today"] == 0 + assert data["active_count"] == 0 + assert data["total_session_minutes_today"] == 0 + assert data["active_sessions"] == [] + assert data["recent_completions"] == [] + assert "tree_counts" in data + + @pytest.mark.asyncio + async def test_sidebar_stats_with_active_session( + self, client: AsyncClient, auth_headers: dict, test_tree: dict + ): + """Active session appears in activity feed.""" + create_resp = await client.post( + "/api/v1/sessions", + json={"tree_id": test_tree["id"], "ticket_number": "TK-100"}, + headers=auth_headers, + ) + assert create_resp.status_code == 201 + + response = await client.get( + "/api/v1/sessions/sidebar-stats?tz_offset=0", + headers=auth_headers, + ) + assert response.status_code == 200 + data = response.json() + assert data["active_count"] == 1 + assert len(data["active_sessions"]) == 1 + assert data["active_sessions"][0]["ticket_number"] == "TK-100" + assert data["active_sessions"][0]["tree_id"] == test_tree["id"] + + @pytest.mark.asyncio + async def test_sidebar_stats_resolved_today( + self, client: AsyncClient, auth_headers: dict, test_tree: dict + ): + """Resolved session counts in resolved_today.""" + create_resp = await client.post( + "/api/v1/sessions", + json={"tree_id": test_tree["id"]}, + headers=auth_headers, + ) + session_id = create_resp.json()["id"] + + # Complete with resolved outcome + await client.post( + f"/api/v1/sessions/{session_id}/complete", + json={"outcome": "resolved", "outcome_notes": "Fixed it"}, + headers=auth_headers, + ) + + response = await client.get( + "/api/v1/sessions/sidebar-stats?tz_offset=0", + headers=auth_headers, + ) + assert response.status_code == 200 + data = response.json() + assert data["resolved_today"] >= 1 + assert data["active_count"] == 0 + assert len(data["recent_completions"]) >= 1 + + @pytest.mark.asyncio + async def test_sidebar_stats_max_active_sessions( + self, client: AsyncClient, auth_headers: dict, test_tree: dict + ): + """Active sessions capped at 5.""" + for i in range(7): + await client.post( + "/api/v1/sessions", + json={"tree_id": test_tree["id"], "ticket_number": f"TK-{i}"}, + headers=auth_headers, + ) + + response = await client.get( + "/api/v1/sessions/sidebar-stats?tz_offset=0", + headers=auth_headers, + ) + assert response.status_code == 200 + data = response.json() + assert data["active_count"] == 7 + assert len(data["active_sessions"]) == 5 + + @pytest.mark.asyncio + async def test_sidebar_stats_recent_completions_max_3( + self, client: AsyncClient, auth_headers: dict, test_tree: dict + ): + """Recent completions capped at 3.""" + for i in range(5): + create_resp = await client.post( + "/api/v1/sessions", + json={"tree_id": test_tree["id"]}, + headers=auth_headers, + ) + session_id = create_resp.json()["id"] + await client.post( + f"/api/v1/sessions/{session_id}/complete", + json={"outcome": "resolved", "outcome_notes": "Done"}, + headers=auth_headers, + ) + + response = await client.get( + "/api/v1/sessions/sidebar-stats?tz_offset=0", + headers=auth_headers, + ) + assert response.status_code == 200 + data = response.json() + assert len(data["recent_completions"]) == 3 + + @pytest.mark.asyncio + async def test_sidebar_stats_requires_auth(self, client: AsyncClient): + """Endpoint requires authentication.""" + response = await client.get("/api/v1/sessions/sidebar-stats?tz_offset=0") + assert response.status_code == 401 + + @pytest.mark.asyncio + async def test_sidebar_stats_requires_tz_offset( + self, client: AsyncClient, auth_headers: dict + ): + """tz_offset query param is required.""" + response = await client.get( + "/api/v1/sessions/sidebar-stats", + headers=auth_headers, + ) + assert response.status_code == 422 diff --git a/docs/plans/Frontend/sidebar-grouping-concepts.html b/docs/plans/Frontend/sidebar-grouping-concepts.html new file mode 100644 index 00000000..f3026901 --- /dev/null +++ b/docs/plans/Frontend/sidebar-grouping-concepts.html @@ -0,0 +1,630 @@ + + + + + +ResolutionFlow — Sidebar Grouping Concepts + + + + + +

Sidebar Grouping Concepts

+

+ Reorganizing the navigation around workflow stages — what engineers do when they're working a ticket vs + building flows vs reviewing results. Also explores splitting AI Assistant into two + distinct tools with clearer purpose. +

+ + +
+

Concept A — Resolve / Build / Insights

+

Three clear workflow stages. "Resolve" is front and center because that's what engineers spend most time doing. AI is split: copilot lives in Resolve, builder lives in Build.

+
+ +
+ + +
+
+
Concept A1
+
With Section Labels
+
Explicit uppercase labels separate each group. Clear visual hierarchy.
+
+ +
+ + +
+
+
Concept A2
+
With Dividers Only
+
Same grouping but uses subtle divider lines instead of text labels. Cleaner, less visual noise. Groups are implied by proximity.
+
+ +
+ + +
+
+
Concept A3
+
Dashboard First
+
Dashboard stays at the top as the "home" landing, then workflow groups follow. Some engineers want the overview first before diving into work.
+
+ +
+ +
+ + +
+

Concept B — Work / Build (Simpler Split)

+

Only two groups instead of three. Dashboard top, then "Work" (active troubleshooting) and "Build" (authoring + review). Exports and Analytics move into Build since they're about improving process, not resolving tickets.

+
+ +
+ +
+
+
Concept B1
+
Two Groups + Dashboard
+
Fewer sections = less cognitive load. "Work" is what you do when tickets come in. "Build" is everything else.
+
+ +
+ +
+ + +
+

AI Assistant Split — Naming Options

+
+ +
+

Troubleshooting AI (Copilot)

+

Used during active sessions. Suggests next steps, explains errors, provides context from ticket data and knowledge base.

+ +
+
+ AI Copilot + Current robot icon — generic but clear purpose +
+
+
+ Resolve AI + Sparkle icon — ties to the "Resolve" section name +
+
+ +
+

Flow Builder AI (Authoring)

+

Used when building new flows from scratch. Conversational interface to generate decision trees, procedural steps, and intake forms.

+ +
+
+ AI Builder + Robot icon — explicit "builder" label +
+
+
+ Flow Forge + Wrench icon — "forge" = crafting/building +
+
+ +
+
+ + + + + diff --git a/docs/plans/Frontend/sidebar-icon-concepts.html b/docs/plans/Frontend/sidebar-icon-concepts.html new file mode 100644 index 00000000..46559529 --- /dev/null +++ b/docs/plans/Frontend/sidebar-icon-concepts.html @@ -0,0 +1,734 @@ + + + + + +ResolutionFlow — Sidebar Icon Concepts (Refined) + + + + + +

Sidebar Icons — Style × Shape Matrix

+

+ Concept 1 (Semantic Colors) and Concept 2 (Tinted Pills) shown with 3 different icon sets each. + Current = existing Lucide icons, Set A = more descriptive/metaphorical icons, + Set B = minimal/geometric alternatives. Click any nav item to toggle active state. +

+ + +
+

Concept 1 — Semantic Colored Icons

+

Always-on color per icon. No pill background — just the stroke color creates landmarks.

+
+ +
+ + +
+
+
Concept 1 × Current Icons
+
Original Lucide Set
+
LayoutGrid, Box, PenLine, Clock, FileText, BotMessageSquare, Bookmark, Terminal, Sparkles, BarChart3
+
+ +
+ + +
+
+
Concept 1 × Icon Set A
+
Descriptive / Metaphorical
+
Gauge, GitFork, Wrench, Zap, Share2, Brain, Layers, Code2, Rocket, TrendingUp
+
+ +
+ + +
+
+
Concept 1 × Icon Set B
+
Minimal / Geometric
+
Compass, Network, PenTool, Radio, FileOutput, Wand2, Library, ScrollText, Lightbulb, PieChart
+
+ +
+ +
+ + +
+

Concept 2 — Tinted Pill Backgrounds

+

Colored icon inside a soft tinted rounded-square. Creates visual weight — feels like an app dock.

+
+ +
+ + +
+
+
Concept 2 × Current Icons
+
Original Lucide Set
+
LayoutGrid, Box, PenLine, Clock, FileText, BotMessageSquare, Bookmark, Terminal, Sparkles, BarChart3
+
+ +
+ + +
+
+
Concept 2 × Icon Set A
+
Descriptive / Metaphorical
+
Gauge, GitFork, Wrench, Zap, Share2, Brain, Layers, Code2, Rocket, TrendingUp
+
+ +
+ + +
+
+
Concept 2 × Icon Set B
+
Minimal / Geometric
+
Compass, Network, PenTool, Radio, FileOutput, Wand2, Library, ScrollText, Lightbulb, PieChart
+
+ +
+ +
+ + +
+

Icon Comparison Reference

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Nav ItemCurrentSet A (Descriptive)Set B (Minimal)Color
DashboardLayoutGrid — 4 squaresGauge — speedometer, "command center"Compass — navigation hubCyan
All FlowsBox — generic 3D cubeGitFork — branching paths (literally what flows are)Network — connected nodesViolet
Flow EditorPenLine — pen tipWrench — builder toolPenTool — vector/design penAmber
SessionsClock — time-basedZap — active energy, live sessionsRadio — broadcasting/live signalEmerald
ExportsFileText — generic docShare2 — share network nodesFileOutput — file with arrow outBlue
AI AssistantBotMessageSquare — robot chatBrain — intelligence, organic feelWand2 — magic wand with sparklesFuchsia
Step LibraryBookmark — generic ribbonLayers — stacked layers = reusable stepsLibrary — book spinesOrange
Script LibraryTerminal — prompt cursorCode2 — angle brackets with slashScrollText — script/scroll with textTeal
KB AcceleratorSparkles — magic starRocket — acceleration/speedLightbulb — ideas/insightRose
AnalyticsBarChart3 — vertical barsTrendingUp — growth line with arrowPieChart — pie segmentsSky
+
+ + + + + diff --git a/docs/plans/Frontend/sidebar-redesign-context.md b/docs/plans/Frontend/sidebar-redesign-context.md new file mode 100644 index 00000000..8346f8d6 --- /dev/null +++ b/docs/plans/Frontend/sidebar-redesign-context.md @@ -0,0 +1,83 @@ +# Sidebar Redesign — Context & Decisions + +> Branch: `design/sidebar-icon-concepts` +> Date: 2026-03-15 + +## What's Already Implemented + +### Icon Changes (committed) +Swapped generic icons for more descriptive ones: +- Dashboard: `LayoutGrid` (kept) +- All Flows: `Box` → `Network` +- Flow Editor: `PenLine` → `Wrench` +- Sessions: `Clock` (kept) +- Exports: `FileText` → `FileOutput` +- AI Assistant: `BotMessageSquare` (kept) +- Step Library: `Bookmark` → `Library` +- Script Library: `Terminal` → `Code2` +- KB Accelerator: `Sparkles` → `Lightbulb` +- Analytics: `BarChart3` (kept) + +### Semantic Icon Colors (committed) +Each nav icon now has a permanent color (Concept 1 style): +| Item | Color | Hex | +|------|-------|-----| +| Dashboard | Cyan | `#22d3ee` | +| All Flows | Violet | `#a78bfa` | +| Flow Editor | Amber | `#f59e0b` | +| Sessions | Emerald | `#34d399` | +| Exports | Blue | `#60a5fa` | +| AI Assistant | Fuchsia | `#e879f9` | +| Step Library | Orange | `#fb923c` | +| Script Library | Teal | `#2dd4bf` | +| KB Accelerator | Rose | `#fb7185` | +| Analytics | Sky | `#38bdf8` | +| User Guides | Lime | `#a3e635` | +| Feedback | Indigo | `#818cf8` | +| Account/Collapse | No color (stays muted) | + +Colors defined in `NAV_COLORS` constant in `Sidebar.tsx`, applied via `iconColor` prop on `NavItem`. + +### Session History Tab Reorder (committed) +- Default tab: `Active` (was `All`) +- Tab order: Active → Prepared → Completed → All + +## Still Deciding + +### Nav Grouping +Mockups in `sidebar-grouping-concepts.html`. Four options explored: + +**Concept A1 — Three labeled groups:** +- Resolve: Sessions, All Flows, FlowPilot, Script Library +- Build: Flow Editor, Flow Assist, Step Library, KB Accelerator +- Insights: Dashboard, Exports, Analytics + +**Concept A2 — Same groups, divider lines only (no labels)** + +**Concept A3 — Dashboard first, then Resolve / Build / Insights** + +**Concept B1 — Two groups only (Work / Build) with Dashboard on top** + +### AI Assistant Split +Currently one "AI Assistant" nav item does two jobs. Considering splitting into: + +**In-session copilot (Resolve group):** +- Recommended name: **FlowPilot** (already used in copilot panel) +- Icon: Brain (fuchsia) +- Purpose: helps during active troubleshooting sessions + +**Flow builder (Build group):** +- Recommended name: **Flow Assist** (already used in embedded editor AI) +- Icon: Wand2 (pink) +- Purpose: conversational flow creation/authoring + +Other naming options explored: AI Copilot, Resolve AI, AI Builder, Flow Forge + +## Files Changed +- `frontend/src/components/layout/Sidebar.tsx` — new icons, colors, NAV_COLORS constant +- `frontend/src/components/layout/NavItem.tsx` — added `iconColor` prop +- `frontend/src/pages/SessionHistoryPage.tsx` — default tab + tab order + +## Mockup Files (open in browser) +- `docs/plans/Frontend/sidebar-icon-concepts.html` — icon + color comparison (6 panels) +- `docs/plans/Frontend/sidebar-grouping-concepts.html` — grouping + AI naming options diff --git a/docs/superpowers/plans/2026-03-15-sidebar-redesign.md b/docs/superpowers/plans/2026-03-15-sidebar-redesign.md new file mode 100644 index 00000000..97c7162d --- /dev/null +++ b/docs/superpowers/plans/2026-03-15-sidebar-redesign.md @@ -0,0 +1,1269 @@ +# Sidebar Redesign Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the sidebar's pinned flows section with a daily stats bar + activity feed, reorganize nav into Resolve/Build/Insights groups, and split AI Assistant into FlowPilot + Flow Assist. + +**Architecture:** New lightweight backend endpoint (`/sessions/sidebar-stats`) returns daily stats and active/recent session data in a single call. Frontend restructures `Sidebar.tsx` to render stats bar, activity feed, and grouped nav. Pinned flows system removed from frontend only (backend tables/endpoints left for future cleanup). + +**Tech Stack:** Python FastAPI + SQLAlchemy async (backend), React + TypeScript + Zustand + Tailwind (frontend), Lucide React icons. + +**Spec:** `docs/superpowers/specs/2026-03-15-sidebar-redesign-design.md` + +--- + +## File Map + +### Backend (new) +- `backend/app/schemas/sidebar.py` — Pydantic schemas for sidebar stats response +- `backend/app/api/endpoints/sidebar.py` — `GET /sessions/sidebar-stats` endpoint + +### Backend (modify) +- `backend/app/api/router.py` — register new sidebar router + +### Frontend (new) +- `frontend/src/components/sidebar/SidebarStatsBar.tsx` — three-stat row component +- `frontend/src/components/sidebar/SidebarActivityFeed.tsx` — activity feed (active + recents) +- `frontend/src/components/sidebar/ActivityItem.tsx` — single activity row +- `frontend/src/api/sidebar.ts` — API client for sidebar stats +- `frontend/src/pages/FlowAssistPage.tsx` — standalone flow builder AI page + +### Frontend (modify) +- `frontend/src/components/layout/Sidebar.tsx` — major restructure +- `frontend/src/router.tsx` — add `/flow-assist` route +- `frontend/src/api/index.ts` — export new sidebar API +- `frontend/src/pages/TreeLibraryPage.tsx` — remove pin integration +- `frontend/src/components/library/TreeGridView.tsx` — remove pin button +- `frontend/src/components/library/TreeListView.tsx` — remove pin button +- `frontend/src/components/library/TreeTableView.tsx` — remove pin button + +### Frontend (delete) +- `frontend/src/components/sidebar/PinnedFlowsSection.tsx` +- `frontend/src/store/pinnedFlowsStore.ts` +- `frontend/src/api/pinnedFlows.ts` + +### Tests (new) +- `backend/tests/test_sidebar_stats.py` — integration tests for sidebar endpoint + +--- + +## Chunk 1: Backend — Sidebar Stats Endpoint + +### Task 1: Sidebar Stats Schema + +**Files:** +- Create: `backend/app/schemas/sidebar.py` + +- [ ] **Step 1: Create the Pydantic schemas** + +```python +"""Schemas for sidebar stats endpoint.""" + +from datetime import datetime +from typing import Optional +from uuid import UUID + +from pydantic import BaseModel + + +class SidebarActiveSession(BaseModel): + """An active or paused session for the activity feed.""" + session_id: UUID + tree_name: str + tree_id: UUID + tree_type: str + started_at: datetime + ticket_number: Optional[str] = None + psa_ticket_id: Optional[str] = None + + +class SidebarRecentSession(BaseModel): + """A recently completed session for the activity feed.""" + session_id: UUID + tree_name: str + tree_id: UUID + tree_type: str + completed_at: datetime + + +class SidebarTreeCounts(BaseModel): + """Tree counts for All Flows sub-items.""" + total: int + troubleshooting: int + procedural: int + maintenance: int + + +class SidebarStatsResponse(BaseModel): + """Response for GET /sessions/sidebar-stats.""" + resolved_today: int + active_count: int + total_session_minutes_today: int + tree_counts: SidebarTreeCounts + active_sessions: list[SidebarActiveSession] + recent_completions: list[SidebarRecentSession] +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend/app/schemas/sidebar.py +git commit -m "feat: add sidebar stats Pydantic schemas" +``` + +--- + +### Task 2: Sidebar Stats Endpoint — Tests First + +**Files:** +- Create: `backend/tests/test_sidebar_stats.py` + +- [ ] **Step 3: Write failing tests** + +```python +"""Integration tests for sidebar stats endpoint.""" + +import pytest +from httpx import AsyncClient + + +class TestSidebarStats: + """Tests for GET /sessions/sidebar-stats.""" + + @pytest.mark.asyncio + async def test_sidebar_stats_no_sessions( + self, client: AsyncClient, auth_headers: dict + ): + """Empty stats when user has no sessions.""" + response = await client.get( + "/api/v1/sessions/sidebar-stats?tz_offset=0", + headers=auth_headers, + ) + assert response.status_code == 200 + data = response.json() + assert data["resolved_today"] == 0 + assert data["active_count"] == 0 + assert data["total_session_minutes_today"] == 0 + assert data["active_sessions"] == [] + assert data["recent_completions"] == [] + + @pytest.mark.asyncio + async def test_sidebar_stats_with_active_session( + self, client: AsyncClient, auth_headers: dict, test_tree: dict + ): + """Active session appears in activity feed.""" + # Create a session (creates as active with started_at set) + create_resp = await client.post( + "/api/v1/sessions", + json={"tree_id": test_tree["id"], "ticket_number": "TK-100"}, + headers=auth_headers, + ) + assert create_resp.status_code == 201 + + response = await client.get( + "/api/v1/sessions/sidebar-stats?tz_offset=0", + headers=auth_headers, + ) + assert response.status_code == 200 + data = response.json() + assert data["active_count"] == 1 + assert len(data["active_sessions"]) == 1 + assert data["active_sessions"][0]["ticket_number"] == "TK-100" + assert data["active_sessions"][0]["tree_id"] == test_tree["id"] + + @pytest.mark.asyncio + async def test_sidebar_stats_resolved_today( + self, client: AsyncClient, auth_headers: dict, test_tree: dict + ): + """Resolved session counts in resolved_today.""" + # Create and complete a session + create_resp = await client.post( + "/api/v1/sessions", + json={"tree_id": test_tree["id"]}, + headers=auth_headers, + ) + session_id = create_resp.json()["id"] + + # Complete with resolved outcome + await client.post( + f"/api/v1/sessions/{session_id}/complete", + json={"outcome": "resolved", "outcome_notes": "Fixed it"}, + headers=auth_headers, + ) + + response = await client.get( + "/api/v1/sessions/sidebar-stats?tz_offset=0", + headers=auth_headers, + ) + assert response.status_code == 200 + data = response.json() + assert data["resolved_today"] >= 1 + assert data["active_count"] == 0 + assert len(data["recent_completions"]) >= 1 + + @pytest.mark.asyncio + async def test_sidebar_stats_max_active_sessions( + self, client: AsyncClient, auth_headers: dict, test_tree: dict + ): + """Active sessions capped at 5.""" + # Create 7 active sessions + for i in range(7): + await client.post( + "/api/v1/sessions", + json={"tree_id": test_tree["id"], "ticket_number": f"TK-{i}"}, + headers=auth_headers, + ) + + response = await client.get( + "/api/v1/sessions/sidebar-stats?tz_offset=0", + headers=auth_headers, + ) + assert response.status_code == 200 + data = response.json() + assert data["active_count"] == 7 # Total count is accurate + assert len(data["active_sessions"]) == 5 # But list capped at 5 + + @pytest.mark.asyncio + async def test_sidebar_stats_recent_completions_max_3( + self, client: AsyncClient, auth_headers: dict, test_tree: dict + ): + """Recent completions capped at 3.""" + # Create and complete 5 sessions + for i in range(5): + create_resp = await client.post( + "/api/v1/sessions", + json={"tree_id": test_tree["id"]}, + headers=auth_headers, + ) + session_id = create_resp.json()["id"] + await client.post( + f"/api/v1/sessions/{session_id}/complete", + json={"outcome": "resolved", "outcome_notes": "Done"}, + headers=auth_headers, + ) + + response = await client.get( + "/api/v1/sessions/sidebar-stats?tz_offset=0", + headers=auth_headers, + ) + assert response.status_code == 200 + data = response.json() + assert len(data["recent_completions"]) == 3 + + @pytest.mark.asyncio + async def test_sidebar_stats_requires_auth(self, client: AsyncClient): + """Endpoint requires authentication.""" + response = await client.get("/api/v1/sessions/sidebar-stats?tz_offset=0") + assert response.status_code == 401 + + @pytest.mark.asyncio + async def test_sidebar_stats_requires_tz_offset( + self, client: AsyncClient, auth_headers: dict + ): + """tz_offset query param is required.""" + response = await client.get( + "/api/v1/sessions/sidebar-stats", + headers=auth_headers, + ) + assert response.status_code == 422 +``` + +- [ ] **Step 4: Run tests to verify they fail** + +```bash +cd backend && python -m pytest tests/test_sidebar_stats.py -v --override-ini="addopts=" +``` + +Expected: All tests FAIL (404 — endpoint doesn't exist yet). + +- [ ] **Step 5: Commit failing tests** + +```bash +git add backend/tests/test_sidebar_stats.py +git commit -m "test: add failing tests for sidebar stats endpoint" +``` + +--- + +### Task 3: Sidebar Stats Endpoint — Implementation + +**Files:** +- Create: `backend/app/api/endpoints/sidebar.py` +- Modify: `backend/app/api/router.py` + +- [ ] **Step 6: Implement the sidebar stats endpoint** + +Create `backend/app/api/endpoints/sidebar.py`: + +```python +"""Sidebar stats and activity feed endpoint.""" + +from datetime import datetime, timedelta, timezone + +from fastapi import APIRouter, Depends, Query +from sqlalchemy import func, select, and_, case +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.session import Session +from app.models.tree import Tree +from app.models.user import User +from app.schemas.sidebar import ( + SidebarActiveSession, + SidebarRecentSession, + SidebarStatsResponse, + SidebarTreeCounts, +) + +router = APIRouter(prefix="/sessions", tags=["sidebar"]) + + +@router.get("/sidebar-stats", response_model=SidebarStatsResponse) +async def get_sidebar_stats( + tz_offset: int = Query(..., description="Client UTC offset in minutes (e.g. -300 for EST)"), + current_user: User = Depends(get_current_active_user), + db: AsyncSession = Depends(get_db), +) -> SidebarStatsResponse: + """Get sidebar stats and activity feed for the current user. + + Returns daily stats (resolved count, active count, time in session) + and lists of active + recently completed sessions. + """ + # Compute "today" start in user's timezone, then convert to UTC + now_utc = datetime.now(timezone.utc) + user_tz = timezone(timedelta(minutes=-tz_offset)) + now_local = now_utc.astimezone(user_tz) + today_start_local = now_local.replace(hour=0, minute=0, second=0, microsecond=0) + today_start_utc = today_start_local.astimezone(timezone.utc) + + user_filter = Session.user_id == current_user.id + + # --- Resolved today --- + resolved_result = await db.execute( + select(func.count()).where( + and_( + user_filter, + Session.completed_at >= today_start_utc, + Session.outcome == "resolved", + ) + ) + ) + resolved_today = resolved_result.scalar() or 0 + + # --- Active count (all time, not just today) --- + active_result = await db.execute( + select(func.count()).where( + and_( + user_filter, + Session.started_at.isnot(None), + Session.completed_at.is_(None), + ) + ) + ) + active_count = active_result.scalar() or 0 + + # --- Total session minutes today --- + # Sum of (completed_at or now) - started_at for sessions started today + duration_expr = func.extract( + "epoch", + func.coalesce(Session.completed_at, now_utc) - Session.started_at, + ) / 60.0 + duration_result = await db.execute( + select(func.coalesce(func.sum(duration_expr), 0)).where( + and_( + user_filter, + Session.started_at.isnot(None), + Session.started_at >= today_start_utc, + ) + ) + ) + total_minutes = int(duration_result.scalar() or 0) + + # --- Active sessions (max 5, most recent first) --- + active_sessions_result = await db.execute( + select( + Session.id, + Session.tree_id, + Session.started_at, + Session.ticket_number, + Session.psa_ticket_id, + Session.tree_snapshot["name"].as_string().label("tree_name"), + Session.tree_snapshot["tree_type"].as_string().label("tree_type"), + ) + .where( + and_( + user_filter, + Session.started_at.isnot(None), + Session.completed_at.is_(None), + ) + ) + .order_by(Session.started_at.desc()) + .limit(5) + ) + active_sessions = [ + SidebarActiveSession( + session_id=row.id, + tree_name=row.tree_name or "Unknown Flow", + tree_id=row.tree_id, + tree_type=row.tree_type or "troubleshooting", + started_at=row.started_at, + ticket_number=row.ticket_number, + psa_ticket_id=row.psa_ticket_id, + ) + for row in active_sessions_result.all() + ] + + # --- Recent completions (max 3, completed today, most recent first) --- + recent_result = await db.execute( + select( + Session.id, + Session.tree_id, + Session.completed_at, + Session.tree_snapshot["name"].as_string().label("tree_name"), + Session.tree_snapshot["tree_type"].as_string().label("tree_type"), + ) + .where( + and_( + user_filter, + Session.completed_at.isnot(None), + Session.completed_at >= today_start_utc, + ) + ) + .order_by(Session.completed_at.desc()) + .limit(3) + ) + recent_completions = [ + SidebarRecentSession( + session_id=row.id, + tree_name=row.tree_name or "Unknown Flow", + tree_id=row.tree_id, + tree_type=row.tree_type or "troubleshooting", + completed_at=row.completed_at, + ) + for row in recent_result.all() + ] + + # --- Tree counts (for All Flows sub-items) --- + tree_counts_result = await db.execute( + select( + func.count().label("total"), + func.count().filter(Tree.tree_type == "troubleshooting").label("troubleshooting"), + func.count().filter(Tree.tree_type == "procedural").label("procedural"), + func.count().filter(Tree.tree_type == "maintenance").label("maintenance"), + ).where( + and_( + Tree.account_id == current_user.account_id, + Tree.is_active.is_(True), + Tree.deleted_at.is_(None), + ) + ) + ) + tc = tree_counts_result.one() + + return SidebarStatsResponse( + resolved_today=resolved_today, + active_count=active_count, + total_session_minutes_today=total_minutes, + tree_counts=SidebarTreeCounts( + total=tc.total, + troubleshooting=tc.troubleshooting, + procedural=tc.procedural, + maintenance=tc.maintenance, + ), + active_sessions=active_sessions, + recent_completions=recent_completions, + ) +``` + +- [ ] **Step 7: Register the router in `router.py`** + +In `backend/app/api/router.py`, add the import and registration. The sidebar router must be registered BEFORE the sessions router since both use `/sessions` prefix — FastAPI matches routes in registration order, and `/sessions/sidebar-stats` needs to match before `/sessions/{session_id}`. + +```python +from app.api.endpoints import sidebar +# ... existing imports ... + +# Add BEFORE sessions router registration: +api_router.include_router(sidebar.router) +``` + +- [ ] **Step 8: Run tests to verify they pass** + +```bash +cd backend && python -m pytest tests/test_sidebar_stats.py -v --override-ini="addopts=" +``` + +Expected: All 7 tests PASS. + +- [ ] **Step 9: Commit** + +```bash +git add backend/app/api/endpoints/sidebar.py backend/app/api/router.py backend/app/schemas/sidebar.py +git commit -m "feat: add sidebar stats endpoint with daily stats and activity feed" +``` + +--- + +## Chunk 2: Frontend — Sidebar API Client & Activity Components + +### Task 4: Sidebar API Client + +**Files:** +- Create: `frontend/src/api/sidebar.ts` +- Modify: `frontend/src/api/index.ts` + +- [ ] **Step 10: Create the sidebar API client** + +```typescript +import { apiClient } from './client' + +export interface SidebarActiveSession { + session_id: string + tree_name: string + tree_id: string + tree_type: 'troubleshooting' | 'procedural' | 'maintenance' + started_at: string + ticket_number: string | null + psa_ticket_id: string | null +} + +export interface SidebarRecentSession { + session_id: string + tree_name: string + tree_id: string + tree_type: 'troubleshooting' | 'procedural' | 'maintenance' + completed_at: string +} + +export interface SidebarTreeCounts { + total: number + troubleshooting: number + procedural: number + maintenance: number +} + +export interface SidebarStatsResponse { + resolved_today: number + active_count: number + total_session_minutes_today: number + tree_counts: SidebarTreeCounts + active_sessions: SidebarActiveSession[] + recent_completions: SidebarRecentSession[] +} + +export const sidebarApi = { + getStats: async (): Promise => { + const tzOffset = new Date().getTimezoneOffset() + const response = await apiClient.get( + `/sessions/sidebar-stats?tz_offset=${tzOffset}` + ) + return response.data + }, +} +``` + +- [ ] **Step 11: Export from api/index.ts** + +Add to `frontend/src/api/index.ts`: + +```typescript +export { sidebarApi } from './sidebar' +``` + +- [ ] **Step 12: Commit** + +```bash +git add frontend/src/api/sidebar.ts frontend/src/api/index.ts +git commit -m "feat: add sidebar stats API client" +``` + +--- + +### Task 5: ActivityItem Component + +**Files:** +- Create: `frontend/src/components/sidebar/ActivityItem.tsx` + +- [ ] **Step 13: Create the ActivityItem component** + +```typescript +import { useNavigate } from 'react-router-dom' +import { getTreeNavigatePath } from '@/lib/routing' +import { cn } from '@/lib/utils' + +interface ActivityItemProps { + sessionId: string + treeName: string + treeId: string + treeType: 'troubleshooting' | 'procedural' | 'maintenance' + status: 'active' | 'paused' | 'recent' + ticketNumber?: string | null + timestamp?: string | null +} + +function formatRelativeTime(dateString: string): string { + const now = Date.now() + const then = new Date(dateString).getTime() + const diffMinutes = Math.floor((now - then) / 60000) + + if (diffMinutes < 1) return 'just now' + if (diffMinutes < 60) return `${diffMinutes}m ago` + const diffHours = Math.floor(diffMinutes / 60) + if (diffHours < 24) return `${diffHours}h ago` + return 'yesterday' +} + +export function ActivityItem({ + sessionId, + treeName, + treeId, + treeType, + status, + ticketNumber, + timestamp, +}: ActivityItemProps) { + const navigate = useNavigate() + + const handleClick = () => { + navigate(getTreeNavigatePath(treeId, treeType), { + state: { sessionId }, + }) + } + + const isRecent = status === 'recent' + + return ( + + ) +} +``` + +- [ ] **Step 14: Commit** + +```bash +git add frontend/src/components/sidebar/ActivityItem.tsx +git commit -m "feat: add ActivityItem component for sidebar activity feed" +``` + +--- + +### Task 6: SidebarStatsBar Component + +**Files:** +- Create: `frontend/src/components/sidebar/SidebarStatsBar.tsx` + +- [ ] **Step 15: Create the stats bar component** + +```typescript +interface SidebarStatsBarProps { + resolved: number + active: number + sessionMinutes: number +} + +function formatDuration(minutes: number): string { + if (minutes < 60) return `${minutes}m` + const h = Math.floor(minutes / 60) + const m = minutes % 60 + return m > 0 ? `${h}h ${m}m` : `${h}h` +} + +export function SidebarStatsBar({ resolved, active, sessionMinutes }: SidebarStatsBarProps) { + return ( +
+
+
+ {resolved} +
+
+ Resolved +
+
+
+
+ {active} +
+
+ Active +
+
+
+
+ {formatDuration(sessionMinutes)} +
+
+ In Session +
+
+
+ ) +} +``` + +- [ ] **Step 16: Commit** + +```bash +git add frontend/src/components/sidebar/SidebarStatsBar.tsx +git commit -m "feat: add SidebarStatsBar component" +``` + +--- + +### Task 7: SidebarActivityFeed Component + +**Files:** +- Create: `frontend/src/components/sidebar/SidebarActivityFeed.tsx` + +- [ ] **Step 17: Create the activity feed component** + +```typescript +import { Clock } from 'lucide-react' +import { useNavigate } from 'react-router-dom' +import { ActivityItem } from './ActivityItem' +import type { SidebarActiveSession, SidebarRecentSession } from '@/api/sidebar' + +interface SidebarActivityFeedProps { + activeSessions: SidebarActiveSession[] + recentCompletions: SidebarRecentSession[] + totalActive: number +} + +export function SidebarActivityFeed({ + activeSessions, + recentCompletions, + totalActive, +}: SidebarActivityFeedProps) { + const navigate = useNavigate() + const hasActivity = activeSessions.length > 0 || recentCompletions.length > 0 + + return ( +
+ {/* Header */} +
+ + + Activity + +
+ + {!hasActivity ? ( +

+ No activity today +

+ ) : ( +
+ {/* Active sessions */} + {activeSessions.map((session) => ( + + ))} + + {/* Overflow link */} + {totalActive > 5 && ( + + )} + + {/* Divider between active and recent */} + {activeSessions.length > 0 && recentCompletions.length > 0 && ( +
+ )} + + {/* Recent completions */} + {recentCompletions.map((session) => ( + + ))} +
+ )} +
+ ) +} +``` + +- [ ] **Step 18: Commit** + +```bash +git add frontend/src/components/sidebar/SidebarActivityFeed.tsx +git commit -m "feat: add SidebarActivityFeed component" +``` + +--- + +## Chunk 3: Frontend — Sidebar Restructure + +### Task 8: Add Pulse Dot CSS Animation + +**Files:** +- Modify: `frontend/src/index.css` + +- [ ] **Step 19: Add the pulse-dot keyframe animation** + +Add to `frontend/src/index.css` (in the global styles section, after existing keyframes): + +```css +@keyframes pulse-dot { + 0%, 100% { box-shadow: 0 0 4px rgba(52,211,153,0.4); } + 50% { box-shadow: 0 0 8px rgba(52,211,153,0.7); } +} +``` + +- [ ] **Step 20: Commit** + +```bash +git add frontend/src/index.css +git commit -m "feat: add pulse-dot animation for active session indicator" +``` + +--- + +### Task 9: Restructure Sidebar.tsx + +**Files:** +- Modify: `frontend/src/components/layout/Sidebar.tsx` + +This is the largest change. The sidebar is rewritten to use the new layout. + +- [ ] **Step 21: Rewrite Sidebar.tsx** + +Replace the entire file content. Key changes: +1. Remove `PinnedFlowsSection` import and `usePinnedFlowsStore` usage +2. Remove `sessionsApi` and `treesApi` data fetches (replaced by `sidebarApi.getStats`) +3. Add `SidebarStatsBar` and `SidebarActivityFeed` in expanded mode +4. Add `Brain` and `WandSparkles` icon imports +5. Reorganize nav items into Dashboard → Resolve → Build → Insights groups +6. Update collapsed view with all 13 items +7. Add `flowPilot` and `flowAssist` to `NAV_COLORS` + +The new `Sidebar.tsx` should: + +**Imports:** +```typescript +import { useEffect, useState } from 'react' +import { useLocation } from 'react-router-dom' +import { + LayoutGrid, Network, Wrench, Clock, FileOutput, BarChart3, + Settings, PanelLeftClose, PanelLeftOpen, MessageSquareText, + BookOpen, Lightbulb, Code2, Library, Brain, WandSparkles, +} from 'lucide-react' +import { cn } from '@/lib/utils' +import { useUserPreferencesStore } from '@/store/userPreferencesStore' +import { sidebarApi } from '@/api' +import type { SidebarStatsResponse } from '@/api/sidebar' +import { SidebarStatsBar } from '@/components/sidebar/SidebarStatsBar' +import { SidebarActivityFeed } from '@/components/sidebar/SidebarActivityFeed' +import { NavItem } from './NavItem' +``` + +**NAV_COLORS** — add two new entries, remove `ai`: +```typescript +const NAV_COLORS = { + dashboard: '#22d3ee', + flows: '#a78bfa', + editor: '#f59e0b', + sessions: '#34d399', + exports: '#60a5fa', + flowPilot: '#e879f9', + flowAssist:'#f472b6', + stepLib: '#fb923c', + scripts: '#2dd4bf', + kb: '#fb7185', + analytics: '#38bdf8', + guides: '#a3e635', + feedback: '#818cf8', +} as const +``` + +**Data fetch** — replace the two separate fetches with one `sidebarApi.getStats()` call. Use `location.pathname` as a dependency so stats refresh on navigation (the sidebar persists across pages and does NOT re-mount): +```typescript +const location = useLocation() +const [stats, setStats] = useState(null) + +useEffect(() => { + sidebarApi.getStats().then(setStats).catch(() => {}) +}, [location.pathname]) +``` + +**Expanded layout** (inside the `else` branch): +```tsx +{/* Stats Bar */} + + +{/* Activity Feed */} + + +
+ +{/* Dashboard (standalone) */} +
+ + + {/* Resolve group */} +
+ Resolve +
+ + + + + + {/* Build group */} +
+ Build +
+ + + + + + {/* Insights group */} +
+ Insights +
+ + +
+``` + +**Collapsed layout** — update to include all 13 items (add Brain/FlowPilot and WandSparkles/Flow Assist, remove BotMessageSquare/AI Assistant): +```tsx +
+ + + + + + + + + + + + + +
+``` + +- [ ] **Step 22: Verify the app builds** + +```bash +cd frontend && npm run build +``` + +Expected: Build succeeds. Fix any TypeScript or import errors. + +- [ ] **Step 23: Commit** + +```bash +git add frontend/src/components/layout/Sidebar.tsx +git commit -m "feat: restructure sidebar with stats bar, activity feed, and grouped nav" +``` + +--- + +### Task 10: Remove Pinned Flows Frontend Code + +**Files:** +- Delete: `frontend/src/components/sidebar/PinnedFlowsSection.tsx` +- Delete: `frontend/src/store/pinnedFlowsStore.ts` +- Delete: `frontend/src/api/pinnedFlows.ts` +- Modify: `frontend/src/pages/TreeLibraryPage.tsx` — remove pin integration +- Modify: `frontend/src/components/library/TreeGridView.tsx` — remove pin button +- Modify: `frontend/src/components/library/TreeListView.tsx` — remove pin button + +- [ ] **Step 24: Delete pinned flows files** + +```bash +rm frontend/src/components/sidebar/PinnedFlowsSection.tsx +rm frontend/src/store/pinnedFlowsStore.ts +rm frontend/src/api/pinnedFlows.ts +``` + +- [ ] **Step 25: Remove pin imports and props from TreeLibraryPage.tsx** + +Remove: +- `usePinnedFlowsStore` import and all related hooks (`pinnedItems`, `isMutatingByTreeId`, `pinnedTreeIds`, `pinLoadingTreeIds`, `togglePin`, `loadPinned`) +- The `useEffect` that calls `loadPinned()` +- The `pinnedTreeIds`, `onTogglePin`, `pinLoadingTreeIds` props passed to `TreeGridView`, `TreeListView`, and `TreeTableView` + +- [ ] **Step 26: Remove pin button from TreeGridView.tsx** + +Remove: +- `Star` icon import from lucide-react +- `pinnedTreeIds`, `onTogglePin`, `pinLoadingTreeIds` from component props interface +- The star/pin button JSX (lines ~67-86) + +- [ ] **Step 27: Remove pin button from TreeListView.tsx** + +Same pattern as TreeGridView — remove pin-related props and button. + +- [ ] **Step 27b: Remove pin button from TreeTableView.tsx** + +Same pattern as TreeGridView — remove pin-related props and button. + +- [ ] **Step 28: Remove pinnedFlows export from api/index.ts** + +Remove the `export { pinnedFlowsApi } from './pinnedFlows'` line (or similar). + +- [ ] **Step 29: Verify the app builds** + +```bash +cd frontend && npm run build +``` + +Expected: Build succeeds with no references to removed files. + +- [ ] **Step 30: Commit** + +```bash +git add frontend/src/components/sidebar/PinnedFlowsSection.tsx frontend/src/store/pinnedFlowsStore.ts frontend/src/api/pinnedFlows.ts frontend/src/pages/TreeLibraryPage.tsx frontend/src/components/library/TreeGridView.tsx frontend/src/components/library/TreeListView.tsx frontend/src/components/library/TreeTableView.tsx frontend/src/api/index.ts +git commit -m "refactor: remove pinned flows frontend (PinnedFlowsSection, store, API, pin buttons)" +``` + +Note: The deleted files will show as "deleted" in `git add` — that's expected. Use `git add -u` as a fallback if the explicit paths don't stage the deletions correctly. + +--- + +## Chunk 4: Frontend — Flow Assist Page & Route + +### Task 11: FlowAssistPage + +**Files:** +- Create: `frontend/src/pages/FlowAssistPage.tsx` +- Modify: `frontend/src/router.tsx` + +- [ ] **Step 31: Create FlowAssistPage** + +This is a standalone page for the conversational flow builder. For now, create a placeholder page that will be expanded when the AI split is fully implemented. The page should use the same glass-card layout pattern as other pages. + +```typescript +import { WandSparkles } from 'lucide-react' + +export default function FlowAssistPage() { + return ( +
+
+

+ + + Flow Assist + +

+

+ Build flows from natural language — describe what you need and Flow Assist will generate the decision tree or procedural steps. +

+
+ +
+ +

+ Coming Soon +

+

+ Flow Assist will be available here as a dedicated conversational flow builder. + In the meantime, use the AI panel in the Flow Editor to generate flows. +

+
+
+ ) +} +``` + +- [ ] **Step 32: Add the route in router.tsx** + +Add inside the protected route children (alongside other page routes like `/assistant`, `/scripts`, etc.): + +```typescript +{ + path: 'flow-assist', + lazy: () => import('./pages/FlowAssistPage').then(m => ({ Component: m.default })), +}, +``` + +If the router doesn't use lazy loading, use the direct import pattern matching the existing routes. + +- [ ] **Step 33: Verify the app builds** + +```bash +cd frontend && npm run build +``` + +- [ ] **Step 34: Commit** + +```bash +git add frontend/src/pages/FlowAssistPage.tsx frontend/src/router.tsx +git commit -m "feat: add FlowAssistPage placeholder and /flow-assist route" +``` + +--- + +## Chunk 5: Final Verification & Cleanup + +### Task 12: Full Test Suite & Build Verification + +- [ ] **Step 35: Run backend tests** + +```bash +cd backend && python -m pytest tests/test_sidebar_stats.py -v --override-ini="addopts=" +``` + +Expected: All sidebar tests pass. + +- [ ] **Step 36: Run full backend test suite** + +```bash +cd backend && python -m pytest --override-ini="addopts=" -x -q +``` + +Expected: No regressions. If any pinned-flows tests exist and fail (because we didn't touch backend), that's expected — they should still pass since backend is unchanged. + +- [ ] **Step 37: Run frontend build** + +```bash +cd frontend && npm run build +``` + +Expected: Clean build, no errors. + +- [ ] **Step 38: Manual smoke test** + +Start the dev servers and verify: +1. Sidebar loads with stats bar showing 0/0/0m on fresh state +2. Activity feed shows "No activity today" when empty +3. Nav is grouped: Dashboard → Resolve (Sessions, All Flows, FlowPilot, Script Library) → Build (Flow Editor, Flow Assist, Step Library, KB Accelerator) → Insights (Exports, Analytics) +4. FlowPilot link goes to `/assistant` +5. Flow Assist link goes to `/flow-assist` (placeholder page) +6. Collapsed sidebar shows all 13 icon-only items +7. Pin buttons are gone from tree library grid/list views +8. No console errors + +- [ ] **Step 39: Final commit with any cleanup** + +If there are any remaining changes from smoke test fixes, stage the specific files and commit: + +```bash +git add +git commit -m "chore: sidebar redesign cleanup and verification" +``` + +--- + +## Summary + +| Chunk | Tasks | What it delivers | +|-------|-------|-----------------| +| 1 | Tasks 1-3 | Backend endpoint with tests — `GET /sessions/sidebar-stats` | +| 2 | Tasks 4-7 | Frontend API client + ActivityItem, StatsBar, ActivityFeed components | +| 3 | Tasks 8-10 | Sidebar restructure + pinned flows removal | +| 4 | Task 11 | FlowAssistPage + `/flow-assist` route | +| 5 | Task 12 | Full verification and cleanup | diff --git a/docs/superpowers/specs/2026-03-15-sidebar-redesign-design.md b/docs/superpowers/specs/2026-03-15-sidebar-redesign-design.md new file mode 100644 index 00000000..a39a7cb8 --- /dev/null +++ b/docs/superpowers/specs/2026-03-15-sidebar-redesign-design.md @@ -0,0 +1,271 @@ +# Sidebar Redesign — Design Spec + +> **Date:** 2026-03-15 +> **Branch:** `design/sidebar-icon-concepts` +> **Status:** Approved for implementation + +--- + +## Overview + +Redesign the ResolutionFlow sidebar from a flat nav list with a pinned flows section into a structured, activity-aware navigation organized around engineer workflow stages. The sidebar becomes a "shift dashboard" — showing what you're working on, how your day is going, and organizing tools by what you do with them. + +## Goals + +1. **Surface active work** — engineers should see their in-progress sessions and CW ticket context without navigating away +2. **Organize by workflow** — group nav items into Resolve (working tickets) / Build (authoring flows) / Insights (reviewing results) +3. **Provide daily pulse** — lightweight stats (resolved today, active, time in session) visible at a glance +4. **Clarify AI tools** — split the single "AI Assistant" into two purpose-specific tools (FlowPilot for troubleshooting, Flow Assist for authoring) +5. **Remove low-value features** — pinned/quick access flows are too static for the dynamic MSP workflow + +## Full Sidebar Layout (top to bottom) + +``` +┌─────────────────────────┐ +│ Stats Bar │ ← 3 counters: Resolved / Active / In Session +│ [7 done] [2 open] [2h] │ +├─────────────────────────┤ +│ ● Activity │ ← "Activity" label with emerald clock icon +│ ● O365 Mail Flow #4829 │ ← green dot = active, blue ticket # +│ ● DHCP Scope #4828 │ ← amber dot = paused/idle +│ · · · · · · · · · · · │ ← subtle sub-divider +│ · DNS Resolution 2h │ ← muted dot + relative time +│ · Printer Spooler 5h │ +├─────────────────────────┤ ← glass-border divider +│ ▣ Dashboard │ ← standalone, no group label +│ │ +│ RESOLVE │ ← group label (JetBrains Mono, uppercase) +│ ◷ Sessions [3] │ +│ ⬡ All Flows [32] │ +│ ◉ FlowPilot │ +│ Script Library │ +│ │ +│ BUILD │ +│ 🔧 Flow Editor │ +│ ✦ Flow Assist │ +│ ⊞ Step Library │ +│ 💡 KB Accelerator │ +│ │ +│ INSIGHTS │ +│ 📄 Exports │ +│ 📊 Analytics │ +│ │ +│ ── flex spacer ── │ ← pushes footer to bottom +├─────────────────────────┤ ← glass-border divider +│ 📖 User Guides │ +│ 💬 Feedback │ +│ ⚙ Account │ +│ ◁ Collapse │ +└─────────────────────────┘ +``` + +No brand/logo section at top (unchanged from current). Dashboard sits below the divider with no group label — it's visually separated from the activity zone by the divider and from the "Resolve" label by standard spacing (same as current "first nav item before first group" pattern). + +## What Changes + +### Removed +- **Pinned Flows section** — the entire `PinnedFlowsSection` component, `pinnedFlowsStore`, and `pinnedFlows` API module. MSP engineers' work is too dynamic for static pinning; the activity feed replaces this. +- **Single "AI Assistant" nav item** — replaced by two distinct items + +### Added + +#### 1. Daily Stats Bar (top of sidebar) +Three compact counters in a horizontal row (`display: flex; gap: 2px`), each counter is a flex-1 cell. + +| Stat | Color | Source | +|------|-------|--------| +| Resolved | `#34d399` (emerald) | Count of sessions completed today with `outcome = 'resolved'` | +| Active | `#22d3ee` (cyan) | Count of sessions where `completed_at IS NULL` | +| In Session | `#8891a0` (muted) | Sum of time spent in active sessions today (new metric — snapshot, acceptable to be stale) | + +**Dimensions:** `padding: 8px 12px 4px` on the container. Each stat cell: `padding: 6px 4px`, `border-radius: 6px`, `background: rgba(255,255,255,0.02)`. Value: `JetBrains Mono, 14px, font-weight: 600`. Label: `JetBrains Mono, 7px, uppercase, letter-spacing: 0.1em, color: #3d4350`. + +Stats reset daily. "In Session" is a snapshot value computed server-side — it does not live-update as a timer. + +#### 2. Activity Feed (below stats bar) +Two sub-sections separated by a subtle divider (`1px solid rgba(255,255,255,0.03)`): + +**Active sessions (max 5):** +- Green pulsing dot — currently active session +- Amber dot (`#f59e0b`, design system amber) — paused/idle session (started but not interacted with recently) +- Flow name (truncated, `13px`, `color: #e2e8f0`) +- ConnectWise ticket number in blue (`#60a5fa`, `JetBrains Mono, 9px`) — only shown if the user's team has a PSA connection AND the session is linked to a ticket +- If more than 5 active sessions, show a "View all in Sessions →" link styled as `text-muted-foreground, 11px, hover:text-foreground` + +**Green pulsing dot animation:** +```css +.active-dot { + width: 7px; height: 7px; border-radius: 50%; + background: #34d399; + box-shadow: 0 0 6px rgba(52,211,153,0.5); + animation: pulse-dot 2s ease-in-out infinite; +} +@keyframes pulse-dot { + 0%, 100% { box-shadow: 0 0 4px rgba(52,211,153,0.4); } + 50% { box-shadow: 0 0 8px rgba(52,211,153,0.7); } +} +``` + +**Recent completions:** +- Small muted dot (`4px`, `#3d4350`) +- Flow name in muted text (`11.5px`, `color: #6b7280`) +- Relative timestamp (`JetBrains Mono, 9px`, `color: #5a6170`) +- Show last 3 completed sessions from today + +The activity section has an "Activity" header: `JetBrains Mono, 9px, uppercase, letter-spacing: 0.12em, color: #5a6170`, with a small emerald clock SVG icon (10px). + +**Activity feed height:** The zone scrolls with the rest of the sidebar — no independent scroll container. The overall sidebar already has `handleSidebarWheel` for scroll forwarding. With max 5 active + 3 recent, the zone is ~280px max which is acceptable. + +**Refresh:** Data fetches on mount (same as current). No polling. Navigating back to a page that renders the sidebar triggers a re-mount/re-fetch. A future enhancement could add polling or WebSocket updates but that's out of scope. + +#### 3. Nav Grouping (Concept A3) +Dashboard stands alone at the top (no group label, no special treatment — just a regular `NavItem` above the first group label). + +**Resolve** — what engineers do when working tickets: +- Sessions (Clock, emerald `#34d399`, badge: active count) +- All Flows (Network, violet `#a78bfa`, badge: total count, with type sub-items) +- FlowPilot (Brain, fuchsia `#e879f9`) — NEW: in-session AI copilot +- Script Library (Code2, teal `#2dd4bf`) + +**Build** — authoring and improving flows: +- Flow Editor (Wrench, amber `#f59e0b`) +- Flow Assist (WandSparkles, pink `#f472b6`) — NEW: conversational flow builder AI +- Step Library (Library, orange `#fb923c`) +- KB Accelerator (Lightbulb, rose `#fb7185`) + +**Insights** — reviewing results: +- Exports (FileOutput, blue `#60a5fa`) +- Analytics (BarChart3, sky `#38bdf8`) + +Group labels use `JetBrains Mono`, `0.5625rem`, uppercase, `letter-spacing: 0.12em`, `color: #5a6170`, `padding: 14px 12px 5px` (first group: `padding-top: 8px`). + +#### 4. AI Assistant Split + +| Nav Item | Group | Icon | Color | Purpose | Route | +|----------|-------|------|-------|---------|-------| +| FlowPilot | Resolve | Brain | `#e879f9` (fuchsia) | In-session AI copilot — suggests next steps, explains errors, provides ticket context during active troubleshooting | `/assistant` (existing route, rename label in UI only) | +| Flow Assist | Build | WandSparkles | `#f472b6` (pink) | Conversational flow builder — standalone page with a chat interface for generating flows from natural language. Renders a full-page chat UI (similar to `AssistantChatPage`) that creates flows via the AI chat service with `flow_type` selection. NOT a modal wrapper — it's a dedicated page. | `/flow-assist` (new route + new page component `FlowAssistPage`) | + +Both names are already in use internally (FlowPilot in the copilot panel, Flow Assist in the editor AI panel). This just promotes them to top-level navigation. + +**Note:** `Wand2` is an alias for `WandSparkles` in lucide-react@0.563.0. Use `WandSparkles` as the canonical import. + +### Footer (unchanged structure) +- User Guides (BookOpen, lime `#a3e635`) +- Feedback (MessageSquareText, indigo `#818cf8`) +- Account (Settings, no color) +- Collapse (PanelLeftClose, no color) + +### Collapsed Sidebar +When collapsed, the stats bar and activity feed are hidden. Icon-only nav items are shown in a flat list (no group labels). The collapsed view includes all 13 nav items including the two new AI items: + +- Dashboard (LayoutGrid, cyan) +- Sessions (Clock, emerald) +- All Flows (Network, violet) +- FlowPilot (Brain, fuchsia) +- Script Library (Code2, teal) +- Flow Editor (Wrench, amber) +- Flow Assist (WandSparkles, pink) +- Step Library (Library, orange) +- KB Accelerator (Lightbulb, rose) +- Exports (FileOutput, blue) +- Analytics (BarChart3, sky) +- User Guides (BookOpen, lime) +- Feedback (MessageSquareText, indigo) + +Account and Collapse remain in the footer section of the collapsed view (same as current). + +## Data Requirements + +### New Backend Endpoint + +**`GET /api/v1/sessions/sidebar-stats`** — lightweight endpoint for sidebar data, returns: + +```json +{ + "resolved_today": 7, + "active_count": 2, + "total_session_minutes_today": 134, + "active_sessions": [ + { + "session_id": "uuid", + "tree_name": "O365 Mail Flow", + "tree_id": "uuid", + "tree_type": "troubleshooting", + "started_at": "2026-03-15T14:30:00Z", + "ticket_number": "#48291" + } + ], + "recent_completions": [ + { + "session_id": "uuid", + "tree_name": "DNS Resolution", + "tree_id": "uuid", + "tree_type": "troubleshooting", + "completed_at": "2026-03-15T12:15:00Z" + } + ] +} +``` + +**Query parameters:** +- `tz_offset` (integer, required): Client's UTC offset in minutes (e.g., `-300` for EST). Used to compute "today" boundaries. Frontend sends `new Date().getTimezoneOffset()`. + +**Backend logic:** +- `resolved_today`: `SELECT COUNT(*) FROM sessions WHERE user_id = :uid AND completed_at >= :today_start AND outcome = 'resolved'` +- `active_count`: `SELECT COUNT(*) FROM sessions WHERE user_id = :uid AND completed_at IS NULL` +- `total_session_minutes_today`: Sum of `EXTRACT(EPOCH FROM (COALESCE(completed_at, now()) - started_at)) / 60` for all sessions where `started_at >= :today_start` +- `active_sessions`: Up to 5, ordered by `started_at DESC`, with joined tree name/type +- `recent_completions`: Up to 3 completed today, ordered by `completed_at DESC` +- `ticket_number`: Populated from PSA connection context if available (depends on CW integration state — nullable) +- `today_start`: Computed as midnight in the client's timezone, converted to UTC using `tz_offset` + +**Sessions spanning midnight:** A session started yesterday that is still active today counts toward `total_session_minutes_today` for its full duration (from `started_at`, not from midnight). It also appears in `active_sessions`. This matches user intuition — "I've been on this for 3 hours" regardless of when midnight fell. + +### Frontend Components + +**New components:** +- `SidebarStatsBar` — three-stat row component +- `SidebarActivityFeed` — active sessions + recents feed with "Activity" header +- `ActivityItem` — single activity row (dot + name + ticket/timestamp) +- `FlowAssistPage` — standalone page for conversational flow building + +**Modified components:** +- `Sidebar.tsx` — major restructure: remove PinnedFlowsSection, add stats bar + activity feed, reorganize nav into groups with labels, add FlowPilot + Flow Assist items, update collapsed view icon set +- `router.tsx` — add `/flow-assist` route +- `NavItem.tsx` — no changes needed (already supports `iconColor` prop) + +**Removed:** +- `PinnedFlowsSection.tsx` +- `pinnedFlowsStore.ts` +- `api/pinnedFlows.ts` (if it exists) +- Related pinning UI in flow cards/pages (pin buttons, pin actions) + +## Visual Reference + +Mockups created during brainstorming are in `.superpowers/brainstorm/` (HTML files, open in browser). The approved composite is `a3-full-sidebar.html` showing the "With AI Split" variant. + +## Out of Scope + +- ConnectWise ticket deep-linking (future — just display the number for now) +- Team activity feed (showing what other engineers are doing — potential future enhancement) +- Removing the pinned flows backend API/database tables (just remove frontend usage; backend cleanup is separate) +- Changes to the analytics pages themselves +- Mobile/responsive sidebar behavior (current behavior unchanged) +- Polling/WebSocket for live activity updates (fetch on mount only) + +## Edge Cases + +- **No active sessions:** Stats bar shows "0" for Active and "0m" for In Session. Activity section shows only recents (or "No activity today" placeholder if no recents either). +- **No CW integration:** Ticket numbers simply don't appear. Activity items show flow name only. +- **Many active sessions:** Activity feed caps at 5 active sessions with a "View all in Sessions →" link below. +- **Midnight rollover:** Stats reset based on client timezone offset passed to the API. Frontend sends `tz_offset` on each sidebar fetch. +- **Sessions spanning midnight:** Count full duration from `started_at`, not partial duration from midnight. +- **New user / no sessions ever:** Stats bar shows all zeros. Activity section shows an empty state: "No activity today". + +## Accessibility + +- Pulsing green dot: add `aria-label="Active session"` to the dot element +- Amber dot: `aria-label="Paused session"` +- Stats bar values: wrap in elements with `aria-label` combining value + label (e.g., "7 resolved today") +- Activity items: render as ` - )} {tree.is_public ? ( diff --git a/frontend/src/components/library/TreeListView.tsx b/frontend/src/components/library/TreeListView.tsx index 43e02529..ebef1397 100644 --- a/frontend/src/components/library/TreeListView.tsx +++ b/frontend/src/components/library/TreeListView.tsx @@ -1,5 +1,5 @@ import { Link } from 'react-router-dom' -import { Pencil, Globe, Lock, GitBranch, FileText, Trash2, Wrench, Star, Download, ClipboardList } from 'lucide-react' +import { Pencil, Globe, Lock, GitBranch, FileText, Trash2, Wrench, Download, ClipboardList } from 'lucide-react' import type { TreeListItem } from '@/types' import { TagBadges } from '@/components/common/TagBadges' import { cn } from '@/lib/utils' @@ -15,9 +15,6 @@ interface TreeListViewProps { onDeleteTree: (tree: TreeListItem) => void onForkTree?: (treeId: string) => void onExportTree?: (treeId: string) => void - pinnedTreeIds?: Set - onTogglePin?: (treeId: string) => void - pinLoadingTreeIds?: Set } export function TreeListView({ @@ -28,9 +25,6 @@ export function TreeListView({ onDeleteTree, onForkTree, onExportTree, - pinnedTreeIds, - onTogglePin, - pinLoadingTreeIds, }: TreeListViewProps) { const { canEditTree } = usePermissions() @@ -99,26 +93,6 @@ export function TreeListView({
- {onTogglePin && ( - - )} {onExportTree && (