Live agent profile: list kata attempts with progress
/agents/:name now hits Forgejo for the user (404s cleanly if missing) and their repos, then computes per-attempt progress in parallel via the existing parseCommit pipeline. The page shows a table per kata: verified count out of the kata's total steps, and a colored red / green / refactor tally — same vocabulary as the repo page. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 file changed · +50 −3
src/server.ts
+50
−3
| @@ -341,10 +341,57 @@ const server = Bun.serve({ | ||
| 341 | 341 | "/agents": htmlResponse(AGENTS_INDEX_HTML), |
| 342 | 342 | "/agents/register": htmlResponse(REGISTER_HTML), |
| 343 | 343 | "/agents/:name": async (req) => { |
| 344 | + const name = req.params.name; | |
| 345 | + const userRes = await fetch(`${FORGEJO_INTERNAL}/api/v1/users/${encodeURIComponent(name)}`); | |
| 346 | + if (userRes.status === 404) { | |
| 347 | + const html = await renderPage({ | |
| 348 | + title: `${name} — agents — tdd.md`, | |
| 349 | + bodyMarkdown: `# agents / ${name}\n\n> No agent registered with this name.\n\n[← all agents](/agents) · [register your own →](/agents/register)`, | |
| 350 | + ogPath: `https://tdd.md/agents/${name}`, | |
| 351 | + active: "agents", | |
| 352 | + }); | |
| 353 | + return htmlResponse(html, 404); | |
| 354 | + } | |
| 355 | + const reposRes = await fetch(`${FORGEJO_INTERNAL}/api/v1/users/${encodeURIComponent(name)}/repos?limit=50`); | |
| 356 | + const repos = reposRes.ok ? ((await reposRes.json()) as { name: string; description: string }[]) : []; | |
| 357 | + | |
| 358 | + let body = `# agents / ${name}\n\n`; | |
| 359 | + if (repos.length === 0) { | |
| 360 | + body += "> Registered, but no kata attempts yet.\n\n[← all agents](/agents)"; | |
| 361 | + } else { | |
| 362 | + const progressByRepo = await Promise.all( | |
| 363 | + repos.map(async (r) => { | |
| 364 | + const cRes = await fetch(`${FORGEJO_INTERNAL}/api/v1/repos/${encodeURIComponent(name)}/${encodeURIComponent(r.name)}/commits?limit=50&stat=false`); | |
| 365 | + const commits = cRes.ok ? ((await cRes.json()) as { commit: { message: string } }[]) : []; | |
| 366 | + return { repo: r, progress: computeProgress(commits) }; | |
| 367 | + }), | |
| 368 | + ); | |
| 369 | + | |
| 370 | + const totals: Record<string, number> = {}; | |
| 371 | + for (const r of repos) { | |
| 372 | + try { | |
| 373 | + const game = await loadGame(r.name); | |
| 374 | + totals[r.name] = game.steps.length; | |
| 375 | + } catch { | |
| 376 | + // unknown kata, no total | |
| 377 | + } | |
| 378 | + } | |
| 379 | + | |
| 380 | + body += "## attempts\n\n"; | |
| 381 | + body += "| kata | verified | phases |\n|---|---|---|\n"; | |
| 382 | + for (const { repo: r, progress } of progressByRepo) { | |
| 383 | + const total = totals[r.name]; | |
| 384 | + const verified = progress.verifiedSteps.size; | |
| 385 | + const counter = total !== undefined ? `${verified} / ${total}` : `${verified} / ?`; | |
| 386 | + const phases = `<span class="red">red ${progress.redCount}</span> · <span class="green">green ${progress.greenCount}</span> · <span class="blue">refactor ${progress.refactorCount}</span>`; | |
| 387 | + body += `| [${r.name}](/${name}/${r.name}) | ${counter} | ${phases} |\n`; | |
| 388 | + } | |
| 389 | + } | |
| 390 | + | |
| 344 | 391 | const html = await renderPage({ |
| 345 | - title: `${req.params.name} — agents — tdd.md`, | |
| 346 | - bodyMarkdown: `# agents / ${req.params.name}\n\n> Not yet registered or no attempts.\n\nWhen this agent submits a run, their commits and verdicts will appear here.`, | |
| 347 | - ogPath: `https://tdd.md/agents/${req.params.name}`, | |
| 392 | + title: `${name} — agents — tdd.md`, | |
| 393 | + bodyMarkdown: body, | |
| 394 | + ogPath: `https://tdd.md/agents/${name}`, | |
| 348 | 395 | active: "agents", |
| 349 | 396 | }); |
| 350 | 397 | return htmlResponse(html); |