// c21 — handlers: /projects cluster. Landing page lists every active // project from the SQLite store, /projects/new accepts a `owner/repo` // form (GitHub source-of-truth check + upsert), /projects/:owner/:name // renders the per-project detail page. Extracted from c21_app.ts per // the SAMA Atomic rule. import { parseUrl } from "./c14_request_parse.ts"; import { renderPage, renderNotFound, htmlResponse, } from "./b51_render_layout.ts"; import { projectsLandingMd, projectRegisterMd, projectDetailMd, } from "./b51_render_projects.ts"; import { parseRepoIdentifier } from "./a31_project_config.ts"; import { fetchProjectConfig } from "./c14_github.ts"; import { listActiveProjects, getProject, upsertProject, } from "./c13_database.ts"; import { getViewer } from "./b32_session.ts"; export const projectsLandingHandler = async (): Promise => { const projects = listActiveProjects(); const html = await renderPage({ title: "Projects — tdd.md", description: "Real repos opted in to tdd.md scoring. Each project drops .tdd-md.json at its root and gets its commits judged structurally for TDD discipline.", bodyMarkdown: projectsLandingMd(projects), ogPath: "https://tdd.md/projects", }); return htmlResponse(html); }; export const projectsNewHandler = async (req: Request): Promise => { const viewer = await getViewer(req); if (req.method === "GET") { const urlR = parseUrl(req.url); const prefilled = urlR.ok ? (urlR.value.searchParams.get("repo") ?? undefined) : undefined; const html = await renderPage({ title: "Register a project — tdd.md", description: "Onboard a real repo for TDD-discipline scoring. Drops .tdd-md.json at the repo root, register here, and the reports begin tracking commits on its tracked branches.", bodyMarkdown: projectRegisterMd(viewer, prefilled), ogPath: "https://tdd.md/projects/new", noindex: true, }); return htmlResponse(html); } if (req.method !== "POST") return new Response("method not allowed", { status: 405 }); if (!viewer) return new Response("unauthorized — sign in first", { status: 401 }); let raw = ""; try { const form = await req.formData(); raw = String(form.get("repo") ?? "").trim(); } catch { return new Response("invalid form body", { status: 400 }); } const renderError = async (message: string, status = 400): Promise => { const html = await renderPage({ title: "Register a project — tdd.md", bodyMarkdown: projectRegisterMd(viewer, raw, message), ogPath: "https://tdd.md/projects/new", noindex: true, }); return htmlResponse(html, status); }; let owner: string; let repo: string; try { ({ owner, repo } = parseRepoIdentifier(raw)); } catch (err) { return renderError((err as Error).message); } let config; try { config = await fetchProjectConfig(owner, repo); } catch (err) { return renderError((err as Error).message); } upsertProject(viewer, owner, repo, config); return new Response(null, { status: 303, headers: { Location: `/projects/${owner}/${repo}` }, }); }; export const projectDetailHandler = async ( req: Request & { params: { repoOwner: string; repoName: string } }, ): Promise => { const { repoOwner, repoName } = req.params; const project = getProject(repoOwner, repoName); if (!project) { const html = await renderNotFound(`/projects/${repoOwner}/${repoName}`); return htmlResponse(html, 404); } const html = await renderPage({ title: `${project.displayName ?? `${project.repoOwner}/${project.repoName}`} — tdd.md`, description: `${project.repoOwner}/${project.repoName} on tdd.md — ${ project.testRunner === "none" ? "trace-mode" : project.testRunner } judging across ${project.trackedBranches.join(", ")}.`, bodyMarkdown: projectDetailMd(project), ogPath: `https://tdd.md/projects/${project.repoOwner}/${project.repoName}`, }); return htmlResponse(html); };