From 357f8e2d0802a493340cb9f43774d8752cd5bc79 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Mon, 16 Mar 2026 01:35:16 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20sidebar=20redesign=20=E2=80=94=20activi?= =?UTF-8?q?ty=20feed,=20grouped=20nav,=20AI=20split=20(#107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add 5 sidebar icon color concepts for UX review Co-Authored-By: Claude Opus 4.6 (1M context) * feat(ui): add semantic icon colors and updated icons to sidebar nav Swap generic icons for more descriptive alternatives (Network, Wrench, FileOutput, Library, Code2, Lightbulb) and assign each nav item a unique semantic color for instant visual landmarks. Co-Authored-By: Claude Opus 4.6 (1M context) * fix(ui): default Sessions page to Active tab, reorder tabs Active sessions are what engineers care about most. Tab order is now Active, Prepared, Completed, All. Co-Authored-By: Claude Opus 4.6 (1M context) * docs: add sidebar grouping and AI naming concept mockups Co-Authored-By: Claude Opus 4.6 (1M context) * docs: add sidebar redesign context and decision summary Co-Authored-By: Claude Opus 4.6 (1M context) * docs: add sidebar redesign spec and implementation plan Design spec covers: activity zone with daily stats + session feed, nav grouping (Resolve/Build/Insights), AI split (FlowPilot + Flow Assist), pinned flows removal. Implementation plan has 5 chunks, 12 tasks, 39 steps. Co-Authored-By: Claude Opus 4.6 (1M context) * feat: add sidebar stats Pydantic schemas Co-Authored-By: Claude Opus 4.6 (1M context) * test: add failing tests for sidebar stats endpoint Co-Authored-By: Claude Opus 4.6 (1M context) * feat: add sidebar stats endpoint with daily stats and activity feed Co-Authored-By: Claude Opus 4.6 (1M context) * feat: add sidebar API client, stats bar, activity feed components New components: SidebarStatsBar, SidebarActivityFeed, ActivityItem. New API client for sidebar stats endpoint. Pulse-dot CSS animation. Co-Authored-By: Claude Opus 4.6 (1M context) * feat: restructure sidebar with stats bar, activity feed, and grouped nav Dashboard-first layout with Resolve/Build/Insights groups. AI split: FlowPilot (Resolve) + Flow Assist (Build). Stats bar: Resolved/Active/In Session daily counters. Activity feed: active sessions with CW ticket #, recent completions. Co-Authored-By: Claude Opus 4.6 (1M context) * refactor: remove pinned flows frontend (PinnedFlowsSection, store, API, pin buttons) Removed: PinnedFlowsSection component, pinnedFlowsStore, pinnedFlows API client. Cleaned: pin buttons from TreeGridView, TreeListView, TreeTableView. Cleaned: favorites section from QuickStartPage, pin props from TreeLibraryPage. Co-Authored-By: Claude Opus 4.6 (1M context) * feat: add FlowAssistPage placeholder and /flow-assist route Co-Authored-By: Claude Opus 4.6 (1M context) * feat: real-time sidebar stats via session-changed events Sidebar now refreshes stats when sessions are created or completed, not just on page navigation. Uses window event bus pattern (same as folder-changed events in codebase). Co-Authored-By: Claude Opus 4.6 (1M context) * feat: live-ticking In Session timer using active session start times SidebarStatsBar now computes active session elapsed time client-side from started_at timestamps, ticking every 60s. Backend only returns completed session minutes to avoid double-counting. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: sidebar In Session timer ticks every second and shows seconds Timer now uses 1s interval (not 60s) and displays seconds when under a minute so it matches the session timer in the flow UI. Co-Authored-By: Claude Opus 4.6 (1M context) * chore: trigger PR environment redeploy Co-Authored-By: Claude Opus 4.6 (1M context) * debug: add console.log to SidebarStatsBar for timer investigation Co-Authored-By: Claude Opus 4.6 (1M context) * fix: parse sidebar timestamps as UTC (append Z suffix) Backend returns naive UTC timestamps without timezone indicator. JS Date() treats bare ISO strings as local time, causing the timer to compute negative elapsed time (future timestamps). Appending 'Z' forces UTC parsing. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: rename 'In Session' to 'Total Time' for clarity Makes it clear the timer is an aggregate of all sessions today, not just the current one. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- backend/app/api/endpoints/sidebar.py | 178 +++ backend/app/api/router.py | 3 +- backend/app/schemas/sidebar.py | 45 + backend/tests/test_sidebar_stats.py | 142 ++ .../Frontend/sidebar-grouping-concepts.html | 630 ++++++++ .../plans/Frontend/sidebar-icon-concepts.html | 734 ++++++++++ .../Frontend/sidebar-redesign-context.md | 83 ++ .../plans/2026-03-15-sidebar-redesign.md | 1269 +++++++++++++++++ .../2026-03-15-sidebar-redesign-design.md | 271 ++++ frontend/src/api/index.ts | 2 +- frontend/src/api/pinnedFlows.ts | 34 - frontend/src/api/sessions.ts | 2 + frontend/src/api/sidebar.ts | 45 + frontend/src/components/layout/NavItem.tsx | 7 +- frontend/src/components/layout/Sidebar.tsx | 172 ++- .../src/components/library/TreeGridView.tsx | 28 +- .../src/components/library/TreeListView.tsx | 28 +- .../src/components/library/TreeTableView.tsx | 35 +- .../src/components/sidebar/ActivityItem.tsx | 105 ++ .../components/sidebar/PinnedFlowsSection.tsx | 98 -- .../sidebar/SidebarActivityFeed.tsx | 80 ++ .../components/sidebar/SidebarStatsBar.tsx | 93 ++ frontend/src/index.css | 5 + frontend/src/pages/FlowAssistPage.tsx | 30 + frontend/src/pages/QuickStartPage.tsx | 95 +- frontend/src/pages/SessionHistoryPage.tsx | 4 +- frontend/src/pages/TreeLibraryPage.tsx | 26 +- frontend/src/router.tsx | 2 + frontend/src/store/pinnedFlowsStore.ts | 104 -- 29 files changed, 3836 insertions(+), 514 deletions(-) create mode 100644 backend/app/api/endpoints/sidebar.py create mode 100644 backend/app/schemas/sidebar.py create mode 100644 backend/tests/test_sidebar_stats.py create mode 100644 docs/plans/Frontend/sidebar-grouping-concepts.html create mode 100644 docs/plans/Frontend/sidebar-icon-concepts.html create mode 100644 docs/plans/Frontend/sidebar-redesign-context.md create mode 100644 docs/superpowers/plans/2026-03-15-sidebar-redesign.md create mode 100644 docs/superpowers/specs/2026-03-15-sidebar-redesign-design.md delete mode 100644 frontend/src/api/pinnedFlows.ts create mode 100644 frontend/src/api/sidebar.ts create mode 100644 frontend/src/components/sidebar/ActivityItem.tsx delete mode 100644 frontend/src/components/sidebar/PinnedFlowsSection.tsx create mode 100644 frontend/src/components/sidebar/SidebarActivityFeed.tsx create mode 100644 frontend/src/components/sidebar/SidebarStatsBar.tsx create mode 100644 frontend/src/pages/FlowAssistPage.tsx delete mode 100644 frontend/src/store/pinnedFlowsStore.ts 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 && (