Skip to content

Commit 30c47b2

Browse files
authored
Gate claude_billing behind login + clarify estimated costs (#7960)
## Summary - Gate the `claude_billing` page behind GitHub authentication (requires write permissions to pytorch/pytorch, matching the flambeau/TorchAgent pattern) - Rename heading to **Claude Code Review — Estimated Costs** to clarify these are estimates - Add disclaimer explaining costs are calculated from GitHub Actions token counts × Anthropic list prices Context: Team discussion concluded the billing page should be semi-private (behind login) since it shows per-user token usage data. ## Test plan - [ ] Visit `/claude_billing` while logged out → see Authentication Required + Sign In button - [ ] Sign in with GitHub account WITHOUT write access → see Insufficient Permissions - [ ] Sign in with write access → see the Grafana dashboard - [ ] Verify heading reads Claude Code Review — Estimated Costs - [ ] Verify disclaimer text is shown above the dashboard
1 parent c6ad3e8 commit 30c47b2

2 files changed

Lines changed: 196 additions & 1 deletion

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// CloudFront Function (viewer-response) for distribution E2Z345QRXN6Y77
2+
// Strips frame-ancestors from CSP and x-frame-options to allow iframe embedding
3+
// on hud.pytorch.org.
4+
//
5+
// Deploy: CloudFront Console > Functions > Create > paste this code >
6+
// Associate with distribution E2Z345QRXN6Y77 (disz2yd9jqnwc.cloudfront.net),
7+
// event type: viewer-response, cache behavior: Default (*).
8+
function handler(event) {
9+
var response = event.response;
10+
var headers = response.headers;
11+
12+
// Remove x-frame-options (legacy frame-blocking header)
13+
delete headers["x-frame-options"];
14+
15+
// Rewrite CSP: replace frame-ancestors 'none' with hud.pytorch.org
16+
if (headers["content-security-policy"]) {
17+
headers["content-security-policy"].value =
18+
headers["content-security-policy"].value.replace(
19+
/frame-ancestors\s+'none'/,
20+
"frame-ancestors 'self' https://hud.pytorch.org"
21+
);
22+
}
23+
24+
return response;
25+
}

torchci/pages/claude_billing.tsx

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,50 @@
1-
import { Box, Button, Container, Typography } from "@mui/material";
1+
import {
2+
Box,
3+
Button,
4+
CircularProgress,
5+
Container,
6+
Typography,
7+
} from "@mui/material";
8+
import { signIn, useSession } from "next-auth/react";
29
import Head from "next/head";
10+
import { useCallback, useEffect, useState } from "react";
311
import { useDarkMode } from "../lib/DarkModeContext";
412

513
const CLAUDE_BILLING_DASHBOARD_ID = "9127e39ec5a7410ebb419fac06a08ca0";
614

715
export default function ClaudeBillingPage() {
816
const { themeMode, darkMode } = useDarkMode();
17+
const session = useSession();
18+
const [permissionState, setPermissionState] = useState<
19+
"unchecked" | "checking" | "sufficient" | "insufficient"
20+
>("unchecked");
21+
22+
const checkUserPermissions = useCallback(async () => {
23+
if (!session.data?.user || permissionState !== "unchecked") return;
24+
25+
setPermissionState("checking");
26+
try {
27+
const response = await fetch("/api/torchagent-check-permissions", {
28+
method: "GET",
29+
headers: { "Content-Type": "application/json" },
30+
});
31+
32+
if (response.ok) {
33+
setPermissionState("sufficient");
34+
} else {
35+
setPermissionState("insufficient");
36+
}
37+
} catch (error) {
38+
console.error("Error checking permissions:", error);
39+
setPermissionState("insufficient");
40+
}
41+
}, [session.data?.user, permissionState]);
42+
43+
useEffect(() => {
44+
if (session.data?.user && permissionState === "unchecked") {
45+
checkUserPermissions();
46+
}
47+
}, [session.data?.user, permissionState, checkUserPermissions]);
948

1049
let chartTheme = "light";
1150
if (themeMode === "system") {
@@ -17,6 +56,137 @@ export default function ClaudeBillingPage() {
1756
const dashboardUrl = `https://disz2yd9jqnwc.cloudfront.net/public-dashboards/${CLAUDE_BILLING_DASHBOARD_ID}?theme=${chartTheme}`;
1857
const grafanaUrl = `https://pytorchci.grafana.net/public-dashboards/${CLAUDE_BILLING_DASHBOARD_ID}`;
1958

59+
// Loading state
60+
if (session.status === "loading" || permissionState === "checking") {
61+
return (
62+
<>
63+
<Head>
64+
<title>Claude Code Review Billing - PyTorch CI HUD</title>
65+
</Head>
66+
<Container
67+
maxWidth={false}
68+
sx={{
69+
py: 10,
70+
display: "flex",
71+
flexDirection: "column",
72+
alignItems: "center",
73+
}}
74+
>
75+
<CircularProgress />
76+
<Typography variant="h6" sx={{ mt: 2 }}>
77+
{session.status === "loading"
78+
? "Checking authentication..."
79+
: "Checking permissions..."}
80+
</Typography>
81+
</Container>
82+
</>
83+
);
84+
}
85+
86+
// Unauthenticated
87+
if (
88+
session.status === "unauthenticated" ||
89+
!session.data?.user ||
90+
!(session.data as any)?.accessToken
91+
) {
92+
return (
93+
<>
94+
<Head>
95+
<title>Claude Code Review Billing - PyTorch CI HUD</title>
96+
</Head>
97+
<Container
98+
maxWidth={false}
99+
sx={{
100+
py: 10,
101+
display: "flex",
102+
flexDirection: "column",
103+
alignItems: "center",
104+
}}
105+
>
106+
<Typography variant="h4" gutterBottom>
107+
Authentication Required
108+
</Typography>
109+
<Typography variant="body1" sx={{ mb: 2 }}>
110+
You must be signed in with write permissions to pytorch/pytorch to
111+
view Claude billing data.
112+
</Typography>
113+
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
114+
Please sign in with GitHub to continue.
115+
</Typography>
116+
<Button
117+
variant="contained"
118+
color="primary"
119+
size="large"
120+
onClick={() => signIn()}
121+
sx={{ minWidth: "200px" }}
122+
>
123+
Sign In
124+
</Button>
125+
</Container>
126+
</>
127+
);
128+
}
129+
130+
// Insufficient permissions
131+
if (permissionState === "insufficient") {
132+
return (
133+
<>
134+
<Head>
135+
<title>Claude Code Review Billing - PyTorch CI HUD</title>
136+
</Head>
137+
<Container
138+
maxWidth={false}
139+
sx={{
140+
py: 10,
141+
display: "flex",
142+
flexDirection: "column",
143+
alignItems: "center",
144+
}}
145+
>
146+
<Typography variant="h4" gutterBottom>
147+
Insufficient Permissions
148+
</Typography>
149+
<Typography variant="body1" sx={{ mb: 2 }}>
150+
You are signed in as{" "}
151+
<strong>{session.data.user.name || session.data.user.email}</strong>
152+
, but you need write permissions to pytorch/pytorch to view this
153+
page.
154+
</Typography>
155+
<Box
156+
sx={{
157+
display: "flex",
158+
gap: 2,
159+
justifyContent: "center",
160+
flexWrap: "wrap",
161+
}}
162+
>
163+
<Button
164+
variant="contained"
165+
color="primary"
166+
component="a"
167+
href="https://forms.gle/SoLgaCucjJqc6F647"
168+
target="_blank"
169+
rel="noopener noreferrer"
170+
>
171+
Request Access
172+
</Button>
173+
<Button
174+
variant="outlined"
175+
color="secondary"
176+
onClick={() => {
177+
setPermissionState("unchecked");
178+
checkUserPermissions();
179+
}}
180+
>
181+
Try Again
182+
</Button>
183+
</Box>
184+
</Container>
185+
</>
186+
);
187+
}
188+
189+
// Authorized — show the dashboard
20190
return (
21191
<>
22192
<Head>

0 commit comments

Comments
 (0)