chrysopedia/frontend/src/pages/AdminUsage.tsx
jlightner be1919e223 feat: Added GET /admin/usage endpoint with today/week/month token aggre…
- "backend/routers/admin.py"
- "frontend/src/api/admin-usage.ts"
- "frontend/src/pages/AdminUsage.tsx"
- "frontend/src/pages/AdminUsage.module.css"
- "frontend/src/App.tsx"
- "frontend/src/components/AdminDropdown.tsx"
- "frontend/src/api/index.ts"

GSD-Task: S04/T02
2026-04-04 13:44:44 +00:00

153 lines
5.2 KiB
TypeScript

import { useEffect, useState } from "react";
import { useAuth } from "../context/AuthContext";
import { fetchUsageStats, type UsageStats } from "../api/admin-usage";
import { useDocumentTitle } from "../hooks/useDocumentTitle";
import styles from "./AdminUsage.module.css";
function formatNumber(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
return n.toLocaleString();
}
function shortDay(dateStr: string): string {
const d = new Date(dateStr + "T00:00:00");
return d.toLocaleDateString(undefined, { weekday: "short" });
}
export default function AdminUsage() {
useDocumentTitle("Usage — Admin");
const { token } = useAuth();
const [stats, setStats] = useState<UsageStats | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!token) return;
setLoading(true);
fetchUsageStats(token)
.then(setStats)
.catch((e) => setError(e.message || "Failed to load usage stats"))
.finally(() => setLoading(false));
}, [token]);
if (loading) {
return (
<div className={styles.page}>
<h1 className={styles.title}>Usage</h1>
<p className={styles.loading}>Loading usage stats</p>
</div>
);
}
if (error || !stats) {
return (
<div className={styles.page}>
<h1 className={styles.title}>Usage</h1>
<p className={styles.error}>{error || "No data available"}</p>
</div>
);
}
const maxDaily = Math.max(...stats.daily_counts.map((d) => d.request_count), 1);
return (
<div className={styles.page}>
<h1 className={styles.title}>Usage</h1>
{/* Summary cards */}
<div className={styles.summaryGrid}>
{([
{ label: "Today", data: stats.today },
{ label: "This Week", data: stats.week },
{ label: "This Month", data: stats.month },
] as const).map(({ label, data }) => (
<div key={label} className={styles.card}>
<span className={styles.cardLabel}>{label}</span>
<span className={styles.cardValue}>
{formatNumber(data.total_tokens)} tokens
</span>
<span className={styles.cardSub}>
{data.request_count} request{data.request_count !== 1 ? "s" : ""}
{" · "}
{formatNumber(data.prompt_tokens)} prompt / {formatNumber(data.completion_tokens)} completion
</span>
</div>
))}
</div>
{/* Daily bar chart */}
{stats.daily_counts.length > 0 && (
<div className={styles.section}>
<h2 className={styles.sectionTitle}>Last 7 Days</h2>
<div className={styles.chartContainer}>
{stats.daily_counts.map((d) => (
<div key={d.date} className={styles.barColumn}>
<span className={styles.barValue}>{d.request_count}</span>
<div
className={styles.bar}
style={{ height: `${(d.request_count / maxDaily) * 80}%` }}
/>
<span className={styles.barLabel}>{shortDay(d.date)}</span>
</div>
))}
</div>
</div>
)}
{/* Top creators */}
<div className={styles.section}>
<h2 className={styles.sectionTitle}>Top Creators (This Month)</h2>
{stats.top_creators.length === 0 ? (
<p className={styles.empty}>No creator usage data yet.</p>
) : (
<table className={styles.table}>
<thead>
<tr>
<th>Creator</th>
<th className={styles.numCell}>Requests</th>
<th className={styles.numCell}>Tokens</th>
</tr>
</thead>
<tbody>
{stats.top_creators.map((c) => (
<tr key={c.creator_slug}>
<td>{c.creator_slug}</td>
<td className={styles.numCell}>{c.request_count.toLocaleString()}</td>
<td className={styles.numCell}>{formatNumber(c.total_tokens)}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{/* Top users */}
<div className={styles.section}>
<h2 className={styles.sectionTitle}>Top Users (This Month)</h2>
{stats.top_users.length === 0 ? (
<p className={styles.empty}>No user usage data yet.</p>
) : (
<table className={styles.table}>
<thead>
<tr>
<th>User / IP</th>
<th className={styles.numCell}>Requests</th>
<th className={styles.numCell}>Tokens</th>
</tr>
</thead>
<tbody>
{stats.top_users.map((u, i) => (
<tr key={`${u.identifier}-${i}`}>
<td>{u.identifier}</td>
<td className={styles.numCell}>{u.request_count.toLocaleString()}</td>
<td className={styles.numCell}>{formatNumber(u.total_tokens)}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}