From 6a1b387dd294de63d1cda75f90c23bd74ff5bd72 Mon Sep 17 00:00:00 2001 From: Jack Levy Date: Sat, 28 Feb 2026 22:25:49 -0500 Subject: [PATCH] Add analysis status panel to admin page - GET /api/admin/stats returns total bills, docs fetched, briefs generated (full + amendment), and remaining count - Admin page shows stat cards + progress bar, auto-refreshes every 30s Authored-By: Jack Levy --- backend/app/api/admin.py | 31 +++++++++++++- frontend/app/settings/page.tsx | 77 +++++++++++++++++++++++++++------- frontend/lib/api.ts | 12 ++++++ 3 files changed, 104 insertions(+), 16 deletions(-) diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index 2130e19..e20dc7c 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.dependencies import get_current_admin from app.database import get_db -from app.models import Follow +from app.models import Bill, BillBrief, BillDocument, Follow from app.models.user import User from app.schemas.schemas import UserResponse @@ -79,6 +79,35 @@ async def toggle_admin( return user +# ── Analysis Stats ──────────────────────────────────────────────────────────── + +@router.get("/stats") +async def get_stats( + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_admin), +): + """Return analysis pipeline progress counters.""" + total_bills = (await db.execute(select(func.count()).select_from(Bill))).scalar() + docs_fetched = (await db.execute( + select(func.count()).select_from(BillDocument).where(BillDocument.raw_text.isnot(None)) + )).scalar() + total_briefs = (await db.execute(select(func.count()).select_from(BillBrief))).scalar() + full_briefs = (await db.execute( + select(func.count()).select_from(BillBrief).where(BillBrief.brief_type == "full") + )).scalar() + amendment_briefs = (await db.execute( + select(func.count()).select_from(BillBrief).where(BillBrief.brief_type == "amendment") + )).scalar() + return { + "total_bills": total_bills, + "docs_fetched": docs_fetched, + "briefs_generated": total_briefs, + "full_briefs": full_briefs, + "amendment_briefs": amendment_briefs, + "remaining": total_bills - total_briefs, + } + + # ── Celery Tasks ────────────────────────────────────────────────────────────── @router.post("/trigger-poll") diff --git a/frontend/app/settings/page.tsx b/frontend/app/settings/page.tsx index d4481c6..10ddbcd 100644 --- a/frontend/app/settings/page.tsx +++ b/frontend/app/settings/page.tsx @@ -13,6 +13,9 @@ import { Trash2, ShieldCheck, ShieldOff, + FileText, + Brain, + BarChart3, } from "lucide-react"; import { settingsAPI, adminAPI, type AdminUser } from "@/lib/api"; import { useAuthStore } from "@/stores/authStore"; @@ -33,6 +36,13 @@ export default function SettingsPage() { queryFn: () => settingsAPI.get(), }); + const { data: stats } = useQuery({ + queryKey: ["admin-stats"], + queryFn: () => adminAPI.getStats(), + enabled: !!currentUser?.is_admin, + refetchInterval: 30_000, + }); + const { data: users, isLoading: usersLoading } = useQuery({ queryKey: ["admin-users"], queryFn: () => adminAPI.listUsers(), @@ -85,13 +95,13 @@ export default function SettingsPage() { if (settingsLoading) return
Loading...
; if (!currentUser?.is_admin) { - return ( -
- Admin access required. -
- ); + return
Admin access required.
; } + const pct = stats && stats.total_bills > 0 + ? Math.round((stats.briefs_generated / stats.total_bills) * 100) + : 0; + return (
@@ -101,6 +111,51 @@ export default function SettingsPage() {

Manage users, LLM provider, and system settings

+ {/* Analysis Status */} +
+

+ Analysis Status + refreshes every 30s +

+ {stats ? ( + <> +
+
+ +
{stats.total_bills.toLocaleString()}
+
Total Bills
+
+
+ +
{stats.docs_fetched.toLocaleString()}
+
Docs Fetched
+
+
+ +
{stats.briefs_generated.toLocaleString()}
+
Briefs Generated
+
+
+ + {/* Progress bar */} +
+
+ {stats.full_briefs} full · {stats.amendment_briefs} amendments + {pct}% analyzed · {stats.remaining.toLocaleString()} remaining +
+
+
+
+
+ + ) : ( +

Loading stats...

+ )} +
+ {/* User Management */}

@@ -137,19 +192,12 @@ export default function SettingsPage() { title={u.is_admin ? "Remove admin" : "Make admin"} className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors" > - {u.is_admin ? ( - - ) : ( - - )} + {u.is_admin ? : } {confirmDelete === u.id ? (
-