* 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>
179 lines
5.9 KiB
Python
179 lines
5.9 KiB
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_
|
|
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,
|
|
)
|