- "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
153 lines
5.2 KiB
TypeScript
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>
|
|
);
|
|
}
|