import type { Context, Hono } from "hono"; import { MAX_CHARS } from "../issues/spam.ts"; import { addEntry, ISSUE_TYPES, type IssueType, type OperatorFilter, type OperatorIssue, operatorIssue, operatorIssues, setClosed, setHidden, setType, threadEntries, } from "../issues/store.ts"; import { saveByPrefix } from "../mirror/queries.ts"; import { openMirror } from "../mirror/store.ts"; import { html } from "../pages/html.ts"; import { formatDate, relativeTime } from "../pages/layout.ts"; import { type AdminContext, adminPage, formField, notice, textFields } from "./console.ts"; const FILTERS: OperatorFilter[] = ["open", "closed", "hidden"]; const SAVE_PREFIX = /^[0-9a-f]{6,12}$/; const ISSUE_PATH = "/admin/issues/:repo/:n{[0-9]{1,9}}"; const unixNow = () => Math.floor(Date.now() / 1000); /** §24's Issues section: every repo's issues, and the operator's actions of §23. */ export function registerIssues(app: Hono, ctx: AdminContext): void { app.get("/admin/issues", (c) => { const filter = FILTERS.find((f) => f === c.req.query("filter")) ?? "open"; const tab = (f: OperatorFilter) => html`${f}`; const issues = operatorIssues(ctx.kepr, filter); const body = html`

${FILTERS.map(tab)}

${issues.length > 0 ? issues.map((issue) => html`${issue.title}${issue.repo} #${issue.number} · ${issue.type} · ${issue.status} · ${relativeTime(issue.filed_at)}`) : html`

no ${filter} issues

`}
`; return c.html(adminPage(c, ctx, "issues", body)); }); app.get(ISSUE_PATH, (c) => { const issue = operatorIssue(ctx.kepr, c.req.param("repo"), Number(c.req.param("n"))); return issue ? c.html(issuePage(c, ctx, issue, "")) : c.notFound(); }); app.post(ISSUE_PATH, async (c) => { const issue = operatorIssue(ctx.kepr, c.req.param("repo"), Number(c.req.param("n"))); if (!issue) return c.notFound(); const fields = await textFields(c); const refuse = (error: string) => c.html(issuePage(c, ctx, issue, error), 400); switch (fields.action) { case "reply": { const body = (fields.body ?? "").trim(); if (body === "" || body.length > MAX_CHARS) { return refuse(`A reply is 1 to ${MAX_CHARS.toLocaleString("en-US")} characters.`); } addEntry(ctx.kepr, issue.id, "operator", body, unixNow()); break; } case "close": { const prefix = (fields.fixed_in ?? "").trim(); if (prefix === "") { setClosed(ctx.kepr, issue.id, unixNow()); break; } const save = SAVE_PREFIX.test(prefix) ? saveByPrefix(openMirror(ctx.mirrorDir, issue.repo), prefix) : null; if (!save) return refuse(`No save ${prefix} in ${issue.repo}.`); setClosed(ctx.kepr, issue.id, unixNow(), save.id); break; } case "reopen": setClosed(ctx.kepr, issue.id, null); break; case "hide": case "unhide": setHidden(ctx.kepr, issue.id, fields.action === "hide"); break; case "type": if (!ISSUE_TYPES.includes(fields.type as IssueType)) return refuse("Pick a type."); setType(ctx.kepr, issue.id, fields.type as IssueType); break; default: return refuse("Unknown action."); } return c.redirect(`/admin/issues/${issue.repo}/${issue.number}`, 303); }); } function issuePage(c: Context, ctx: AdminContext, issue: OperatorIssue, error: string): string { const action = `/admin/issues/${issue.repo}/${issue.number}`; const button = (name: string, label: string) => html`
${formField(c)}
`; const post = (operator: boolean, at: number, text: string) => html`

${operator ? html`operator` : html`${issue.reporter_name || "anonymous"}`}${formatDate(at)}

${text}
`; const typeOption = (type: string) => html``; const body = html`${notice(error)}

${issue.repo} #${issue.number} · ${issue.status}${issue.fixed_in && ` · fixed in ${issue.fixed_in}`}${issue.hidden ? " · hidden" : ""} · ${issue.version && `v${issue.version} · `}filed ${formatDate(issue.filed_at)}

reporter: ${issue.reporter_name || "anonymous"} · contact: ${issue.contact_email || "none given"}

${post(false, issue.filed_at, issue.body)} ${threadEntries(ctx.kepr, issue.id).map((entry) => post(entry.author === "operator", entry.created_at, entry.body))}
${formField(c)}
${ issue.status === "open" ? html`
${formField(c)}
` : button("reopen", "reopen") }
${formField(c)}
${issue.hidden ? button("unhide", "unhide") : button("hide", "hide (spam)")}`; return adminPage(c, ctx, issue.title, body); }