diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json
index c6b7f43..6153ed9 100644
--- a/apps/dashboard/package.json
+++ b/apps/dashboard/package.json
@@ -26,13 +26,13 @@
"@diffkit/ui": "workspace:*",
"@pierre/diffs": "^1.1.12",
"@tailwindcss/vite": "^4.1.18",
- "@tanstack/react-devtools": "latest",
- "@tanstack/react-query": "latest",
- "@tanstack/react-router": "latest",
- "@tanstack/react-router-devtools": "latest",
- "@tanstack/react-router-ssr-query": "latest",
- "@tanstack/react-start": "latest",
- "@tanstack/router-plugin": "^1.132.0",
+ "@tanstack/react-devtools": "~0.10.2",
+ "@tanstack/react-query": "~5.97.0",
+ "@tanstack/react-router": "~1.168.13",
+ "@tanstack/react-router-devtools": "~1.166.11",
+ "@tanstack/react-router-ssr-query": "~1.166.10",
+ "@tanstack/react-start": "~1.167.23",
+ "@tanstack/router-plugin": "~1.167.12",
"agentation": "^3.0.2",
"better-auth": "^1.6.0",
"drizzle-orm": "^0.45.2",
@@ -47,7 +47,7 @@
"@biomejs/biome": "2.4.5",
"@cloudflare/workers-types": "^4.20260405.1",
"@diffkit/typescript-config": "workspace:*",
- "@tanstack/devtools-vite": "latest",
+ "@tanstack/devtools-vite": "~0.6.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.0",
"@types/node": "^22.10.2",
diff --git a/apps/dashboard/src/components/layouts/dashboard-layout.tsx b/apps/dashboard/src/components/layouts/dashboard-layout.tsx
index b780a99..fbb326b 100644
--- a/apps/dashboard/src/components/layouts/dashboard-layout.tsx
+++ b/apps/dashboard/src/components/layouts/dashboard-layout.tsx
@@ -49,18 +49,20 @@ export function DashboardLayout() {
...githubMyIssuesQueryOptions(scope),
enabled: hasMounted,
});
- const pullCount = pullsQuery.data
- ? pullsQuery.data.reviewRequested.length +
- pullsQuery.data.assigned.length +
- pullsQuery.data.authored.length +
- pullsQuery.data.mentioned.length +
- pullsQuery.data.involved.length
- : undefined;
- const issueCount = issuesQuery.data
- ? issuesQuery.data.assigned.length +
- issuesQuery.data.authored.length +
- issuesQuery.data.mentioned.length
- : undefined;
+ const pullCount =
+ hasMounted && pullsQuery.data
+ ? pullsQuery.data.reviewRequested.length +
+ pullsQuery.data.assigned.length +
+ pullsQuery.data.authored.length +
+ pullsQuery.data.mentioned.length +
+ pullsQuery.data.involved.length
+ : undefined;
+ const issueCount =
+ hasMounted && issuesQuery.data
+ ? issuesQuery.data.assigned.length +
+ issuesQuery.data.authored.length +
+ issuesQuery.data.mentioned.length
+ : undefined;
const tabsReady = hasMounted && Boolean(pullsQuery.data && issuesQuery.data);
useEffect(() => {
@@ -87,7 +89,9 @@ export function DashboardLayout() {
counts={{
pulls: pullCount,
issues: issueCount,
- reviews: pullsQuery.data?.reviewRequested.length,
+ reviews: hasMounted
+ ? pullsQuery.data?.reviewRequested.length
+ : undefined,
}}
/>
@@ -104,7 +108,9 @@ export function DashboardLayout() {
counts={{
pulls: pullCount,
issues: issueCount,
- reviews: pullsQuery.data?.reviewRequested.length,
+ reviews: hasMounted
+ ? pullsQuery.data?.reviewRequested.length
+ : undefined,
}}
/>
diff --git a/apps/dashboard/src/components/layouts/dashboard-topbar.tsx b/apps/dashboard/src/components/layouts/dashboard-topbar.tsx
index fc19067..aeb0b39 100644
--- a/apps/dashboard/src/components/layouts/dashboard-topbar.tsx
+++ b/apps/dashboard/src/components/layouts/dashboard-topbar.tsx
@@ -108,9 +108,23 @@ export function DashboardTopbar({
useEffect(() => {
if (!tabsReady) return;
- void Promise.allSettled(
- primaryNavRoutes.map((to) => routerRef.current.preloadRoute({ to })),
- );
+ // Preload routes serially to avoid a burst of concurrent server function
+ // RPCs that can overwhelm the Cloudflare Worker.
+ let cancelled = false;
+ (async () => {
+ for (const to of primaryNavRoutes) {
+ if (cancelled) break;
+ try {
+ await routerRef.current.preloadRoute({ to });
+ } catch {
+ // preload is best-effort
+ }
+ }
+ })();
+
+ return () => {
+ cancelled = true;
+ };
}, [tabsReady]);
function navigateToTab(tab: Tab | undefined) {
diff --git a/apps/dashboard/src/components/pulls/review/review-page.tsx b/apps/dashboard/src/components/pulls/review/review-page.tsx
index a9af473..c7d412c 100644
--- a/apps/dashboard/src/components/pulls/review/review-page.tsx
+++ b/apps/dashboard/src/components/pulls/review/review-page.tsx
@@ -87,7 +87,6 @@ function useIsDesktop() {
export function ReviewPage() {
const { user } = routeApi.useRouteContext();
- const loaderData = routeApi.useLoaderData();
const { owner, repo, pullId } = routeApi.useParams();
const pullNumber = Number(pullId);
const scope = { userId: user.id };
@@ -110,7 +109,6 @@ export function ReviewPage() {
refetchOnWindowFocus: false,
});
- const firstFilesPage = loaderData?.firstFilesPage ?? null;
const filesQuery = useInfiniteQuery({
queryKey: githubQueryKeys.pulls.files(scope, input),
initialPageParam: 1,
@@ -123,14 +121,6 @@ export function ReviewPage() {
},
}),
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
- ...(firstFilesPage
- ? {
- initialData: {
- pages: [firstFilesPage],
- pageParams: [1],
- },
- }
- : {}),
refetchOnMount: false,
refetchOnWindowFocus: false,
});
@@ -142,9 +132,8 @@ export function ReviewPage() {
refetchOnWindowFocus: false,
});
- const pr = pageQuery.data?.detail ?? loaderData?.pageData?.detail ?? null;
- const sidebarFiles =
- fileSummariesQuery.data ?? loaderData?.fileSummaries ?? [];
+ const pr = pageQuery.data?.detail ?? null;
+ const sidebarFiles = fileSummariesQuery.data ?? [];
const diffFiles = useMemo(
() => filesQuery.data?.pages.flatMap((page) => page.files) ?? [],
[filesQuery.data],
diff --git a/apps/dashboard/src/lib/auth-runtime.ts b/apps/dashboard/src/lib/auth-runtime.ts
index adb2343..b166e1c 100644
--- a/apps/dashboard/src/lib/auth-runtime.ts
+++ b/apps/dashboard/src/lib/auth-runtime.ts
@@ -12,6 +12,7 @@ import {
getGitHubAppUserAccessTokenByUserId,
getGitHubOAuthConfig,
} from "./github-app.server";
+import { configureGitHubRequestPolicies } from "./github-request-policy";
const authDb = drizzle(env.DB, { schema });
@@ -52,11 +53,15 @@ export async function getRequestSession() {
export async function getGitHubClientByUserId(
userId: string,
): Promise {
- return new Octokit({
+ const octokit = new Octokit({
auth: await getGitHubAccessTokenByUserId(userId),
retry: { enabled: false },
throttle: { enabled: false },
});
+
+ configureGitHubRequestPolicies(octokit);
+
+ return octokit;
}
export async function getGitHubAppUserClientByUserId(
@@ -67,9 +72,13 @@ export async function getGitHubAppUserClientByUserId(
return null;
}
- return new Octokit({
+ const octokit = new Octokit({
auth: token,
retry: { enabled: false },
throttle: { enabled: false },
});
+
+ configureGitHubRequestPolicies(octokit);
+
+ return octokit;
}
diff --git a/apps/dashboard/src/lib/github-app.server.ts b/apps/dashboard/src/lib/github-app.server.ts
index 6ea0cda..d0e3629 100644
--- a/apps/dashboard/src/lib/github-app.server.ts
+++ b/apps/dashboard/src/lib/github-app.server.ts
@@ -4,6 +4,7 @@ import { and, eq } from "drizzle-orm";
import { getDb } from "../db";
import { account } from "../db/schema";
import { normalizeGitHubAppPrivateKey } from "./github-private-key";
+import { GITHUB_REQUEST_TIMEOUT_MS } from "./github-request-policy";
type WorkerEnvRecord = typeof env & Record;
@@ -150,6 +151,7 @@ async function requestGitHubAppUserToken(params: Record) {
Accept: "application/json",
"Content-Type": "application/x-www-form-urlencoded",
},
+ signal: AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS),
body: new URLSearchParams(params),
});
const payload = (await response.json()) as GitHubTokenResponse;
diff --git a/apps/dashboard/src/lib/github-request-policy.ts b/apps/dashboard/src/lib/github-request-policy.ts
new file mode 100644
index 0000000..f792d0d
--- /dev/null
+++ b/apps/dashboard/src/lib/github-request-policy.ts
@@ -0,0 +1,35 @@
+import type { Octokit as OctokitType } from "octokit";
+
+const GITHUB_READ_RETRY_COUNT = 1;
+export const GITHUB_REQUEST_TIMEOUT_MS = 12_000;
+
+type GitHubRequestOptions = Parameters<
+ OctokitType["hook"]["before"]
+>[1] extends (options: infer Options) => unknown
+ ? Options & {
+ method?: string;
+ request?: {
+ retries?: number;
+ signal?: AbortSignal;
+ };
+ }
+ : never;
+
+function isSafeGitHubRetryMethod(method: string | undefined) {
+ return method === "GET" || method === "HEAD" || method === "OPTIONS";
+}
+
+function createGitHubRequestTimeoutSignal() {
+ return AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS);
+}
+
+export function configureGitHubRequestPolicies(octokit: OctokitType) {
+ octokit.hook.before("request", (options: GitHubRequestOptions) => {
+ const requestOptions = options.request ?? {};
+ options.request = requestOptions;
+ requestOptions.retries = isSafeGitHubRetryMethod(options.method)
+ ? GITHUB_READ_RETRY_COUNT
+ : 0;
+ requestOptions.signal ??= createGitHubRequestTimeoutSignal();
+ });
+}
diff --git a/apps/dashboard/src/lib/github.query.ts b/apps/dashboard/src/lib/github.query.ts
index 99bca72..c378c25 100644
--- a/apps/dashboard/src/lib/github.query.ts
+++ b/apps/dashboard/src/lib/github.query.ts
@@ -253,7 +253,6 @@ export function githubPullDetailQueryOptions(
queryFn: () => getPullFromRepo({ data: input }),
staleTime: githubCachePolicy.detail.staleTimeMs,
gcTime: githubCachePolicy.detail.gcTimeMs,
- refetchOnMount: "always",
meta: tabPersistedMeta,
});
}
@@ -265,10 +264,8 @@ export function githubPullPageQueryOptions(
return queryOptions({
queryKey: githubQueryKeys.pulls.page(scope, input),
queryFn: () => getPullPageData({ data: input }),
- staleTime: githubCachePolicy.activity.staleTimeMs,
+ staleTime: githubCachePolicy.detail.staleTimeMs,
gcTime: githubCachePolicy.detail.gcTimeMs,
- refetchOnMount: "always",
- refetchOnWindowFocus: "always",
meta: tabPersistedMeta,
});
}
@@ -282,7 +279,6 @@ export function githubPullCommentsQueryOptions(
queryFn: () => getPullComments({ data: input }),
staleTime: githubCachePolicy.activity.staleTimeMs,
gcTime: githubCachePolicy.activity.gcTimeMs,
- refetchOnMount: "always",
meta: tabPersistedMeta,
});
}
@@ -296,7 +292,6 @@ export function githubPullStatusQueryOptions(
queryFn: () => getPullStatus({ data: input }),
staleTime: githubCachePolicy.status.staleTimeMs,
gcTime: githubCachePolicy.status.gcTimeMs,
- refetchOnMount: "always",
meta: tabPersistedMeta,
});
}
@@ -310,7 +305,6 @@ export function githubPullFilesQueryOptions(
queryFn: () => getPullFiles({ data: input }),
staleTime: githubCachePolicy.detail.staleTimeMs,
gcTime: githubCachePolicy.detail.gcTimeMs,
- refetchOnMount: "always",
meta: tabPersistedMeta,
});
}
@@ -324,7 +318,6 @@ export function githubPullFileSummariesQueryOptions(
queryFn: () => getPullFileSummaries({ data: input }),
staleTime: githubCachePolicy.detail.staleTimeMs,
gcTime: githubCachePolicy.detail.gcTimeMs,
- refetchOnMount: "always",
meta: tabPersistedMeta,
});
}
@@ -338,7 +331,6 @@ export function githubPullReviewCommentsQueryOptions(
queryFn: () => getPullReviewComments({ data: input }),
staleTime: githubCachePolicy.activity.staleTimeMs,
gcTime: githubCachePolicy.activity.gcTimeMs,
- refetchOnMount: "always",
meta: tabPersistedMeta,
});
}
@@ -424,7 +416,6 @@ export function githubIssueDetailQueryOptions(
queryFn: () => getIssueFromRepo({ data: input }),
staleTime: githubCachePolicy.detail.staleTimeMs,
gcTime: githubCachePolicy.detail.gcTimeMs,
- refetchOnMount: "always",
meta: tabPersistedMeta,
});
}
@@ -436,10 +427,8 @@ export function githubIssuePageQueryOptions(
return queryOptions({
queryKey: githubQueryKeys.issues.page(scope, input),
queryFn: () => getIssuePageData({ data: input }),
- staleTime: githubCachePolicy.activity.staleTimeMs,
+ staleTime: githubCachePolicy.detail.staleTimeMs,
gcTime: githubCachePolicy.detail.gcTimeMs,
- refetchOnMount: "always",
- refetchOnWindowFocus: "always",
meta: tabPersistedMeta,
});
}
@@ -453,7 +442,6 @@ export function githubIssueCommentsQueryOptions(
queryFn: () => getIssueComments({ data: input }),
staleTime: githubCachePolicy.activity.staleTimeMs,
gcTime: githubCachePolicy.activity.gcTimeMs,
- refetchOnMount: "always",
meta: tabPersistedMeta,
});
}
diff --git a/apps/dashboard/src/lib/github.server.test.ts b/apps/dashboard/src/lib/github.server.test.ts
index f530d06..3739bab 100644
--- a/apps/dashboard/src/lib/github.server.test.ts
+++ b/apps/dashboard/src/lib/github.server.test.ts
@@ -50,7 +50,7 @@ beforeEach(() => {
});
describe("getGitHubClient", () => {
- it("configures Octokit throttling and safe-method retries", async () => {
+ it("configures Octokit throttling, bounded retries, and request timeouts", async () => {
const { getGitHubClient } = await import("./github.server");
await getGitHubClient("user-123");
@@ -102,23 +102,28 @@ describe("getGitHubClient", () => {
expect(instance.hookBefore).toHaveBeenCalledTimes(1);
const [hookEvent, hookHandler] = instance.hookBefore.mock.calls[0] as [
string,
- (options: { method?: string; request?: { retries?: number } }) => void,
+ (options: {
+ method?: string;
+ request?: { retries?: number; signal?: AbortSignal };
+ }) => void,
];
expect(hookEvent).toBe("request");
const getOptions = { method: "GET" } as {
method?: string;
- request?: { retries?: number };
+ request?: { retries?: number; signal?: AbortSignal };
};
hookHandler(getOptions);
- expect(getOptions.request?.retries).toBe(2);
+ expect(getOptions.request?.retries).toBe(1);
+ expect(getOptions.request?.signal).toBeInstanceOf(AbortSignal);
const postOptions = { method: "POST" } as {
method?: string;
- request?: { retries?: number };
+ request?: { retries?: number; signal?: AbortSignal };
};
hookHandler(postOptions);
expect(postOptions.request?.retries).toBe(0);
+ expect(postOptions.request?.signal).toBeInstanceOf(AbortSignal);
expect(
options.throttle.onRateLimit(
@@ -129,7 +134,7 @@ describe("getGitHubClient", () => {
},
0,
),
- ).toBe(true);
+ ).toBe(false);
expect(
options.throttle.onRateLimit(
30,
@@ -149,7 +154,7 @@ describe("getGitHubClient", () => {
),
).toBe(false);
expect(instance.log.warn).toHaveBeenCalled();
- expect(instance.log.info).toHaveBeenCalledTimes(1);
+ expect(instance.log.info).not.toHaveBeenCalled();
});
it("creates GitHub App installation clients from app credentials", async () => {
diff --git a/apps/dashboard/src/lib/github.server.ts b/apps/dashboard/src/lib/github.server.ts
index cf371b7..c1024bb 100644
--- a/apps/dashboard/src/lib/github.server.ts
+++ b/apps/dashboard/src/lib/github.server.ts
@@ -5,10 +5,9 @@ import {
getGitHubAppId,
getGitHubAppPrivateKey,
} from "./github-app.server";
+import { configureGitHubRequestPolicies } from "./github-request-policy";
const GITHUB_CLIENT_USER_AGENT = "quickhub-dashboard";
-const GITHUB_READ_RETRY_COUNT = 2;
-const GITHUB_RATE_LIMIT_RETRY_COUNT = 1;
const GITHUB_SECONDARY_RATE_LIMIT_FALLBACK_SECONDS = 60;
type GitHubThrottleRequestOptions = {
@@ -18,33 +17,6 @@ type GitHubThrottleRequestOptions = {
type GitHubThrottleClient = Pick;
-function isSafeGitHubRetryMethod(method: string | undefined) {
- return method === "GET" || method === "HEAD" || method === "OPTIONS";
-}
-
-function shouldRetryGitHubRateLimitedRequest({
- method,
- retryCount,
-}: {
- method: string | undefined;
- retryCount: number;
-}) {
- return (
- isSafeGitHubRetryMethod(method) &&
- retryCount < GITHUB_RATE_LIMIT_RETRY_COUNT
- );
-}
-
-function configureGitHubRequestPolicies(octokit: OctokitType) {
- octokit.hook.before("request", (options) => {
- const requestOptions = options.request ?? {};
- options.request = requestOptions;
- requestOptions.retries = isSafeGitHubRetryMethod(options.method)
- ? GITHUB_READ_RETRY_COUNT
- : 0;
- });
-}
-
export async function getGitHubClient(userId: string): Promise {
const octokit = new Octokit({
auth: await getGitHubAccessTokenByUserId(userId),
@@ -65,18 +37,6 @@ export async function getGitHubClient(userId: string): Promise {
`GitHub rate limit for ${options.method} ${options.url}; retryAfter=${retryAfter}s retryCount=${retryCount}`,
);
- if (
- shouldRetryGitHubRateLimitedRequest({
- method: options.method,
- retryCount,
- })
- ) {
- throttledOctokit.log.info(
- `Retrying ${options.method} ${options.url} after ${retryAfter}s`,
- );
- return true;
- }
-
return false;
},
onSecondaryRateLimit: (
@@ -89,18 +49,6 @@ export async function getGitHubClient(userId: string): Promise {
`GitHub secondary rate limit for ${options.method} ${options.url}; retryAfter=${retryAfter}s retryCount=${retryCount}`,
);
- if (
- shouldRetryGitHubRateLimitedRequest({
- method: options.method,
- retryCount,
- })
- ) {
- throttledOctokit.log.info(
- `Retrying ${options.method} ${options.url} after secondary rate limit (${retryAfter}s)`,
- );
- return true;
- }
-
return false;
},
},
diff --git a/apps/dashboard/src/lib/tab-store.ts b/apps/dashboard/src/lib/tab-store.ts
index da15ec7..dab5d37 100644
--- a/apps/dashboard/src/lib/tab-store.ts
+++ b/apps/dashboard/src/lib/tab-store.ts
@@ -88,6 +88,8 @@ export function removeTabsToRight(id: string) {
emitChange();
}
+const emptyTabs: Tab[] = [];
+
export function useTabs() {
- return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
+ return useSyncExternalStore(subscribe, getSnapshot, () => emptyTabs);
}
diff --git a/apps/dashboard/src/lib/use-github-revalidation.ts b/apps/dashboard/src/lib/use-github-revalidation.ts
index 06cf9f4..cb7520a 100644
--- a/apps/dashboard/src/lib/use-github-revalidation.ts
+++ b/apps/dashboard/src/lib/use-github-revalidation.ts
@@ -10,6 +10,7 @@ import {
import { type Tab, useTabs } from "./tab-store";
const GITHUB_REVALIDATION_POLL_INTERVAL_MS = 10_000;
+const GITHUB_REVALIDATION_INITIAL_DELAY_MS = 5_000;
function getUniqueSignalKeys(tabs: Tab[]) {
return Array.from(
@@ -104,8 +105,8 @@ export function useGitHubRevalidation(userId: string) {
const signalsByKey = new Map(
records.map((record) => [record.signalKey, record.updatedAt]),
);
- const invalidations: Promise[] = [];
+ // Invalidate list queries first (lightweight)
const pullsMineUpdatedAt =
signalsByKey.get(githubRevalidationSignalKeys.pullsMine) ?? 0;
if (
@@ -115,11 +116,9 @@ export function useGitHubRevalidation(userId: string) {
debug("github-revalidation", "invalidating pull list queries", {
pullsMineUpdatedAt,
});
- invalidations.push(
- queryClient.invalidateQueries({
- queryKey: githubQueryKeys.pulls.mine(scope),
- }),
- );
+ await queryClient.invalidateQueries({
+ queryKey: githubQueryKeys.pulls.mine(scope),
+ });
}
const issuesMineUpdatedAt =
@@ -131,14 +130,16 @@ export function useGitHubRevalidation(userId: string) {
debug("github-revalidation", "invalidating issue list queries", {
issuesMineUpdatedAt,
});
- invalidations.push(
- queryClient.invalidateQueries({
- queryKey: githubQueryKeys.issues.mine(scope),
- }),
- );
+ await queryClient.invalidateQueries({
+ queryKey: githubQueryKeys.issues.mine(scope),
+ });
}
+ // Invalidate tab queries serially to avoid a burst of concurrent
+ // server function RPCs that can overwhelm the Worker.
for (const tab of tabs) {
+ if (cancelled) break;
+
const signalKey = getGitHubRevalidationSignalKeysForTab(tab)[0];
const updatedAt = signalsByKey.get(signalKey) ?? 0;
if (updatedAt === 0) {
@@ -157,9 +158,7 @@ export function useGitHubRevalidation(userId: string) {
signalKey,
tabId: tab.id,
});
- invalidations.push(
- invalidatePullTabQueries(queryClient, scope, tab),
- );
+ await invalidatePullTabQueries(queryClient, scope, tab);
}
continue;
}
@@ -175,13 +174,9 @@ export function useGitHubRevalidation(userId: string) {
signalKey,
tabId: tab.id,
});
- invalidations.push(
- invalidateIssueTabQueries(queryClient, scope, tab),
- );
+ await invalidateIssueTabQueries(queryClient, scope, tab);
}
}
-
- await Promise.all(invalidations);
} catch (error) {
debug("github-revalidation", "poll failed", {
error: error instanceof Error ? error.message : String(error),
@@ -196,7 +191,12 @@ export function useGitHubRevalidation(userId: string) {
}
};
- void pollSignals();
+ // Delay the first poll so it doesn't collide with initial data loading
+ // (route loaders + preloading) which already makes server function RPCs.
+ timeoutId = window.setTimeout(
+ pollSignals,
+ GITHUB_REVALIDATION_INITIAL_DELAY_MS,
+ );
return () => {
cancelled = true;
diff --git a/apps/dashboard/src/routes/_protected/$owner/$repo/issues.$issueId.tsx b/apps/dashboard/src/routes/_protected/$owner/$repo/issues.$issueId.tsx
index 7d4cd2f..997ff0e 100644
--- a/apps/dashboard/src/routes/_protected/$owner/$repo/issues.$issueId.tsx
+++ b/apps/dashboard/src/routes/_protected/$owner/$repo/issues.$issueId.tsx
@@ -1,12 +1,13 @@
import { createFileRoute } from "@tanstack/react-router";
import { IssueDetailPage } from "#/components/issues/detail/issue-detail-page";
import { githubIssuePageQueryOptions } from "#/lib/github.query";
-import { buildSeo, formatPageTitle, summarizeText } from "#/lib/seo";
+import { buildSeo, formatPageTitle } from "#/lib/seo";
export const Route = createFileRoute(
"/_protected/$owner/$repo/issues/$issueId",
)({
- loader: async ({ context, params }) => {
+ ssr: false,
+ loader: ({ context, params }) => {
const issueNumber = Number(params.issueId);
const scope = { userId: context.user.id };
const pageOptions = githubIssuePageQueryOptions(scope, {
@@ -15,10 +16,8 @@ export const Route = createFileRoute(
issueNumber,
});
+ // Clean up null cache entries (issue not found)
const cachedData = context.queryClient.getQueryData(pageOptions.queryKey);
- if (cachedData !== null && cachedData !== undefined) {
- return cachedData;
- }
if (cachedData === null) {
context.queryClient.removeQueries({
queryKey: pageOptions.queryKey,
@@ -26,25 +25,16 @@ export const Route = createFileRoute(
});
}
- return context.queryClient.ensureQueryData(pageOptions);
+ // Never block navigation — fire prefetch and let the component
+ // show cached data instantly or a skeleton while loading.
+ void context.queryClient.prefetchQuery(pageOptions);
},
- head: ({ loaderData, match, params }) => {
- const issue = loaderData?.detail;
- const issueTitle = issue
- ? formatPageTitle(`Issue #${issue.number}: ${issue.title}`)
- : formatPageTitle(`Issue #${params.issueId}`);
-
- return buildSeo({
+ head: ({ match, params }) =>
+ buildSeo({
path: match.pathname,
- title: issueTitle,
- description: issue
- ? summarizeText(
- issue.body,
- `Private GitHub issue #${issue.number} in ${params.owner}/${params.repo}.`,
- )
- : `Private GitHub issue #${params.issueId} in ${params.owner}/${params.repo}.`,
+ title: formatPageTitle(`Issue #${params.issueId}`),
+ description: `Private GitHub issue #${params.issueId} in ${params.owner}/${params.repo}.`,
robots: "noindex",
- });
- },
+ }),
component: IssueDetailPage,
});
diff --git a/apps/dashboard/src/routes/_protected/$owner/$repo/pull.$pullId.tsx b/apps/dashboard/src/routes/_protected/$owner/$repo/pull.$pullId.tsx
index 3b99431..07bbdc2 100644
--- a/apps/dashboard/src/routes/_protected/$owner/$repo/pull.$pullId.tsx
+++ b/apps/dashboard/src/routes/_protected/$owner/$repo/pull.$pullId.tsx
@@ -4,10 +4,11 @@ import {
githubPullPageQueryOptions,
githubViewerQueryOptions,
} from "#/lib/github.query";
-import { buildSeo, formatPageTitle, summarizeText } from "#/lib/seo";
+import { buildSeo, formatPageTitle } from "#/lib/seo";
export const Route = createFileRoute("/_protected/$owner/$repo/pull/$pullId")({
- loader: async ({ context, params }) => {
+ ssr: false,
+ loader: ({ context, params }) => {
const pullNumber = Number(params.pullId);
const scope = { userId: context.user.id };
const pageOptions = githubPullPageQueryOptions(scope, {
@@ -16,41 +17,26 @@ export const Route = createFileRoute("/_protected/$owner/$repo/pull/$pullId")({
pullNumber,
});
+ // Clean up broken cache entries (no detail)
const cachedData = context.queryClient.getQueryData(pageOptions.queryKey);
- if (cachedData?.detail) {
- void context.queryClient.ensureQueryData(githubViewerQueryOptions(scope));
- return cachedData;
- }
- if (cachedData !== undefined) {
+ if (cachedData !== undefined && !cachedData?.detail) {
context.queryClient.removeQueries({
queryKey: pageOptions.queryKey,
exact: true,
});
}
- const [pageData] = await Promise.all([
- context.queryClient.ensureQueryData(pageOptions),
- context.queryClient.ensureQueryData(githubViewerQueryOptions(scope)),
- ]);
- return pageData;
+ // Never block navigation — fire prefetches and let the component
+ // show cached data instantly or a skeleton while loading.
+ void context.queryClient.prefetchQuery(pageOptions);
+ void context.queryClient.prefetchQuery(githubViewerQueryOptions(scope));
},
- head: ({ loaderData, match, params }) => {
- const pull = loaderData?.detail;
- const title = pull
- ? formatPageTitle(pull.title)
- : formatPageTitle(`PR #${params.pullId}`);
-
- return buildSeo({
+ head: ({ match, params }) =>
+ buildSeo({
path: match.pathname,
- title,
- description: pull
- ? summarizeText(
- pull.body,
- `Private pull request #${pull.number} in ${params.owner}/${params.repo}.`,
- )
- : `Private pull request #${params.pullId} in ${params.owner}/${params.repo}.`,
+ title: formatPageTitle(`PR #${params.pullId}`),
+ description: `Private pull request #${params.pullId} in ${params.owner}/${params.repo}.`,
robots: "noindex",
- });
- },
+ }),
component: PullDetailPage,
});
diff --git a/apps/dashboard/src/routes/_protected/$owner/$repo/review.$pullId.tsx b/apps/dashboard/src/routes/_protected/$owner/$repo/review.$pullId.tsx
index bf9c76e..1a5e77c 100644
--- a/apps/dashboard/src/routes/_protected/$owner/$repo/review.$pullId.tsx
+++ b/apps/dashboard/src/routes/_protected/$owner/$repo/review.$pullId.tsx
@@ -6,13 +6,14 @@ import {
githubPullPageQueryOptions,
githubQueryKeys,
} from "#/lib/github.query";
-import { buildSeo, formatPageTitle, summarizeText } from "#/lib/seo";
+import { buildSeo, formatPageTitle } from "#/lib/seo";
const PULL_FILES_PAGE_SIZE = 25;
export const Route = createFileRoute("/_protected/$owner/$repo/review/$pullId")(
{
- loader: async ({ context, params }) => {
+ ssr: false,
+ loader: ({ context, params }) => {
const pullNumber = Number(params.pullId);
const scope = { userId: context.user.id };
const input = { owner: params.owner, repo: params.repo, pullNumber };
@@ -22,59 +23,37 @@ export const Route = createFileRoute("/_protected/$owner/$repo/review/$pullId")(
input,
);
- let cachedPageData = context.queryClient.getQueryData(
+ // Clean up broken cache entries (no detail)
+ const cachedPageData = context.queryClient.getQueryData(
pageOptions.queryKey,
);
- const cachedFileSummaries = context.queryClient.getQueryData(
- fileSummariesOptions.queryKey,
- );
if (cachedPageData !== undefined && !cachedPageData?.detail) {
context.queryClient.removeQueries({
queryKey: pageOptions.queryKey,
exact: true,
});
- cachedPageData = undefined;
}
- // Check if infinite query already has data
- const filesQueryKey = githubQueryKeys.pulls.files(scope, input);
- const cachedFilesData = context.queryClient.getQueryData(filesQueryKey);
-
- const [pageData, fileSummaries, firstFilesPage] = await Promise.all([
- cachedPageData ?? context.queryClient.ensureQueryData(pageOptions),
- cachedFileSummaries ??
- context.queryClient.ensureQueryData(fileSummariesOptions),
- cachedFilesData
- ? null
- : getPullFiles({
- data: {
- ...input,
- page: 1,
- perPage: PULL_FILES_PAGE_SIZE,
- },
- }),
- ]);
+ // Never block navigation — fire prefetches and let the component
+ // show cached data instantly or a skeleton while loading.
+ void context.queryClient.prefetchQuery(pageOptions);
+ void context.queryClient.prefetchQuery(fileSummariesOptions);
- return { pageData, fileSummaries, firstFilesPage };
+ // Prefetch first page of files if not cached
+ const filesQueryKey = githubQueryKeys.pulls.files(scope, input);
+ if (!context.queryClient.getQueryData(filesQueryKey)) {
+ void getPullFiles({
+ data: { ...input, page: 1, perPage: PULL_FILES_PAGE_SIZE },
+ });
+ }
},
- head: ({ loaderData, match, params }) => {
- const pull = loaderData?.pageData?.detail;
- const title = pull
- ? formatPageTitle(pull.title)
- : formatPageTitle(`Review PR #${params.pullId}`);
-
- return buildSeo({
+ head: ({ match, params }) =>
+ buildSeo({
path: match.pathname,
- title,
- description: pull
- ? summarizeText(
- pull.body,
- `Private code review workspace for pull request #${pull.number} in ${params.owner}/${params.repo}.`,
- )
- : `Private code review workspace for pull request #${params.pullId} in ${params.owner}/${params.repo}.`,
+ title: formatPageTitle(`Review PR #${params.pullId}`),
+ description: `Private code review workspace for pull request #${params.pullId} in ${params.owner}/${params.repo}.`,
robots: "noindex",
- });
- },
+ }),
component: ReviewPage,
},
);
diff --git a/apps/dashboard/src/routes/_protected/index.tsx b/apps/dashboard/src/routes/_protected/index.tsx
index 4f2e9d7..27627f5 100644
--- a/apps/dashboard/src/routes/_protected/index.tsx
+++ b/apps/dashboard/src/routes/_protected/index.tsx
@@ -12,6 +12,7 @@ import { buildSeo, formatPageTitle } from "#/lib/seo";
import { useHasMounted } from "#/lib/use-has-mounted";
export const Route = createFileRoute("/_protected/")({
+ ssr: false,
loader: async ({ context }) => {
const scope = { userId: context.user.id };
await Promise.all([
diff --git a/apps/dashboard/src/routes/_protected/issues.tsx b/apps/dashboard/src/routes/_protected/issues.tsx
index 9bd3a96..9a0ba9f 100644
--- a/apps/dashboard/src/routes/_protected/issues.tsx
+++ b/apps/dashboard/src/routes/_protected/issues.tsx
@@ -27,6 +27,7 @@ import { buildSeo, formatPageTitle } from "#/lib/seo";
import { useHasMounted } from "#/lib/use-has-mounted";
export const Route = createFileRoute("/_protected/issues")({
+ ssr: false,
loader: async ({ context }) => {
const scope = { userId: context.user.id };
const [, filterStore] = await Promise.all([
diff --git a/apps/dashboard/src/routes/_protected/pulls.tsx b/apps/dashboard/src/routes/_protected/pulls.tsx
index 82cdf48..bd986c1 100644
--- a/apps/dashboard/src/routes/_protected/pulls.tsx
+++ b/apps/dashboard/src/routes/_protected/pulls.tsx
@@ -33,6 +33,7 @@ import { buildSeo, formatPageTitle } from "#/lib/seo";
import { useHasMounted } from "#/lib/use-has-mounted";
export const Route = createFileRoute("/_protected/pulls")({
+ ssr: false,
loader: async ({ context }) => {
const scope = { userId: context.user.id };
const [, filterStore] = await Promise.all([
diff --git a/apps/dashboard/src/routes/_protected/reviews.tsx b/apps/dashboard/src/routes/_protected/reviews.tsx
index 627ae4d..474f7f1 100644
--- a/apps/dashboard/src/routes/_protected/reviews.tsx
+++ b/apps/dashboard/src/routes/_protected/reviews.tsx
@@ -17,6 +17,7 @@ import { buildSeo, formatPageTitle } from "#/lib/seo";
import { useHasMounted } from "#/lib/use-has-mounted";
export const Route = createFileRoute("/_protected/reviews")({
+ ssr: false,
loader: async ({ context }) => {
const scope = { userId: context.user.id };
const [, filterStore] = await Promise.all([
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d826d28..3f45ad5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -45,25 +45,25 @@ importers:
specifier: ^4.1.18
version: 4.2.2(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))
'@tanstack/react-devtools':
- specifier: latest
+ specifier: ~0.10.2
version: 0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.12)
'@tanstack/react-query':
- specifier: latest
+ specifier: ~5.97.0
version: 5.97.0(react@19.2.4)
'@tanstack/react-router':
- specifier: latest
+ specifier: ~1.168.13
version: 1.168.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@tanstack/react-router-devtools':
- specifier: latest
+ specifier: ~1.166.11
version: 1.166.11(@tanstack/react-router@1.168.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.168.9)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@tanstack/react-router-ssr-query':
- specifier: latest
+ specifier: ~1.166.10
version: 1.166.10(@tanstack/query-core@5.97.0)(@tanstack/react-query@5.97.0(react@19.2.4))(@tanstack/react-router@1.168.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.168.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@tanstack/react-start':
- specifier: latest
+ specifier: ~1.167.23
version: 1.167.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))
'@tanstack/router-plugin':
- specifier: ^1.132.0
+ specifier: ~1.167.12
version: 1.167.12(@tanstack/react-router@1.168.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))
agentation:
specifier: ^3.0.2
@@ -103,7 +103,7 @@ importers:
specifier: workspace:*
version: link:../../packages/typescript-config
'@tanstack/devtools-vite':
- specifier: latest
+ specifier: ~0.6.0
version: 0.6.0(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))
'@testing-library/dom':
specifier: ^10.4.1