"use client"; import { useTranslations } from "next-intl"; import { useEffect, useState, useCallback } from "react"; interface DailyUsage { date: string; inputTokens: number; outputTokens: number; spend: number; } interface UsageData { month: string; currentPeriod: { inputTokens: number; outputTokens: number; totalSpend: number; requestCount: number; }; budget: { maxBudget: number | null; spend: number; remaining: number | null }; rateLimits: { rpm: number | null; tpm: number | null }; dailyUsage: DailyUsage[]; } function fmt(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(0)}k`; return n.toString(); } function usd(n: number): string { return `$${n.toFixed(4)}`; } function getCurrentMonth(): string { const now = new Date(); return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`; } function shiftMonth(month: string, delta: number): string { const [y, m] = month.split("-").map(Number); const d = new Date(y, m - 1 + delta, 1); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`; } function formatMonth(month: string, locale: string): string { const [y, m] = month.split("-").map(Number); return new Date(y, m - 1).toLocaleDateString(locale, { year: "numeric", month: "long" }); } function UsageChart({ data }: { data: DailyUsage[] }) { if (!data.length) return null; const maxTokens = Math.max(...data.map((d) => d.inputTokens + d.outputTokens), 1); const barW = Math.max(4, Math.floor(600 / data.length) - 2); const h = 120; return (
{data.map((d, i) => { const total = d.inputTokens + d.outputTokens; const totalH = (total / maxTokens) * h; const inputH = (d.inputTokens / maxTokens) * h; const x = i * (barW + 2); return ( {d.date}: {fmt(d.inputTokens)} in / {fmt(d.outputTokens)} out — {usd(d.spend)} {i % 7 === 0 && ( {d.date.slice(8)} )} ); })}
Input Output
); } /** * Usage display widget. * * Pass `tenant=` for the canonical path — works for both * customers and admins, the API resolves team+alias from the tenant * CR's status. The visibility check on the API ensures users can't * query tenants they shouldn't see. * * `teamId`/`keyAlias` remain available as a platform-admin escape * hatch for cross-org debugging, but the tenant-detail and dashboard * paths should always use `tenant`. * * Bug 19 fix: previous version omitted both props for customer * sessions, expecting the API to "figure it out". The API's fallback * was "first visible tenant", which meant siblings in the same org * showed identical numbers regardless of which detail page was open. * Now the page passes the tenant name explicitly; no fallback exists. */ export function UsageDisplay({ tenant, teamId, keyAlias, }: { tenant?: string | null; teamId?: string | null; keyAlias?: string | null; }) { const t = useTranslations("usage"); const [month, setMonth] = useState(getCurrentMonth); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const isCurrentMonth = month === getCurrentMonth(); const fetchUsage = useCallback(() => { setLoading(true); setError(null); const params = new URLSearchParams({ month }); if (tenant) { params.set("tenant", tenant); } else if (teamId) { // Admin escape hatch — only honoured by the API when the // viewer is platform-role. params.set("teamId", teamId); if (keyAlias) params.set("keyAlias", keyAlias); } fetch(`/api/usage?${params}`) .then((res) => { if (!res.ok) throw new Error(`${res.status}`); return res.json(); }) .then(setData) .catch((e) => setError(e.message)) .finally(() => setLoading(false)); }, [tenant, teamId, keyAlias, month]); useEffect(() => { fetchUsage(); }, [fetchUsage]); return (
{/* Month selector */}
{formatMonth(month, "en")}
{loading ? (
) : error || !data ? (

{error || t("noData")}

) : ( <>

{t("dailyBreakdown")}

{data.currentPeriod.requestCount} {t("requests")}
)}
); } function StatCard({ label, value, accent }: { label: string; value: string; accent?: boolean }) { return (
{label}
{value}
); }