* docs: add 5 sidebar icon color concepts for UX review Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * docs: add sidebar grouping and AI naming concept mockups Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add sidebar redesign context and decision summary Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * feat: add sidebar stats Pydantic schemas Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add failing tests for sidebar stats endpoint Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add sidebar stats endpoint with daily stats and activity feed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * feat: add FlowAssistPage placeholder and /flow-assist route Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * chore: trigger PR environment redeploy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * debug: add console.log to SidebarStatsBar for timer investigation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
41 KiB
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 responsebackend/app/api/endpoints/sidebar.py—GET /sessions/sidebar-statsendpoint
Backend (modify)
backend/app/api/router.py— register new sidebar router
Frontend (new)
frontend/src/components/sidebar/SidebarStatsBar.tsx— three-stat row componentfrontend/src/components/sidebar/SidebarActivityFeed.tsx— activity feed (active + recents)frontend/src/components/sidebar/ActivityItem.tsx— single activity rowfrontend/src/api/sidebar.ts— API client for sidebar statsfrontend/src/pages/FlowAssistPage.tsx— standalone flow builder AI page
Frontend (modify)
frontend/src/components/layout/Sidebar.tsx— major restructurefrontend/src/router.tsx— add/flow-assistroutefrontend/src/api/index.ts— export new sidebar APIfrontend/src/pages/TreeLibraryPage.tsx— remove pin integrationfrontend/src/components/library/TreeGridView.tsx— remove pin buttonfrontend/src/components/library/TreeListView.tsx— remove pin buttonfrontend/src/components/library/TreeTableView.tsx— remove pin button
Frontend (delete)
frontend/src/components/sidebar/PinnedFlowsSection.tsxfrontend/src/store/pinnedFlowsStore.tsfrontend/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
"""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
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
"""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
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
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:
"""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}.
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
cd backend && python -m pytest tests/test_sidebar_stats.py -v --override-ini="addopts="
Expected: All 7 tests PASS.
- Step 9: Commit
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
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<SidebarStatsResponse> => {
const tzOffset = new Date().getTimezoneOffset()
const response = await apiClient.get<SidebarStatsResponse>(
`/sessions/sidebar-stats?tz_offset=${tzOffset}`
)
return response.data
},
}
- Step 11: Export from api/index.ts
Add to frontend/src/api/index.ts:
export { sidebarApi } from './sidebar'
- Step 12: Commit
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
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 (
<button
onClick={handleClick}
className={cn(
'flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left transition-colors',
'hover:bg-[rgba(255,255,255,0.03)]',
isRecent ? 'text-[#6b7280] text-[0.72rem]' : 'text-[#e2e8f0] text-[0.8rem]'
)}
title={`${treeName}${ticketNumber ? ` (${ticketNumber})` : ''} — click to resume`}
aria-label={
status === 'active'
? `Active session: ${treeName}`
: status === 'paused'
? `Paused session: ${treeName}`
: `Recent session: ${treeName}`
}
>
{/* Status dot */}
{status === 'active' && (
<span
className="h-[7px] w-[7px] shrink-0 rounded-full"
style={{
background: '#34d399',
boxShadow: '0 0 6px rgba(52,211,153,0.5)',
animation: 'pulse-dot 2s ease-in-out infinite',
}}
aria-label="Active session"
/>
)}
{status === 'paused' && (
<span
className="h-[7px] w-[7px] shrink-0 rounded-full"
style={{
background: '#f59e0b',
boxShadow: '0 0 4px rgba(245,158,11,0.3)',
}}
aria-label="Paused session"
/>
)}
{status === 'recent' && (
<span className="h-1 w-1 shrink-0 rounded-full bg-[#3d4350]" />
)}
{/* Flow name */}
<span className="flex-1 truncate">{treeName}</span>
{/* Ticket number or timestamp */}
{ticketNumber && !isRecent && (
<span className="shrink-0 font-label text-[0.5625rem] text-[#60a5fa]">
{ticketNumber}
</span>
)}
{isRecent && timestamp && (
<span className="shrink-0 font-label text-[0.5625rem] text-[#5a6170]">
{formatRelativeTime(timestamp)}
</span>
)}
</button>
)
}
- Step 14: Commit
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
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 (
<div
className="flex gap-0.5 px-3 pt-2 pb-1"
role="group"
aria-label="Today's stats"
>
<div className="flex-1 rounded-md bg-[rgba(255,255,255,0.02)] px-1 py-1.5 text-center">
<div
className="font-label text-sm font-semibold leading-none"
style={{ color: '#34d399' }}
aria-label={`${resolved} resolved today`}
>
{resolved}
</div>
<div className="mt-1 font-label text-[7px] uppercase tracking-[0.1em] text-[#3d4350]">
Resolved
</div>
</div>
<div className="flex-1 rounded-md bg-[rgba(255,255,255,0.02)] px-1 py-1.5 text-center">
<div
className="font-label text-sm font-semibold leading-none"
style={{ color: '#22d3ee' }}
aria-label={`${active} active sessions`}
>
{active}
</div>
<div className="mt-1 font-label text-[7px] uppercase tracking-[0.1em] text-[#3d4350]">
Active
</div>
</div>
<div className="flex-1 rounded-md bg-[rgba(255,255,255,0.02)] px-1 py-1.5 text-center">
<div
className="font-label text-sm font-semibold leading-none text-muted-foreground"
aria-label={`${formatDuration(sessionMinutes)} in session today`}
>
{formatDuration(sessionMinutes)}
</div>
<div className="mt-1 font-label text-[7px] uppercase tracking-[0.1em] text-[#3d4350]">
In Session
</div>
</div>
</div>
)
}
- Step 16: Commit
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
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 (
<div className="px-3 pb-1">
{/* Header */}
<div className="flex items-center gap-1.5 px-2.5 py-1 mb-0.5">
<Clock size={10} style={{ color: '#34d399' }} />
<span className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170]">
Activity
</span>
</div>
{!hasActivity ? (
<p className="px-2.5 py-2 text-xs text-muted-foreground">
No activity today
</p>
) : (
<div className="space-y-0.5">
{/* Active sessions */}
{activeSessions.map((session) => (
<ActivityItem
key={session.session_id}
sessionId={session.session_id}
treeName={session.tree_name}
treeId={session.tree_id}
treeType={session.tree_type}
status="active"
ticketNumber={session.ticket_number || session.psa_ticket_id}
/>
))}
{/* Overflow link */}
{totalActive > 5 && (
<button
onClick={() => navigate('/sessions')}
className="w-full px-2.5 py-1 text-left text-[0.6875rem] text-muted-foreground hover:text-foreground transition-colors"
>
View all in Sessions →
</button>
)}
{/* Divider between active and recent */}
{activeSessions.length > 0 && recentCompletions.length > 0 && (
<div className="mx-2.5 my-1" style={{ height: '1px', background: 'rgba(255,255,255,0.03)' }} />
)}
{/* Recent completions */}
{recentCompletions.map((session) => (
<ActivityItem
key={session.session_id}
sessionId={session.session_id}
treeName={session.tree_name}
treeId={session.tree_id}
treeType={session.tree_type}
status="recent"
timestamp={session.completed_at}
/>
))}
</div>
)}
</div>
)
}
- Step 18: Commit
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):
@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
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:
- Remove
PinnedFlowsSectionimport andusePinnedFlowsStoreusage - Remove
sessionsApiandtreesApidata fetches (replaced bysidebarApi.getStats) - Add
SidebarStatsBarandSidebarActivityFeedin expanded mode - Add
BrainandWandSparklesicon imports - Reorganize nav items into Dashboard → Resolve → Build → Insights groups
- Update collapsed view with all 13 items
- Add
flowPilotandflowAssisttoNAV_COLORS
The new Sidebar.tsx should:
Imports:
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:
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):
const location = useLocation()
const [stats, setStats] = useState<SidebarStatsResponse | null>(null)
useEffect(() => {
sidebarApi.getStats().then(setStats).catch(() => {})
}, [location.pathname])
Expanded layout (inside the else branch):
{/* Stats Bar */}
<SidebarStatsBar
resolved={stats?.resolved_today ?? 0}
active={stats?.active_count ?? 0}
sessionMinutes={stats?.total_session_minutes_today ?? 0}
/>
{/* Activity Feed */}
<SidebarActivityFeed
activeSessions={stats?.active_sessions ?? []}
recentCompletions={stats?.recent_completions ?? []}
totalActive={stats?.active_count ?? 0}
/>
<div style={{ borderBottom: '1px solid var(--glass-border)' }} />
{/* Dashboard (standalone) */}
<div className="px-3 py-2 space-y-0.5">
<NavItem href="/" icon={LayoutGrid} label="Dashboard" iconColor={NAV_COLORS.dashboard} />
{/* Resolve group */}
<div className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170] px-3 pt-3 pb-1">
Resolve
</div>
<NavItem href="/sessions" icon={Clock} label="Sessions" badge={stats?.active_count || undefined} iconColor={NAV_COLORS.sessions} />
<NavItem
href="/trees"
icon={Network}
label="All Flows"
badge={stats?.tree_counts.total || undefined}
iconColor={NAV_COLORS.flows}
matchPaths={['/trees', '/flows']}
children={[
{ href: '/trees?type=troubleshooting', label: 'Troubleshooting', count: stats?.tree_counts.troubleshooting || undefined },
{ href: '/trees?type=procedural', label: 'Projects', count: stats?.tree_counts.procedural || undefined },
{ href: '/trees?type=maintenance', label: 'Maintenance', count: stats?.tree_counts.maintenance || undefined },
]}
/>
<NavItem href="/assistant" icon={Brain} label="FlowPilot" iconColor={NAV_COLORS.flowPilot} />
<NavItem href="/scripts" icon={Code2} label="Script Library" iconColor={NAV_COLORS.scripts} />
{/* Build group */}
<div className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170] px-3 pt-3 pb-1">
Build
</div>
<NavItem href="/my-trees" icon={Wrench} label="Flow Editor" iconColor={NAV_COLORS.editor} />
<NavItem href="/flow-assist" icon={WandSparkles} label="Flow Assist" iconColor={NAV_COLORS.flowAssist} />
<NavItem href="/step-library" icon={Library} label="Step Library" iconColor={NAV_COLORS.stepLib} />
<NavItem href="/kb-accelerator" icon={Lightbulb} label="KB Accelerator" iconColor={NAV_COLORS.kb} />
{/* Insights group */}
<div className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170] px-3 pt-3 pb-1">
Insights
</div>
<NavItem href="/shares" icon={FileOutput} label="Exports" iconColor={NAV_COLORS.exports} />
<NavItem href="/analytics" icon={BarChart3} label="Analytics" iconColor={NAV_COLORS.analytics} />
</div>
Collapsed layout — update to include all 13 items (add Brain/FlowPilot and WandSparkles/Flow Assist, remove BotMessageSquare/AI Assistant):
<div className="flex flex-col items-center px-1.5 py-3 space-y-1">
<NavItem href="/" icon={LayoutGrid} label="Dashboard" iconColor={NAV_COLORS.dashboard} collapsed />
<NavItem href="/sessions" icon={Clock} label="Sessions" badge={stats?.active_count || undefined} iconColor={NAV_COLORS.sessions} collapsed />
<NavItem href="/trees" icon={Network} label="All Flows" matchPaths={['/trees', '/flows']} iconColor={NAV_COLORS.flows} collapsed />
<NavItem href="/assistant" icon={Brain} label="FlowPilot" iconColor={NAV_COLORS.flowPilot} collapsed />
<NavItem href="/scripts" icon={Code2} label="Script Library" iconColor={NAV_COLORS.scripts} collapsed />
<NavItem href="/my-trees" icon={Wrench} label="Flow Editor" iconColor={NAV_COLORS.editor} collapsed />
<NavItem href="/flow-assist" icon={WandSparkles} label="Flow Assist" iconColor={NAV_COLORS.flowAssist} collapsed />
<NavItem href="/step-library" icon={Library} label="Step Library" iconColor={NAV_COLORS.stepLib} collapsed />
<NavItem href="/kb-accelerator" icon={Lightbulb} label="KB Accelerator" iconColor={NAV_COLORS.kb} collapsed />
<NavItem href="/shares" icon={FileOutput} label="Exports" iconColor={NAV_COLORS.exports} collapsed />
<NavItem href="/analytics" icon={BarChart3} label="Analytics" iconColor={NAV_COLORS.analytics} collapsed />
<NavItem href="/guides" icon={BookOpen} label="User Guides" iconColor={NAV_COLORS.guides} collapsed />
<NavItem href="/feedback" icon={MessageSquareText} label="Feedback" iconColor={NAV_COLORS.feedback} collapsed />
</div>
- Step 22: Verify the app builds
cd frontend && npm run build
Expected: Build succeeds. Fix any TypeScript or import errors.
- Step 23: Commit
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
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:
-
usePinnedFlowsStoreimport and all related hooks (pinnedItems,isMutatingByTreeId,pinnedTreeIds,pinLoadingTreeIds,togglePin,loadPinned) -
The
useEffectthat callsloadPinned() -
The
pinnedTreeIds,onTogglePin,pinLoadingTreeIdsprops passed toTreeGridView,TreeListView, andTreeTableView -
Step 26: Remove pin button from TreeGridView.tsx
Remove:
-
Staricon import from lucide-react -
pinnedTreeIds,onTogglePin,pinLoadingTreeIdsfrom 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
cd frontend && npm run build
Expected: Build succeeds with no references to removed files.
- Step 30: Commit
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.
import { WandSparkles } from 'lucide-react'
export default function FlowAssistPage() {
return (
<div className="p-6">
<div className="mb-6">
<h1 className="font-heading text-2xl font-bold tracking-tight text-foreground">
<span className="inline-flex items-center gap-2">
<WandSparkles size={24} style={{ color: '#f472b6' }} />
Flow Assist
</span>
</h1>
<p className="mt-1 text-sm text-muted-foreground">
Build flows from natural language — describe what you need and Flow Assist will generate the decision tree or procedural steps.
</p>
</div>
<div className="glass-card-static p-8 text-center">
<WandSparkles size={40} className="mx-auto mb-4 text-muted-foreground" />
<h2 className="font-heading text-lg font-semibold text-foreground mb-2">
Coming Soon
</h2>
<p className="text-sm text-muted-foreground max-w-md mx-auto">
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.
</p>
</div>
</div>
)
}
- Step 32: Add the route in router.tsx
Add inside the protected route children (alongside other page routes like /assistant, /scripts, etc.):
{
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
cd frontend && npm run build
- Step 34: Commit
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
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
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
cd frontend && npm run build
Expected: Clean build, no errors.
- Step 38: Manual smoke test
Start the dev servers and verify:
- Sidebar loads with stats bar showing 0/0/0m on fresh state
- Activity feed shows "No activity today" when empty
- Nav is grouped: Dashboard → Resolve (Sessions, All Flows, FlowPilot, Script Library) → Build (Flow Editor, Flow Assist, Step Library, KB Accelerator) → Insights (Exports, Analytics)
- FlowPilot link goes to
/assistant - Flow Assist link goes to
/flow-assist(placeholder page) - Collapsed sidebar shows all 13 icon-only items
- Pin buttons are gone from tree library grid/list views
- 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:
git add <changed-files>
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 |