From 52bc4992729f4fe756eacc4a5cd0394ddb967ee8 Mon Sep 17 00:00:00 2001 From: DJP Date: Fri, 10 Apr 2026 17:25:34 -0400 Subject: [PATCH] fix: sidebar highlighting for shared paths and report SQL errors Fix sidebar nav so Dashboard/Monitoring and Audit Trail/System Logs highlight independently by using useSearchParams to distinguish query-param-based routes. Fix get_jobs_over_time SQL GroupingError by using literal_column for date_trunc interval. Add date filters to status_breakdown query and fix Decimal serialization in locale stats. Co-Authored-By: Claude Opus 4.6 --- backend/app/services/report_service.py | 38 +++++++++++++--------- frontend/src/components/layout/Sidebar.tsx | 24 ++++++++++++-- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/backend/app/services/report_service.py b/backend/app/services/report_service.py index 2df8110..90ef479 100644 --- a/backend/app/services/report_service.py +++ b/backend/app/services/report_service.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import Any from uuid import UUID -from sqlalchemy import case, func, select +from sqlalchemy import case, func, literal_column, select, text from sqlalchemy.ext.asyncio import AsyncSession from app.models.audit import TokenUsageLog @@ -37,12 +37,16 @@ class ReportService: result = await db.execute(job_query) row = result.one() - # Status breakdown + # Status breakdown (apply same filters as main query) status_query = select( Job.status, func.count(Job.id) ).group_by(Job.status) if client_id: status_query = status_query.where(Job.client_id == client_id) + if date_from: + status_query = status_query.where(Job.created_at >= date_from) + if date_to: + status_query = status_query.where(Job.created_at <= date_to) status_result = await db.execute(status_query) status_breakdown = { status.value: count for status, count in status_result.all() @@ -63,16 +67,17 @@ class ReportService: date_to: datetime | None = None, ) -> list[dict[str, Any]]: """Get token usage and cost data grouped by date.""" + day_expr = func.date_trunc(literal_column("'day'"), TokenUsageLog.timestamp) query = ( select( - func.date_trunc("day", TokenUsageLog.timestamp).label("date"), + day_expr.label("date"), func.sum(TokenUsageLog.input_tokens).label("input_tokens"), func.sum(TokenUsageLog.output_tokens).label("output_tokens"), func.sum(TokenUsageLog.total_tokens).label("total_tokens"), func.sum(TokenUsageLog.estimated_cost_usd).label("total_cost"), ) - .group_by(func.date_trunc("day", TokenUsageLog.timestamp)) - .order_by(func.date_trunc("day", TokenUsageLog.timestamp)) + .group_by(day_expr) + .order_by(day_expr) ) if client_id: @@ -155,13 +160,14 @@ class ReportService: LocaleInstance.completed_at.isnot(None), ).group_by(LocaleInstance.locale_code) + needs_job_join = bool(client_id or date_from or date_to) + if needs_job_join: + query = query.join(Job) if client_id: - query = query.join(Job).where(Job.client_id == client_id) + query = query.where(Job.client_id == client_id) if date_from: - query = query.join(Job, isouter=True).where(Job.created_at >= date_from) + query = query.where(Job.created_at >= date_from) if date_to: - if "job" not in str(query): - query = query.join(Job, isouter=True) query = query.where(Job.created_at <= date_to) result = await db.execute(query) @@ -171,7 +177,7 @@ class ReportService: "count": row.count, "total_tokens": row.total_tokens or 0, "total_cost": float(row.total_cost or 0.0), - "avg_duration_minutes": round((row.avg_duration_seconds or 0) / 60, 1), + "avg_duration_minutes": round(float(row.avg_duration_seconds or 0) / 60, 1), } for row in result.all() ] @@ -184,15 +190,17 @@ class ReportService: date_to: datetime | None = None, ) -> list[dict[str, Any]]: """Get job counts grouped by week.""" + week_expr = func.date_trunc(literal_column("'week'"), Job.created_at) + query = select( - func.date_trunc("week", Job.created_at).label("week"), + week_expr.label("week"), func.count(Job.id).label("total"), func.sum(case((Job.status == JobStatus.complete, 1), else_=0)).label("completed_raw"), func.sum(case((Job.status == JobStatus.error, 1), else_=0)).label("errors_raw"), ).group_by( - func.date_trunc("week", Job.created_at) + week_expr ).order_by( - func.date_trunc("week", Job.created_at) + week_expr ) if client_id: @@ -209,7 +217,7 @@ class ReportService: rows.append({ "name": week_str, "jobs": row.total or 0, - "completed": row.completed_raw or 0, - "errors": row.errors_raw or 0, + "completed": int(row.completed_raw or 0), + "errors": int(row.errors_raw or 0), }) return rows diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 3409b4e..938ae8b 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -3,7 +3,7 @@ import React, { useState } from "react"; import Link from "next/link"; import Image from "next/image"; -import { usePathname } from "next/navigation"; +import { usePathname, useSearchParams } from "next/navigation"; import { LayoutDashboard, PlusCircle, @@ -52,11 +52,29 @@ const mockUser = { export function Sidebar() { const pathname = usePathname(); + const searchParams = useSearchParams(); const [mobileOpen, setMobileOpen] = useState(false); const isActive = (href: string) => { - if (href === "/dashboard") return pathname === "/dashboard"; - return pathname.startsWith(href.split("?")[0]); + const [hrefPath, hrefQuery] = href.split("?"); + const basePath = hrefPath; + + // For items that share the same base path but differ by query params, + // we need exact matching including query params + if (basePath === "/dashboard") { + const hrefHasView = hrefQuery?.includes("view=monitoring"); + const currentHasView = searchParams.get("view") === "monitoring"; + if (hrefHasView) return pathname === "/dashboard" && currentHasView; + return pathname === "/dashboard" && !currentHasView; + } + if (basePath === "/admin/logs") { + const hrefHasSystem = hrefQuery?.includes("type=system"); + const currentHasSystem = searchParams.get("type") === "system"; + if (hrefHasSystem) return pathname === "/admin/logs" && currentHasSystem; + return pathname === "/admin/logs" && !currentHasSystem; + } + + return pathname.startsWith(basePath); }; const sidebarContent = (