syntaxai/tdd.md · main · src / d21_handlers_projects.ts
// 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<Response> => {
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<Response> => {
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<Response> => {
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<Response> => {
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);
};