c94f94e93c10adfbc3ee82a86f8abaa216297808 diff --git a/Containerfile b/Containerfile index c8516be4edb8b4eb8f240d26bf1fb3b39e55a0db..02ecd24ff9aa83541d94a08aa39c3f18331ab977 100644 --- a/Containerfile +++ b/Containerfile @@ -21,8 +21,8 @@ COPY public ./public ENV PORT=3000 EXPOSE 3000 -# Healthcheck via /healthz (Bun's HTTP server start <100ms). -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget -q -O- http://localhost:3000/healthz || exit 1 +# No HEALTHCHECK — podman's OCI builds drop it with a warning, and our +# external probes (cloudflared upstream + the deploy script's /healthz +# poll) already cover liveness. CMD ["bun", "src/server.ts"] diff --git a/content/games/string-calc/spec.ts b/content/games/string-calc/spec.ts index fe1cc1b35b8cb2b09ae3caeeee8f7ad1bb94c3e7..df2bf0ae5ce08b0dcc9965a09d9315a69bf34439 100644 --- a/content/games/string-calc/spec.ts +++ b/content/games/string-calc/spec.ts @@ -2,6 +2,7 @@ import type { Game } from "../../../src/games"; export const spec: Game = { id: "string-calc", + description: "Add comma-separated numbers, one rule at a time. Seven steps.", signature: "add(numbers: string): number", importPath: "./add", steps: [ diff --git a/src/games.ts b/src/games.ts index edb78827f8d9450a9b178b51a795bd5bde340c91..4294a10a7aee4750b4009929f03dc9b8656bf2c8 100644 --- a/src/games.ts +++ b/src/games.ts @@ -10,6 +10,8 @@ export interface Step { export interface Game { id: string; + // One-line summary shown on the games index and OG previews. + description: string; // Human-readable function signature the agent must export. Documented // on the kata page so authors know what to build. signature: string; @@ -19,6 +21,30 @@ export interface Game { steps: Step[]; } +import { readdir } from "node:fs/promises"; + +// Reads every kata under content/games/ and returns the loaded specs in +// alphabetical order. Used to build the games index and sitemap without +// hard-coding individual kata ids. +export async function listGames(): Promise { + let entries; + try { + entries = await readdir("./content/games", { withFileTypes: true }); + } catch { + return []; + } + const ids = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort(); + const games: Game[] = []; + for (const id of ids) { + try { + games.push(await loadGame(id)); + } catch { + // skip katas that fail to load (missing spec.ts, etc.) + } + } + return games; +} + export async function loadGame(id: string): Promise { const file = Bun.file(`./content/games/${id}/spec.ts`); if (!(await file.exists())) { diff --git a/src/server.ts b/src/server.ts index 8dd0a3c552c95f753de867ea174276700474e0b7..80793e510783d3e607dbdd49d56d6c8a39edf761 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,7 +2,7 @@ import { renderPage, renderNotFound } from "./render"; import * as github from "./github_oauth"; import * as forgejo from "./forgejo"; import { parseCommit, computeProgress, type Phase } from "./commits"; -import { loadGame } from "./games"; +import { loadGame, listGames } from "./games"; import { judge } from "./judge"; import { latestRun, allLatestRuns } from "./db"; @@ -26,11 +26,16 @@ const HOME_HTML = await renderPage({ }, }); +const ALL_GAMES = await listGames(); + const gamesIndexBody = `# games -| kata | description | language | -|---|---|---| -| [string-calc](/games/string-calc) | Add comma-separated numbers, one rule at a time. Seven steps. | TypeScript | +${ALL_GAMES.length === 0 + ? "_No katas registered yet._" + : `| kata | description | steps |\n|---|---|---|\n${ALL_GAMES.map( + (g) => `| [${g.id}](/games/${g.id}) | ${g.description} | ${g.steps.length} |`, + ).join("\n")}` +} > Ready to play? [Register your agent →](/agents/register) `; @@ -472,11 +477,14 @@ const server = Bun.serve({ const today = new Date().toISOString().slice(0, 10); const url = (loc: string, priority: string) => `${loc}${today}${priority}`; + const kataUrls = ALL_GAMES.map((g) => + url(`https://tdd.md/games/${g.id}`, "0.8"), + ).join("\n"); const xml = ` ${url("https://tdd.md/", "1.0")} ${url("https://tdd.md/games", "0.9")} -${url("https://tdd.md/games/string-calc", "0.8")} +${kataUrls} ${url("https://tdd.md/agents", "0.7")} ${url("https://tdd.md/leaderboard", "0.7")} `; @@ -625,7 +633,7 @@ ${url("https://tdd.md/leaderboard", "0.7")} status: 302, headers: { Location: github.authorizeUrl(nonce, CALLBACK_URL), - "Set-Cookie": `tdd_oauth_state=${nonce}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=600`, + "Set-Cookie": `tdd_oauth_state=${nonce}; Path=/auth; HttpOnly; Secure; SameSite=Lax; Max-Age=600`, }, }); }, @@ -712,7 +720,7 @@ When you push, the judge replays your commits and posts the verdict at [/agents/ return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8", - "Set-Cookie": "tdd_oauth_state=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0", + "Set-Cookie": "tdd_oauth_state=; Path=/auth; HttpOnly; Secure; SameSite=Lax; Max-Age=0", }, }); },