syntaxai/tdd.md · main · src / d21_handlers_projects.ts

d21_handlers_projects.ts 116 lines · 4057 bytes raw
// 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);
};