authorgravatar for luis.wacker@pagar.meLuis Gustavo Couto Wacker <luis.wacker@pagar.me> 2026-08-19 19:26:35-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-19 18:26:35-04:00
log62654d279fcdd910bb755e106dfd51bc8940c263
treeed96b189e27e63975a297ed4a3e51c5170633db6
parenta850895f6833b99d90fc6c50192b5eaa4966d5c7
signature Signed by PGP key B5690EEEBB952194

fix(web): usage hourly breakdown lists every hour chronologically (#7595)


1 files changed, 20 insertions(+), 6 deletions(-)

apps/web/src/components/usage/UsagePage.tsx+20-6
...@@ -68,12 +68,15 @@ export function UsagePage() {...@@ -68,12 +68,15 @@ export function UsagePage() {
68 : enumerateHourStarts(window.sinceTime, window.untilTime),68 : enumerateHourStarts(window.sinceTime, window.untilTime),
69 [window.sinceTime, window.untilTime],69 [window.sinceTime, window.untilTime],
70 );70 );
71 // Newest first: the window can run 90 periods, so the interesting end71 // The hourly window is small enough to render every period: the table then
72 // belongs at the top of the table.72 // reads chronologically like the chart, instead of jumping between the hours
73 const breakdownPeriods = useMemo<readonly (DailyTotals | HourlyTotals)[]>(73 // that happened to have activity. Daily windows can run 90 periods, so those
74 () => (isPast24Hours ? merged.hourly : merged.daily).toReversed(),74 // stay newest-first with the interesting end on top.
75 [isPast24Hours, merged.daily, merged.hourly],75 const breakdownPeriods = useMemo<readonly (DailyTotals | HourlyTotals)[]>(() => {
76 );76 if (!isPast24Hours) return merged.daily.toReversed();
77 const byHour = new Map(merged.hourly.map((entry) => [entry.hourStart, entry]));
78 return hours.map((hourStart) => byHour.get(hourStart) ?? zeroHour(hourStart));
79 }, [isPast24Hours, merged.daily, merged.hourly, hours]);
7780
78 const selectWindow = (days: number) => {81 const selectWindow = (days: number) => {
79 setWindowSelection({82 setWindowSelection({
...@@ -435,6 +438,17 @@ export function UsagePage() {...@@ -435,6 +438,17 @@ export function UsagePage() {
435 );438 );
436}439}
437440
441/** A zero-filled hourly period so the breakdown lists every hour in the window. */
442function zeroHour(hourStart: string): HourlyTotals {
443 return {
444 day: "",
445 hourStart,
446 costUsd: 0,
447 totalTokens: 0,
448 byProvider: new Map<UsageProviderKind, { costUsd: number; totalTokens: number }>(),
449 };
450}
451
438/** Brand mark for the harness a row belongs to. */452/** Brand mark for the harness a row belongs to. */
439function ProviderMark({453function ProviderMark({
440 provider,454 provider,