authorgravatar for 269035359+t3-code[bot]@users.noreply.github.comt3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> 2026-08-20 16:18:36-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-20 16:18:36-04:00
logaa17ec6e7d77d2bcb8f86720e41b7e5402d6b74c
tree70761fb86d6e040d904aa94b2661aa6ef35ef916
parent21e80a06364898b8ecffad8aeddcb5634170260b
signature Signed by PGP key B5690EEEBB952194

revert(web): restore sparse hourly usage breakdown (#7718)

Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>

2 files changed, 116 insertions(+), 20 deletions(-)

apps/web/src/components/usage/UsagePage.test.tsx created+110
...@@ -0,0 +1,110 @@
1import { USAGE_CONTRACT_VERSION } from "@t3tools/contracts";
2import { mergeUsage } from "@t3tools/shared/usageMerge";
3import { renderToStaticMarkup } from "react-dom/server";
4import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
5
6const testState = vi.hoisted(() => ({
7 useUsage: vi.fn(),
8}));
9
10vi.mock("react", async (importOriginal) => {
11 const actual = await importOriginal<typeof import("react")>();
12 return {
13 ...actual,
14 useState: vi.fn((initial: unknown) => [
15 typeof initial === "function"
16 ? {
17 days: 1,
18 window: {
19 sinceDay: "2026-08-10",
20 untilDay: "2026-08-11",
21 timeZone: "UTC",
22 resolution: "hour",
23 sinceTime: "2026-08-10T12:37:00.000Z",
24 untilTime: "2026-08-11T12:37:00.000Z",
25 },
26 }
27 : initial === "model"
28 ? "time"
29 : initial,
30 vi.fn(),
31 ]),
32 };
33});
34
35vi.mock("../../env", () => ({ isElectron: false }));
36vi.mock("../../state/usage", () => ({ useUsage: testState.useUsage }));
37vi.mock("../ui/button", () => ({ Button: "button" }));
38vi.mock("../ui/scroll-area", () => ({ ScrollArea: "div" }));
39vi.mock("../ui/select", () => ({
40 Select: "div",
41 SelectItem: "div",
42 SelectPopup: "div",
43 SelectTrigger: "div",
44 SelectValue: "div",
45}));
46vi.mock("../ui/sidebar", () => ({ SidebarInset: "div" }));
47vi.mock("../ui/toggle-group", () => ({ Toggle: "button", ToggleGroup: "div" }));
48vi.mock("../WorkspaceBreadcrumb", () => ({
49 WorkspaceBreadcrumb: "div",
50 WorkspaceBreadcrumbItem: "div",
51 WorkspaceBreadcrumbSeparator: "span",
52}));
53vi.mock("../WorkspacePageContainer", () => ({ WorkspacePageContainer: "main" }));
54vi.mock("../WorkspacePageHeader", () => ({ WorkspacePageHeader: "header" }));
55vi.mock("./UsageProviderChart", () => ({ UsageProviderChart: "div" }));
56vi.mock("./usageProviders", () => ({
57 PROVIDER_ORDER: ["codex", "claude"],
58 PROVIDER_PRESENTATION: {
59 codex: { color: "white", label: "Codex", mark: "span" },
60 claude: { color: "orange", label: "Claude Code", mark: "span" },
61 },
62}));
63
64import { UsagePage } from "./UsagePage";
65
66const providerTotals = (codex: number, claude: number) =>
67 new Map([
68 ["codex", { costUsd: codex, totalTokens: codex * 1_000 }],
69 ["claude", { costUsd: claude, totalTokens: claude * 1_000 }],
70 ] as const);
71
72beforeEach(() => {
73 testState.useUsage.mockReturnValue({
74 merged: {
75 ...mergeUsage([], USAGE_CONTRACT_VERSION),
76 hourly: [
77 {
78 day: "2026-08-10",
79 hourStart: "2026-08-10T13:37:00.000Z",
80 costUsd: 13,
81 totalTokens: 13_000,
82 byProvider: providerTotals(7, 6),
83 },
84 {
85 day: "2026-08-11",
86 hourStart: "2026-08-11T11:37:00.000Z",
87 costUsd: 11,
88 totalTokens: 11_000,
89 byProvider: providerTotals(6, 5),
90 },
91 ],
92 },
93 environments: [],
94 isPending: false,
95 isPartial: false,
96 refresh: vi.fn(),
97 });
98});
99
100describe("UsagePage hourly breakdown", () => {
101 it("keeps recent activity visible first without empty hourly rows", () => {
102 const markup = renderToStaticMarkup(<UsagePage />);
103 const body = markup.match(/<tbody>(.*?)<\/tbody>/)?.[1] ?? "";
104
105 expect(body.match(/<tr/g)).toHaveLength(2);
106 expect(body).toContain("$11.00");
107 expect(body).toContain("$13.00");
108 expect(body.indexOf("$11.00")).toBeLessThan(body.indexOf("$13.00"));
109 });
110});
apps/web/src/components/usage/UsagePage.tsx+6-20
...@@ -68,15 +68,12 @@ export function UsagePage() {...@@ -68,15 +68,12 @@ 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 // The hourly window is small enough to render every period: the table then71 // Newest first: the window can run 90 periods, so the interesting end
72 // reads chronologically like the chart, instead of jumping between the hours72 // belongs at the top of the table.
73 // that happened to have activity. Daily windows can run 90 periods, so those73 const breakdownPeriods = useMemo<readonly (DailyTotals | HourlyTotals)[]>(
74 // stay newest-first with the interesting end on top.74 () => (isPast24Hours ? merged.hourly : merged.daily).toReversed(),
75 const breakdownPeriods = useMemo<readonly (DailyTotals | HourlyTotals)[]>(() => {75 [isPast24Hours, merged.daily, merged.hourly],
76 if (!isPast24Hours) return merged.daily.toReversed();76 );
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]);
8077
81 const selectWindow = (days: number) => {78 const selectWindow = (days: number) => {
82 setWindowSelection({79 setWindowSelection({
...@@ -438,17 +435,6 @@ export function UsagePage() {...@@ -438,17 +435,6 @@ export function UsagePage() {
438 );435 );
439}436}
440437
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
452/** Brand mark for the harness a row belongs to. */438/** Brand mark for the harness a row belongs to. */
453function ProviderMark({439function ProviderMark({
454 provider,440 provider,