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 <noreply@anthropic.com>
This commit is contained in:
DJP 2026-04-10 17:25:34 -04:00
parent 5ef7e588b6
commit 52bc499272
2 changed files with 44 additions and 18 deletions

View file

@ -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

View file

@ -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 = (