Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions dashboard/src/main.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ import {
WebPushControl,
WebhookHookDetailPage,
WebhookPage,
api,
buildJobQuery,
buildKnowledgeQuery,
changelogMarkdown,
createUnauthorizedHandler,
formatRuntimeUsageSeconds,
groupSessionEvents,
groupTranscriptEntries,
Expand Down Expand Up @@ -133,6 +135,30 @@ function sampleSloMetrics() {
}

describe("dashboard routing and API query helpers", () => {
it("starts only one OAuth login when concurrent API requests find an expired session", async () => {
const redirectToLogin = vi.fn();
const handleUnauthorized = createUnauthorizedHandler(redirectToLogin);
const fetchMock = vi.fn().mockResolvedValue(new Response(null, {
status: 401,
statusText: "Unauthorized",
}));
vi.stubGlobal("fetch", fetchMock);

const results = await Promise.allSettled([
api("/api/me", undefined, handleUnauthorized),
api("/api/status", undefined, handleUnauthorized),
]);

expect(redirectToLogin).toHaveBeenCalledOnce();
expect(redirectToLogin).toHaveBeenCalledWith("/auth/login");
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(results).toEqual([
expect.objectContaining({ status: "rejected", reason: expect.objectContaining({ message: "401 Unauthorized" }) }),
expect.objectContaining({ status: "rejected", reason: expect.objectContaining({ message: "401 Unauthorized" }) }),
]);
vi.unstubAllGlobals();
});

it("builds trimmed job queries and preserves the requested limit", () => {
expect(
buildJobQuery(
Expand Down
23 changes: 22 additions & 1 deletion dashboard/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -763,10 +763,29 @@ function cn(...values: Array<string | false | null | undefined>) {
return twMerge(clsx(values));
}

async function api<T>(path: string, init?: RequestInit): Promise<T> {
function redirectToLogin(path: string) {
window.location.replace(path);
}

function createUnauthorizedHandler(redirect = redirectToLogin) {
let redirectStarted = false;
return () => {
if (redirectStarted) return;
redirectStarted = true;
redirect("/auth/login");
};
}

// Each login replaces the OAuth state cookie, so the page must start this flow only once.
const handleUnauthorized = createUnauthorizedHandler();

async function api<T>(path: string, init?: RequestInit, onUnauthorized = handleUnauthorized): Promise<T> {
const headers = new Headers(init?.headers);
headers.set("Accept", "application/json");
const response = await fetch(path, { ...init, headers });
if (response.status === 401) {
onUnauthorized();
}
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
Expand Down Expand Up @@ -5541,6 +5560,8 @@ export {
webhookTimeseriesPath,
WebhookPage,
WebhookHookDetailPage,
api,
createUnauthorizedHandler,
};

const root = document.getElementById("root");
Expand Down

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/github_agent_bridge/dashboard_static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GitHub Agent Bridge Dashboard</title>
<script type="module" crossorigin src="/assets/index-j9YYBdoS.js"></script>
<script type="module" crossorigin src="/assets/index-CCJFb6gR.js"></script>
<link rel="modulepreload" crossorigin href="/assets/charts-DntrVYl4.js">
<link rel="stylesheet" crossorigin href="/assets/index-wGNxHObK.css">
</head>
Expand Down
Loading