From 50d27a324835b6db34bdb7e5b1d965d47d818b37 Mon Sep 17 00:00:00 2001 From: Vsevolod Avramov Date: Tue, 7 Apr 2026 18:04:52 +0300 Subject: [PATCH 1/3] Add dashboard repository picker with cached GitHub repos --- src/dashboard-server.ts | 65 ++++++++++++++++ src/dashboard/html.ts | 124 +++++++++++++++++++++++++++++- src/github.ts | 28 ++++++- tests/dashboard-pipelines.test.ts | 37 +++++++++ 4 files changed, 248 insertions(+), 6 deletions(-) diff --git a/src/dashboard-server.ts b/src/dashboard-server.ts index df63cc9..f4dae23 100644 --- a/src/dashboard-server.ts +++ b/src/dashboard-server.ts @@ -59,6 +59,17 @@ function parseLimit(value: string | null): number { /** Max request body size: 1 MB (matches webhook-server.ts) */ const MAX_BODY_BYTES = 1024 * 1024; +const GITHUB_REPOSITORIES_CACHE_TTL_MS = 60_000; + +interface CachedGitHubRepositories { + fetchedAt: number; + repositories: Array<{ + fullName: string; + private: boolean; + defaultBranch?: string; + htmlUrl?: string; + }>; +} async function readBody(req: IncomingMessage): Promise { return new Promise((resolve, reject) => { @@ -299,6 +310,9 @@ export function startDashboardServer( onSetupComplete?: () => Promise, evalStore?: EvalStore, ): void { + const githubService = GitHubService.create(config); + let githubRepositoriesCache: CachedGitHubRepositories | undefined; + const server = createServer(async (req, res) => { try { // Security headers — applied to all responses @@ -578,6 +592,57 @@ export function startDashboardServer( return; } + if (req.method === "GET" && pathname === "/api/github/repositories") { + if (!githubService) { + sendJson(res, 501, { error: "GitHub integration is not configured" }); + return; + } + + const refresh = requestUrl.searchParams.get("refresh") === "1"; + const now = Date.now(); + const cachedRepositories = githubRepositoriesCache; + const cacheFresh = cachedRepositories && (now - cachedRepositories.fetchedAt) < GITHUB_REPOSITORIES_CACHE_TTL_MS; + + if (!refresh && cacheFresh) { + sendJson(res, 200, { + repositories: cachedRepositories.repositories, + cached: true, + fetchedAt: new Date(cachedRepositories.fetchedAt).toISOString(), + }); + return; + } + + try { + const repositories = await githubService.listAccessibleRepos(); + githubRepositoriesCache = { + repositories, + fetchedAt: now, + }; + sendJson(res, 200, { + repositories, + cached: false, + fetchedAt: new Date(now).toISOString(), + }); + } catch (error) { + logError("dashboard: failed to list github repositories", { + error: error instanceof Error ? error.message : String(error), + }); + + if (githubRepositoriesCache) { + sendJson(res, 200, { + repositories: githubRepositoriesCache.repositories, + cached: true, + stale: true, + fetchedAt: new Date(githubRepositoriesCache.fetchedAt).toISOString(), + }); + return; + } + + sendJson(res, 502, { error: "Failed to load repositories from GitHub" }); + } + return; + } + if (req.method === "GET" && pathname === "/api/stats") { const stats = await computeRunStats(store); sendJson(res, 200, stats); diff --git a/src/dashboard/html.ts b/src/dashboard/html.ts index 65a1297..4af1033 100644 --- a/src/dashboard/html.ts +++ b/src/dashboard/html.ts @@ -317,6 +317,15 @@ export function dashboardHtml(config: AppConfig): string { .modal select { cursor: pointer; } .modal textarea { min-height: 80px; resize: vertical; } .modal input:focus, .modal textarea:focus, .modal select:focus { border-color: var(--ring); } + .modal-subactions { display: flex; gap: 8px; justify-content: space-between; margin-top: 8px; flex-wrap: wrap; } + .modal-inline-btn { + border: 1px solid var(--border); background: var(--button-bg); color: var(--text); + border-radius: 8px; padding: 6px 10px; font-size: 12px; font-weight: 600; + cursor: pointer; font-family: var(--font-ui); + } + .modal-inline-btn:hover { background: var(--button-bg-hover); } + .modal-inline-btn:disabled { opacity: 0.6; cursor: default; } + .modal-help { margin-top: 6px; font-size: 11px; color: var(--muted); line-height: 1.4; } .modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; } .modal-btn { border: 1px solid var(--border); background: var(--button-bg); color: var(--text); @@ -1642,8 +1651,21 @@ export function dashboardHtml(config: AppConfig): string {