authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-08 21:31:20-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-08 21:31:20-04:00
log70c423a5e48ab34d8b1a033e798ab378b48dde5d
tree0a8b96442e63850e5cc97da4cb21a86931c81c06
parent89c320df0b0884a8c4df1cf596564c6bc725eb54
signature Signed by PGP key B5690EEEBB952194

fix(web): usage page loses the cost quality panel, gains a back button (#5756)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

1 files changed, 128 insertions(+), 141 deletions(-)

apps/web/src/components/usage/UsagePage.tsx+128-141
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import type { UsageProviderKind } from "@t3tools/contracts";1import type { UsageProviderKind } from "@t3tools/contracts";
2import { RefreshCwIcon } from "lucide-react";2import { useCanGoBack, useNavigate, useRouter } from "@tanstack/react-router";
3import { ArrowLeftIcon, RefreshCwIcon } from "lucide-react";
3import { useMemo, useState } from "react";4import { useMemo, useState } from "react";
45
5import { cn } from "../../lib/utils";6import { cn } from "../../lib/utils";
...@@ -27,6 +28,9 @@ export function UsagePage() {...@@ -27,6 +28,9 @@ export function UsagePage() {
27 const [windowDays, setWindowDays] = useState<number>(30);28 const [windowDays, setWindowDays] = useState<number>(30);
28 const [metric, setMetric] = useState<UsageChartMetric>("cost");29 const [metric, setMetric] = useState<UsageChartMetric>("cost");
29 const [breakdown, setBreakdown] = useState<"model" | "day">("model");30 const [breakdown, setBreakdown] = useState<"model" | "day">("model");
31 const canGoBack = useCanGoBack();
32 const navigate = useNavigate();
33 const router = useRouter();
3034
31 // Recomputed only when the window length changes, so a re-render does not35 // Recomputed only when the window length changes, so a re-render does not
32 // shift the range and refetch every environment.36 // shift the range and refetch every environment.
...@@ -57,11 +61,27 @@ export function UsagePage() {...@@ -57,11 +61,27 @@ export function UsagePage() {
57 <ScrollArea className="h-full">61 <ScrollArea className="h-full">
58 <div className="mx-auto flex w-full max-w-6xl flex-col gap-8 px-6 py-6">62 <div className="mx-auto flex w-full max-w-6xl flex-col gap-8 px-6 py-6">
59 <header className="flex flex-wrap items-start justify-between gap-4">63 <header className="flex flex-wrap items-start justify-between gap-4">
60 <div className="flex flex-col gap-1">64 <div className="flex items-start gap-3">
61 <h1 className="text-2xl font-semibold text-foreground">Usage</h1>65 <button
62 <p className="text-sm text-muted-foreground">66 type="button"
63 {formatDayShort(window.sinceDay)} to {formatDayShort(window.untilDay)}67 aria-label="Back"
64 </p>68 onClick={() => {
69 if (canGoBack) {
70 router.history.back();
71 return;
72 }
73 void navigate({ to: "/" });
74 }}
75 className="mt-1 cursor-pointer rounded-md border border-border p-2 text-muted-foreground hover:text-foreground"
76 >
77 <ArrowLeftIcon className="size-3.5" />
78 </button>
79 <div className="flex flex-col gap-1">
80 <h1 className="text-2xl font-semibold text-foreground">Usage</h1>
81 <p className="text-sm text-muted-foreground">
82 {formatDayShort(window.sinceDay)} to {formatDayShort(window.untilDay)}
83 </p>
84 </div>
65 </div>85 </div>
66 <div className="flex items-center gap-2">86 <div className="flex items-center gap-2">
67 <div className="flex overflow-hidden rounded-md border border-border">87 <div className="flex overflow-hidden rounded-md border border-border">
...@@ -222,140 +242,116 @@ export function UsagePage() {...@@ -222,140 +242,116 @@ export function UsagePage() {
222 />242 />
223 </section>243 </section>
224244
225 <section className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,18rem)]">245 <section className="flex flex-col gap-3">
226 <div className="flex flex-col gap-3">246 <div className="flex items-center justify-between gap-3">
227 <div className="flex items-center justify-between gap-3">247 <h2 className="text-sm font-medium text-foreground">Breakdown</h2>
228 <h2 className="text-sm font-medium text-foreground">Breakdown</h2>248 <div className="flex overflow-hidden rounded-md border border-border">
229 <div className="flex overflow-hidden rounded-md border border-border">249 {(["model", "day"] as const).map((option) => (
230 {(["model", "day"] as const).map((option) => (250 <button
231 <button251 key={option}
232 key={option}252 type="button"
233 type="button"253 onClick={() => setBreakdown(option)}
234 onClick={() => setBreakdown(option)}254 className={cn(
235 className={cn(255 "cursor-pointer px-2.5 py-1 text-[10px] tracking-wide uppercase",
236 "cursor-pointer px-2.5 py-1 text-[10px] tracking-wide uppercase",256 option === breakdown
237 option === breakdown257 ? "bg-muted text-foreground"
238 ? "bg-muted text-foreground"258 : "text-muted-foreground hover:text-foreground",
239 : "text-muted-foreground hover:text-foreground",259 )}
240 )}260 >
241 >261 {option}
242 {option}262 </button>
243 </button>263 ))}
244 ))}
245 </div>
246 </div>264 </div>
265 </div>
247266
248 {breakdown === "model" ? (267 {breakdown === "model" ? (
249 <table className="w-full text-sm">268 <table className="w-full text-sm">
250 <thead>269 <thead>
251 <tr className="border-b border-border text-left text-xs text-muted-foreground">270 <tr className="border-b border-border text-left text-xs text-muted-foreground">
252 <th className="py-2 font-normal">Model</th>271 <th className="py-2 font-normal">Model</th>
253 <th className="py-2 text-right font-normal">Cost</th>272 <th className="py-2 text-right font-normal">Cost</th>
254 <th className="py-2 text-right font-normal">Share</th>273 <th className="py-2 text-right font-normal">Share</th>
255 <th className="py-2 text-right font-normal">Tokens</th>274 <th className="py-2 text-right font-normal">Tokens</th>
275 </tr>
276 </thead>
277 <tbody>
278 {merged.models.length === 0 ? (
279 <tr>
280 <td colSpan={4} className="py-6 text-center text-muted-foreground">
281 No activity in this window.
282 </td>
256 </tr>283 </tr>
257 </thead>284 ) : (
258 <tbody>285 merged.models.map((model) => (
259 {merged.models.length === 0 ? (286 <tr
260 <tr>287 key={`${model.provider}:${model.model}`}
261 <td colSpan={4} className="py-6 text-center text-muted-foreground">288 className="border-b border-border/50"
262 No activity in this window.289 >
290 <td className="py-2 text-foreground">
291 <span className="flex items-center gap-2">
292 <ProviderMark provider={model.provider} className="size-3.5" />
293 {model.model}
294 </span>
295 </td>
296 <td className="py-2 text-right text-foreground tabular-nums">
297 {formatUsd(model.costUsd)}
298 </td>
299 <td className="py-2 text-right text-muted-foreground tabular-nums">
300 {formatPercent(model.costShare)}
301 </td>
302 <td className="py-2 text-right text-muted-foreground tabular-nums">
303 {formatTokens(model.totalTokens)}
263 </td>304 </td>
264 </tr>305 </tr>
265 ) : (306 ))
266 merged.models.map((model) => (307 )}
267 <tr308 </tbody>
268 key={`${model.provider}:${model.model}`}309 </table>
269 className="border-b border-border/50"310 ) : (
270 >311 <table className="w-full text-sm">
271 <td className="py-2 text-foreground">312 <thead>
272 <span className="flex items-center gap-2">313 <tr className="border-b border-border text-left text-xs text-muted-foreground">
273 <ProviderMark provider={model.provider} className="size-3.5" />314 <th className="py-2 font-normal">Day</th>
274 {model.model}315 {PROVIDER_ORDER.map((provider) => (
275 </span>316 <th key={provider} className="py-2 text-right font-normal">
276 </td>317 {PROVIDER_LABEL[provider]}
277 <td className="py-2 text-right text-foreground tabular-nums">318 </th>
278 {formatUsd(model.costUsd)}319 ))}
279 </td>320 <th className="py-2 text-right font-normal">Total</th>
280 <td className="py-2 text-right text-muted-foreground tabular-nums">321 <th className="py-2 text-right font-normal">Tokens</th>
281 {formatPercent(model.costShare)}322 </tr>
282 </td>323 </thead>
283 <td className="py-2 text-right text-muted-foreground tabular-nums">324 <tbody>
284 {formatTokens(model.totalTokens)}325 {recentDays.length === 0 ? (
285 </td>326 <tr>
286 </tr>327 <td colSpan={5} className="py-6 text-center text-muted-foreground">
287 ))328 No activity in this window.
288 )}329 </td>
289 </tbody>
290 </table>
291 ) : (
292 <table className="w-full text-sm">
293 <thead>
294 <tr className="border-b border-border text-left text-xs text-muted-foreground">
295 <th className="py-2 font-normal">Day</th>
296 {PROVIDER_ORDER.map((provider) => (
297 <th key={provider} className="py-2 text-right font-normal">
298 {PROVIDER_LABEL[provider]}
299 </th>
300 ))}
301 <th className="py-2 text-right font-normal">Total</th>
302 <th className="py-2 text-right font-normal">Tokens</th>
303 </tr>330 </tr>
304 </thead>331 ) : (
305 <tbody>332 recentDays.map((day) => (
306 {recentDays.length === 0 ? (333 <tr key={day.day} className="border-b border-border/50">
307 <tr>334 <td className="py-2 text-foreground">{formatDayShort(day.day)}</td>
308 <td colSpan={5} className="py-6 text-center text-muted-foreground">335 {PROVIDER_ORDER.map((provider) => (
309 No activity in this window.336 <td
337 key={provider}
338 className="py-2 text-right text-muted-foreground tabular-nums"
339 >
340 {formatUsd(day.byProvider.get(provider)?.costUsd ?? 0)}
341 </td>
342 ))}
343 <td className="py-2 text-right text-foreground tabular-nums">
344 {formatUsd(day.costUsd)}
345 </td>
346 <td className="py-2 text-right text-muted-foreground tabular-nums">
347 {formatTokens(day.totalTokens)}
310 </td>348 </td>
311 </tr>349 </tr>
312 ) : (350 ))
313 recentDays.map((day) => (351 )}
314 <tr key={day.day} className="border-b border-border/50">352 </tbody>
315 <td className="py-2 text-foreground">{formatDayShort(day.day)}</td>353 </table>
316 {PROVIDER_ORDER.map((provider) => (354 )}
317 <td
318 key={provider}
319 className="py-2 text-right text-muted-foreground tabular-nums"
320 >
321 {formatUsd(day.byProvider.get(provider)?.costUsd ?? 0)}
322 </td>
323 ))}
324 <td className="py-2 text-right text-foreground tabular-nums">
325 {formatUsd(day.costUsd)}
326 </td>
327 <td className="py-2 text-right text-muted-foreground tabular-nums">
328 {formatTokens(day.totalTokens)}
329 </td>
330 </tr>
331 ))
332 )}
333 </tbody>
334 </table>
335 )}
336 </div>
337
338 <div className="flex flex-col gap-3">
339 <h2 className="text-sm font-medium text-foreground">Cost quality</h2>
340 <dl className="flex flex-col">
341 <QualityRow
342 label="Provider reported"
343 value={formatPercent(merged.costQuality.providerReportedShare)}
344 />
345 <QualityRow
346 label="Model priced"
347 value={formatPercent(merged.costQuality.modelPricedShare)}
348 />
349 <QualityRow
350 label="Unpriced"
351 value={formatPercent(merged.costQuality.unpricedShare)}
352 />
353 <QualityRow
354 label="Cache savings"
355 value={formatUsd(merged.costQuality.cacheSavingsUsd)}
356 />
357 </dl>
358 </div>
359 </section>355 </section>
360 </>356 </>
361 )}357 )}
...@@ -394,15 +390,6 @@ function Metric({...@@ -394,15 +390,6 @@ function Metric({
394 );390 );
395}391}
396392
397function QualityRow({ label, value }: { readonly label: string; readonly value: string }) {
398 return (
399 <div className="flex items-center justify-between border-b border-border/50 py-2 text-sm">
400 <dt className="text-muted-foreground">{label}</dt>
401 <dd className="text-foreground tabular-nums">{value}</dd>
402 </div>
403 );
404}
405
406/**393/**
407 * Says plainly when the totals are incomplete: an environment still answering,394 * Says plainly when the totals are incomplete: an environment still answering,
408 * one that failed, or one whose transcripts another environment already395 * one that failed, or one whose transcripts another environment already