1import type { Context, Hono } from "hono";2import { MAX_CHARS } from "../issues/spam.ts";3import { openMirror } from "../mirror/store.ts";4import { html } from "../pages/html.ts";5import { plural } from "../pages/layout.ts";6import { type OperatorRelease, operatorReleases, setRelease } from "../releases/releases.ts";7import { type AdminContext, adminPage, formField, notice, textFields } from "./console.ts";8import { allRepos, findRepo } from "./store.ts";91011121314export function registerReleases(app: Hono, ctx: AdminContext): void {15 app.get("/admin/releases", (c) => c.html(releasesPage(c, ctx, "")));1617 app.post("/admin/releases/:repo/:version", async (c) => {18 const repo = findRepo(ctx.kepr, c.req.param("repo"));19 const release =20 repo &&21 operatorReleases(ctx.kepr, openMirror(ctx.mirrorDir, repo.name), repo.name).find(22 (r) => r.version === c.req.param("version"),23 );24 if (!repo || !release) return c.notFound();25 const fields = await textFields(c);26 const notes = (fields.notes ?? "").replace(/\r\n/g, "\n");27 if (notes.length > MAX_CHARS) {28 const error = `${repo.name} ${release.version}: notes are over ${MAX_CHARS.toLocaleString("en-US")} characters.`;29 return c.html(releasesPage(c, ctx, error), 400);30 }31 32 const override = notes === release.recordNotes ? null : notes;33 setRelease(ctx.kepr, repo.name, release.version, fields.hidden === "1", override);34 return c.redirect(`/admin/releases#${repo.name}-${release.version}`, 303);35 });36}3738function releasesPage(c: Context, ctx: AdminContext, error: string): string {39 const sections = allRepos(ctx.kepr).flatMap((repo) => {40 const releases = operatorReleases(ctx.kepr, openMirror(ctx.mirrorDir, repo.name), repo.name);41 if (releases.length === 0) return [];42 return html`<section class="repo-section">43<h2 class="section-label mono">${repo.name}</h2>44${releases.map((release) => releaseForm(c, repo.name, release))}45</section>`;46 });47 const body = html`${notice(error)}48${sections.length > 0 ? sections : html`<p class="muted mono save-empty">no releases in the bucket yet</p>`}`;49 return adminPage(c, ctx, "releases", body);50}5152function releaseForm(c: Context, repo: string, release: OperatorRelease) {53 const total = release.downloads.reduce((sum, [, n]) => sum + n, 0);54 const id = `${repo}-${release.version}`;55 return html`<article class="issue-post" id="${id}">56<p class="issue-post-head mono"><span class="issue-author">${release.version}</span><span>${release.hidden ? "hidden" : "shown"}</span><span>${plural(total, "download")}</span>${release.downloads.map(([platform, n]) => html`<span class="muted">${platform} ${n}</span>`)}</p>57<form class="issues-form" method="post" action="/admin/releases/${repo}/${release.version}">58${formField(c)}59<label class="issues-label" for="${id}-notes">notes${release.notes !== release.recordNotes ? html` <span class="muted">(edited)</span>` : ""}</label>60<textarea class="issues-textarea" id="${id}-notes" name="notes" rows="3" maxlength="${MAX_CHARS}">${release.notes}</textarea>61<label class="issues-label"><input type="checkbox" name="hidden" value="1"${release.hidden ? html` checked` : ""}> hidden</label>62<button class="issues-submit" type="submit">save ${release.version}</button>63</form>64</article>`;65}