diff --git a/src/dashboard-server.ts b/src/dashboard-server.ts index df63cc9..1a8f3f9 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 @@ -569,15 +583,64 @@ export function startDashboardServer( orchestrator: config.orchestratorModel, browserVerify: config.browserVerifyModel, }, - agentCommandTemplate: config.agentCommandTemplate.length > 20 - ? config.agentCommandTemplate.slice(0, 20) + "..." - : config.agentCommandTemplate, + agentCommandTemplate: config.agentCommandTemplate, }, stats, }); 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..cf7ba59 100644 --- a/src/dashboard/html.ts +++ b/src/dashboard/html.ts @@ -254,11 +254,33 @@ export function dashboardHtml(config: AppConfig): string { color: var(--muted); margin: 0 0 10px; font-weight: 700; } .settings-row { - display: flex; justify-content: space-between; align-items: center; - padding: 6px 0; font-size: 13px; + display: flex; justify-content: space-between; align-items: flex-start; + gap: 12px; padding: 6px 0; font-size: 13px; + } + .settings-row .label { color: var(--muted); flex: 0 0 120px; } + .settings-row .value { + font-weight: 600; text-align: right; flex: 1 1 auto; min-width: 0; + overflow-wrap: anywhere; word-break: break-word; + } + .settings-row .value.compact { + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + } + .settings-row.stacked { + display: block; + } + .settings-row.stacked .label { + display: block; flex: none; margin-bottom: 6px; + } + .settings-row.stacked .value { + display: block; text-align: left; + } + .settings-row .value.settings-code { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 12px; + line-height: 1.45; + white-space: pre-wrap; + overflow-wrap: anywhere; } - .settings-row .label { color: var(--muted); } - .settings-row .value { font-weight: 600; text-align: right; max-width: 55%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .settings-badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; @@ -317,6 +339,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 +1673,21 @@ export function dashboardHtml(config: AppConfig): string {