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
This commit is contained in:
Jack Levy
2026-02-28 22:25:49 -05:00
parent c48241fe2f
commit 6a1b387dd2
3 changed files with 104 additions and 16 deletions

View File

@@ -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 <div className="text-center py-20 text-muted-foreground">Loading...</div>;
if (!currentUser?.is_admin) {
return (
<div className="text-center py-20 text-muted-foreground">
Admin access required.
</div>
);
return <div className="text-center py-20 text-muted-foreground">Admin access required.</div>;
}
const pct = stats && stats.total_bills > 0
? Math.round((stats.briefs_generated / stats.total_bills) * 100)
: 0;
return (
<div className="space-y-8 max-w-2xl">
<div>
@@ -101,6 +111,51 @@ export default function SettingsPage() {
<p className="text-muted-foreground text-sm mt-1">Manage users, LLM provider, and system settings</p>
</div>
{/* Analysis Status */}
<section className="bg-card border border-border rounded-lg p-6 space-y-4">
<h2 className="font-semibold flex items-center gap-2">
<BarChart3 className="w-4 h-4" /> Analysis Status
<span className="text-xs text-muted-foreground font-normal ml-auto">refreshes every 30s</span>
</h2>
{stats ? (
<>
<div className="grid grid-cols-3 gap-4">
<div className="bg-muted/50 rounded-lg p-3 text-center">
<FileText className="w-4 h-4 mx-auto mb-1 text-muted-foreground" />
<div className="text-xl font-bold">{stats.total_bills.toLocaleString()}</div>
<div className="text-xs text-muted-foreground">Total Bills</div>
</div>
<div className="bg-muted/50 rounded-lg p-3 text-center">
<FileText className="w-4 h-4 mx-auto mb-1 text-blue-500" />
<div className="text-xl font-bold">{stats.docs_fetched.toLocaleString()}</div>
<div className="text-xs text-muted-foreground">Docs Fetched</div>
</div>
<div className="bg-muted/50 rounded-lg p-3 text-center">
<Brain className="w-4 h-4 mx-auto mb-1 text-green-500" />
<div className="text-xl font-bold">{stats.briefs_generated.toLocaleString()}</div>
<div className="text-xs text-muted-foreground">Briefs Generated</div>
</div>
</div>
{/* Progress bar */}
<div className="space-y-1">
<div className="flex justify-between text-xs text-muted-foreground">
<span>{stats.full_briefs} full · {stats.amendment_briefs} amendments</span>
<span>{pct}% analyzed · {stats.remaining.toLocaleString()} remaining</span>
</div>
<div className="h-2 bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-green-500 rounded-full transition-all duration-500"
style={{ width: `${pct}%` }}
/>
</div>
</div>
</>
) : (
<p className="text-sm text-muted-foreground">Loading stats...</p>
)}
</section>
{/* User Management */}
<section className="bg-card border border-border rounded-lg p-6 space-y-4">
<h2 className="font-semibold flex items-center gap-2">
@@ -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 ? (
<ShieldOff className="w-4 h-4" />
) : (
<ShieldCheck className="w-4 h-4" />
)}
{u.is_admin ? <ShieldOff className="w-4 h-4" /> : <ShieldCheck className="w-4 h-4" />}
</button>
{confirmDelete === u.id ? (
<div className="flex items-center gap-1">
<button
onClick={() => {
deleteUser.mutate(u.id);
setConfirmDelete(null);
}}
onClick={() => { deleteUser.mutate(u.id); setConfirmDelete(null); }}
className="text-xs px-2 py-1 bg-destructive text-destructive-foreground rounded hover:bg-destructive/90"
>
Confirm
@@ -204,7 +252,6 @@ export default function SettingsPage() {
</label>
))}
</div>
<div className="flex items-center gap-3 pt-2 border-t border-border">
<button
onClick={testLLM}

View File

@@ -123,8 +123,20 @@ export interface AdminUser {
created_at: string;
}
export interface AnalysisStats {
total_bills: number;
docs_fetched: number;
briefs_generated: number;
full_briefs: number;
amendment_briefs: number;
remaining: number;
}
// Admin
export const adminAPI = {
// Stats
getStats: () =>
apiClient.get<AnalysisStats>("/api/admin/stats").then((r) => r.data),
// Users
listUsers: () =>
apiClient.get<AdminUser[]>("/api/admin/users").then((r) => r.data),