feat: add sidebar stats endpoint with daily stats and activity feed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
177
backend/app/api/endpoints/sidebar.py
Normal file
177
backend/app/api/endpoints/sidebar.py
Normal file
@@ -0,0 +1,177 @@
|
||||
"""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
|
||||
|
||||
# --- Total session minutes 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,
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user