From 54c3c92336865da96d681e9d1cdffd7041be745d Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Fri, 5 Jun 2026 02:54:47 -0600 Subject: [PATCH] Fix dashboard cached PR aggregate --- src/api/routes.ts | 13 +++++------ src/db/repositories.ts | 32 ++++++++++++++++++++++++++- test/integration/api.test.ts | 43 ++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/src/api/routes.ts b/src/api/routes.ts index 847534015d..91b7406c94 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -69,6 +69,7 @@ import { listRepoLabels, listRepoSyncSegments, listRepoSyncStates, + summarizeRepoSyncOpenPullRequests, listSignalSnapshots, listPullRequests, listRepositories, @@ -827,12 +828,11 @@ export function createApp() { const summary = await getRoleSummaryForIdentity(c.env, identity); if (!summary.roles.some((role) => ["maintainer", "owner", "operator"].includes(role))) return c.json({ error: "insufficient_role" }, 403); - const [allRepositories, allInstallations, allHealth, allRateLimits, allSyncStates] = await Promise.all([ + const [allRepositories, allInstallations, allHealth, allRateLimits] = await Promise.all([ listRepositories(c.env), listInstallations(c.env), listInstallationHealth(c.env), listLatestGitHubRateLimitObservations(c.env, 20), - listRepoSyncStates(c.env), ]); const scope = identity.kind === "session" && !summary.roles.includes("operator") ? await loadControlPanelAccessScope(c.env, identity.actor) : null; const scopedRepoNames = new Set(scope?.repositoryFullNames.map((repo) => repo.toLowerCase()) ?? []); @@ -846,13 +846,10 @@ export function createApp() { ? allHealth.filter((record) => scopedInstallationIds.has(record.installationId) || scopedAccountLogins.has(record.accountLogin.toLowerCase())) : allHealth; const rateLimits = scope ? allRateLimits.filter((record) => record.repoFullName !== undefined && record.repoFullName !== null && scopedRepoNames.has(record.repoFullName.toLowerCase())) : allRateLimits; - // Cached open-PR count is summed across ALL in-scope repos from sync state (a single query) so the - // headline metric is a true global count like its siblings. The per-repo PR fetch below is capped at + // Cached open-PR count is aggregated across ALL in-scope repos from sync state without using the + // capped sync-state listing that powers previews elsewhere. The per-repo PR fetch below is capped at // 12 only to bound the `reviewability` preview list, not the metric. - const scopedRepoNameSet = new Set(repositories.map((repo) => repo.fullName.toLowerCase())); - const scopedSyncStates = allSyncStates.filter((state) => scopedRepoNameSet.has(state.repoFullName.toLowerCase())); - const totalOpenPullRequestsCached = scopedSyncStates.reduce((sum, state) => sum + Math.max(0, state.openPullRequestsCount), 0); - const reposWithOpenPullRequests = scopedSyncStates.filter((state) => state.openPullRequestsCount > 0).length; + const { totalOpenPullRequestsCached, reposWithOpenPullRequests } = await summarizeRepoSyncOpenPullRequests(c.env, repositories.map((repo) => repo.fullName)); const openPullRequests = ( await Promise.all(repositories.slice(0, 12).map((repo) => listOpenPullRequests(c.env, repo.fullName).then((rows) => rows.map((pull) => ({ repoFullName: repo.fullName, pull }))))) ).flat(); diff --git a/src/db/repositories.ts b/src/db/repositories.ts index c77fb05ddf..946259d1c1 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -1,4 +1,4 @@ -import { and, desc, eq, gte, not, or, sql, type SQL } from "drizzle-orm"; +import { and, desc, eq, gte, inArray, not, or, sql, type SQL } from "drizzle-orm"; import { getDb } from "./client"; import { advisories, @@ -512,6 +512,36 @@ export async function listRepoSyncStates(env: Env): Promise { + const db = getDb(env.DB); + const aggregate = async (repoNames?: string[]) => { + const query = db + .select({ + totalOpenPullRequestsCached: sql`coalesce(sum(case when ${repoSyncState.openPullRequestsCount} > 0 then ${repoSyncState.openPullRequestsCount} else 0 end), 0)`, + reposWithOpenPullRequests: sql`coalesce(sum(case when ${repoSyncState.openPullRequestsCount} > 0 then 1 else 0 end), 0)`, + }) + .from(repoSyncState); + const [row] = repoNames ? await query.where(inArray(sql`lower(${repoSyncState.repoFullName})`, repoNames)) : await query; + return { + totalOpenPullRequestsCached: Number(row?.totalOpenPullRequestsCached ?? 0), + reposWithOpenPullRequests: Number(row?.reposWithOpenPullRequests ?? 0), + }; + }; + + if (repoFullNames === undefined) return aggregate(); + + const normalizedRepoNames = Array.from(new Set(repoFullNames.map((name) => name.toLowerCase()))); + const summary = { totalOpenPullRequestsCached: 0, reposWithOpenPullRequests: 0 }; + for (let index = 0; index < normalizedRepoNames.length; index += 450) { + const chunk = normalizedRepoNames.slice(index, index + 450); + if (chunk.length === 0) continue; + const chunkSummary = await aggregate(chunk); + summary.totalOpenPullRequestsCached += chunkSummary.totalOpenPullRequestsCached; + summary.reposWithOpenPullRequests += chunkSummary.reposWithOpenPullRequests; + } + return summary; +} + export async function upsertRepoSyncSegment(env: Env, segment: RepoSyncSegmentRecord): Promise { const db = getDb(env.DB); await db diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index d7e27e0a08..cfa6a6aef5 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -1397,6 +1397,49 @@ describe("api routes", () => { expect(body.metrics.find((metric) => metric.label === "Open PRs cached")?.value).toBe(8); }); + it("counts cached open PRs from sync states beyond the latest 500 rows", async () => { + const app = createApp(); + const env = createTestEnv(); + + vi.setSystemTime(new Date("2026-05-27T00:00:00.000Z")); + await upsertRepositoryFromGitHub(env, { name: "oldest", full_name: "entrius/oldest", private: false, owner: { login: "entrius" }, default_branch: "main" }); + await upsertRepoSyncState(env, { + repoFullName: "entrius/oldest", + status: "success", + sourceKind: "github", + primaryLanguage: "TypeScript", + defaultBranch: "main", + isPrivate: false, + openIssuesCount: 0, + openPullRequestsCount: 7, + recentMergedPullRequestsCount: 0, + warnings: [], + }); + + vi.setSystemTime(new Date("2026-05-28T00:00:00.000Z")); + for (let index = 0; index < 500; index += 1) { + const name = `newer-${index}`; + await upsertRepositoryFromGitHub(env, { name, full_name: `entrius/${name}`, private: false, owner: { login: "entrius" }, default_branch: "main" }); + await upsertRepoSyncState(env, { + repoFullName: `entrius/${name}`, + status: "success", + sourceKind: "github", + primaryLanguage: "TypeScript", + defaultBranch: "main", + isPrivate: false, + openIssuesCount: 0, + openPullRequestsCount: 1, + recentMergedPullRequestsCount: 0, + warnings: [], + }); + } + + const res = await app.request("/v1/app/maintainer-dashboard", { headers: apiHeaders(env) }, env); + expect(res.status).toBe(200); + const body = (await res.json()) as { metrics: Array<{ label: string; value: number }> }; + expect(body.metrics.find((metric) => metric.label === "Open PRs cached")?.value).toBe(507); + }); + it("serves live app dashboards, digest subscriptions, commands, and extension context", async () => { const app = createApp(); const env = createTestEnv({ ADMIN_GITHUB_LOGINS: "oktofeesh1,other", PRODUCT_USAGE_HASH_SALT: "usage-adoption-test-salt" });