| ... | ... | @@ -68,12 +68,15 @@ export function UsagePage() { |
| 68 | 68 | : enumerateHourStarts(window.sinceTime, window.untilTime), |
| 69 | 69 | [window.sinceTime, window.untilTime], |
| 70 | 70 | ); |
| 71 | | // Newest first: the window can run 90 periods, so the interesting end |
| 72 | | // belongs at the top of the table. |
| 73 | | const breakdownPeriods = useMemo<readonly (DailyTotals | HourlyTotals)[]>( |
| 74 | | () => (isPast24Hours ? merged.hourly : merged.daily).toReversed(), |
| 75 | | [isPast24Hours, merged.daily, merged.hourly], |
| 76 | | ); |
| 71 | // The hourly window is small enough to render every period: the table then |
| 72 | // reads chronologically like the chart, instead of jumping between the hours |
| 73 | // that happened to have activity. Daily windows can run 90 periods, so those |
| 74 | // stay newest-first with the interesting end on top. |
| 75 | const breakdownPeriods = useMemo<readonly (DailyTotals | HourlyTotals)[]>(() => { |
| 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]); |
| 77 | 80 | |
| 78 | 81 | const selectWindow = (days: number) => { |
| 79 | 82 | setWindowSelection({ |
| ... | ... | @@ -435,6 +438,17 @@ export function UsagePage() { |
| 435 | 438 | ); |
| 436 | 439 | } |
| 437 | 440 | |
| 441 | /** A zero-filled hourly period so the breakdown lists every hour in the window. */ |
| 442 | function 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 | 452 | /** Brand mark for the harness a row belongs to. */ |
| 439 | 453 | function ProviderMark({ |
| 440 | 454 | provider, |