From 7195fee2f6e55d52402bf045ea74ffd147182791 Mon Sep 17 00:00:00 2001 From: decolua Date: Tue, 3 Mar 2026 16:19:44 +0700 Subject: [PATCH] Refactor UsageChart and UsageStats components to support dynamic period selection --- .../dashboard/usage/components/UsageChart.js | 53 +++++-------- src/app/api/usage/stats/route.js | 23 ++++++ src/lib/usageDb.js | 17 ++++- src/mitm/cert/install.js | 7 +- src/shared/components/UsageStats.js | 75 +++++++++++++++---- 5 files changed, 116 insertions(+), 59 deletions(-) create mode 100644 src/app/api/usage/stats/route.js diff --git a/src/app/(dashboard)/dashboard/usage/components/UsageChart.js b/src/app/(dashboard)/dashboard/usage/components/UsageChart.js index 7e277e8..f4941f6 100644 --- a/src/app/(dashboard)/dashboard/usage/components/UsageChart.js +++ b/src/app/(dashboard)/dashboard/usage/components/UsageChart.js @@ -14,13 +14,6 @@ import { } from "recharts"; import Card from "@/shared/components/Card"; -const PERIODS = [ - { value: "24h", label: "24h" }, - { value: "7d", label: "7D" }, - { value: "30d", label: "30D" }, - { value: "60d", label: "60D" }, -]; - const fmtTokens = (n) => { if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`; if (n >= 1000) return `${(n / 1000).toFixed(1)}K`; @@ -29,8 +22,7 @@ const fmtTokens = (n) => { const fmtCost = (n) => `$${(n || 0).toFixed(4)}`; -export default function UsageChart() { - const [period, setPeriod] = useState("7d"); +export default function UsageChart({ period = "7d" }) { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [viewMode, setViewMode] = useState("tokens"); @@ -58,32 +50,19 @@ export default function UsageChart() { return ( -
-
- - -
-
- {PERIODS.map((p) => ( - - ))} -
+
+ +
{loading ? ( @@ -157,4 +136,6 @@ export default function UsageChart() { ); } -UsageChart.propTypes = {}; +UsageChart.propTypes = { + period: PropTypes.string, +}; diff --git a/src/app/api/usage/stats/route.js b/src/app/api/usage/stats/route.js new file mode 100644 index 0000000..2e8c88a --- /dev/null +++ b/src/app/api/usage/stats/route.js @@ -0,0 +1,23 @@ +import { NextResponse } from "next/server"; +import { getUsageStats } from "@/lib/usageDb"; + +const VALID_PERIODS = new Set(["24h", "7d", "30d", "60d", "all"]); + +export const dynamic = "force-dynamic"; + +export async function GET(request) { + try { + const { searchParams } = new URL(request.url); + const period = searchParams.get("period") || "7d"; + + if (!VALID_PERIODS.has(period)) { + return NextResponse.json({ error: "Invalid period" }, { status: 400 }); + } + + const stats = await getUsageStats(period); + return NextResponse.json(stats); + } catch (error) { + console.error("[API] Failed to get usage stats:", error); + return NextResponse.json({ error: "Failed to fetch usage stats" }, { status: 500 }); + } +} diff --git a/src/lib/usageDb.js b/src/lib/usageDb.js index 838122a..e81513e 100644 --- a/src/lib/usageDb.js +++ b/src/lib/usageDb.js @@ -429,12 +429,21 @@ async function calculateCost(provider, model, tokens) { } } +const PERIOD_MS = { "24h": 86400000, "7d": 604800000, "30d": 2592000000, "60d": 5184000000 }; + /** * Get aggregated usage stats + * @param {"24h"|"7d"|"30d"|"60d"|"all"} period - Time period to filter */ -export async function getUsageStats() { +export async function getUsageStats(period = "all") { const db = await getUsageDb(); - const history = db.data.history || []; + let history = db.data.history || []; + + // Filter history by period + if (period && PERIOD_MS[period]) { + const cutoff = Date.now() - PERIOD_MS[period]; + history = history.filter((e) => new Date(e.timestamp).getTime() >= cutoff); + } // Import localDb to get provider connection names and API keys const { getProviderConnections, getApiKeys, getProviderNodes } = await import("@/lib/localDb.js"); @@ -571,8 +580,8 @@ export async function getUsageStats() { const completionTokens = entry.tokens?.completion_tokens || 0; const entryTime = new Date(entry.timestamp); - // Calculate cost for this entry - const entryCost = await calculateCost(entry.provider, entry.model, entry.tokens); + // Use pre-stored cost (saved at request time), avoid recalculating + const entryCost = entry.cost || 0; stats.totalPromptTokens += promptTokens; stats.totalCompletionTokens += completionTokens; diff --git a/src/mitm/cert/install.js b/src/mitm/cert/install.js index 6d73be5..6613af8 100644 --- a/src/mitm/cert/install.js +++ b/src/mitm/cert/install.js @@ -26,9 +26,10 @@ async function checkCertInstalled(certPath) { function checkCertInstalledMac(certPath) { return new Promise((resolve) => { try { - const fingerprint = getCertFingerprint(certPath); - exec(`security find-certificate -a -Z /Library/Keychains/System.keychain | grep -i "${fingerprint}"`, (error) => { - resolve(!error); + // security outputs fingerprint without colons (e.g. "078B6B5F..."), strip them before grep + const fingerprint = getCertFingerprint(certPath).replace(/:/g, ""); + exec(`security find-certificate -a -Z /Library/Keychains/System.keychain | grep -i "${fingerprint}"`, (error, stdout) => { + resolve(!error && !!stdout?.trim()); }); } catch { resolve(false); diff --git a/src/shared/components/UsageStats.js b/src/shared/components/UsageStats.js index f08a1cf..87ac8ff 100644 --- a/src/shared/components/UsageStats.js +++ b/src/shared/components/UsageStats.js @@ -161,6 +161,13 @@ const TABLE_OPTIONS = [ { value: "endpoint", label: "Usage by Endpoint" }, ]; +const PERIODS = [ + { value: "24h", label: "24h" }, + { value: "7d", label: "7D" }, + { value: "30d", label: "30D" }, + { value: "60d", label: "60D" }, +]; + export default function UsageStats() { const router = useRouter(); const searchParams = useSearchParams(); @@ -170,8 +177,10 @@ export default function UsageStats() { const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); + const [fetching, setFetching] = useState(false); const [tableView, setTableView] = useState("model"); const [providers, setProviders] = useState([]); + const [period, setPeriod] = useState("7d"); // Fetch connected providers once, deduplicate by provider type useEffect(() => { @@ -190,33 +199,48 @@ export default function UsageStats() { .catch(() => {}); }, []); - // SSE connection - no polling, event-driven + // Fetch filtered stats via REST when period changes useEffect(() => { - console.log("[SSE CLIENT] connecting..."); - const es = new EventSource("/api/usage/stream"); + // First load: show full spinner; subsequent: show subtle fetching indicator + if (!stats) setLoading(true); + else setFetching(true); - es.onopen = () => console.log("[SSE CLIENT] connected ✓"); + fetch(`/api/usage/stats?period=${period}`) + .then((r) => r.ok ? r.json() : null) + .then((data) => { + if (data) setStats((prev) => ({ ...prev, ...data })); + }) + .catch(() => {}) + .finally(() => { + setLoading(false); + setFetching(false); + }); + }, [period]); // eslint-disable-line react-hooks/exhaustive-deps + + // SSE connection - real-time updates for activeRequests + recentRequests only + useEffect(() => { + const es = new EventSource("/api/usage/stream"); es.onmessage = (e) => { try { const data = JSON.parse(e.data); - console.log("[SSE CLIENT] message received | activeRequests:", data.activeRequests?.length || 0, "| providers:", data.activeRequests?.map(r => r.provider)); - setStats(data); + // Only update real-time fields from SSE, keep filtered stats intact + setStats((prev) => prev ? { + ...prev, + activeRequests: data.activeRequests, + recentRequests: data.recentRequests, + errorProvider: data.errorProvider, + pending: data.pending, + } : data); setLoading(false); } catch (err) { console.error("[SSE CLIENT] parse error:", err); } }; - es.onerror = (e) => { - console.error("[SSE CLIENT] error | readyState:", es.readyState, e); - setLoading(false); - }; + es.onerror = () => setLoading(false); - return () => { - console.log("[SSE CLIENT] closing"); - es.close(); - }; + return () => es.close(); }, []); const toggleSort = useCallback((tableType, field) => { @@ -357,6 +381,25 @@ export default function UsageStats() { return (
+ {/* Period selector */} +
+
+ {PERIODS.map((p) => ( + + ))} +
+ {fetching && ( + progress_activity + )} +
+ {/* Overview cards */} {loading ? spinner : } @@ -373,8 +416,8 @@ export default function UsageStats() {
)} - {/* Token / Cost chart */} - {loading ? spinner : } + {/* Token / Cost chart - sync period */} + {loading ? spinner : } {/* Table with dropdown selector */}