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${49 sections.length > 050 ? sections51 : html`<p class="muted mono save-empty">52no releases in the bucket yet53</p>`54}`;55 return adminPage(c, ctx, "releases", body);56}5758function releaseForm(c: Context, repo: string, release: OperatorRelease) {59 const total = release.downloads.reduce((sum, [, n]) => sum + n, 0);60 const id = `${repo}-${release.version}`;61 return html`<article class="issue-post" id="${id}">62<p class="issue-post-head mono">63${html`<span class="issue-author">${release.version}</span>`}${html`<span>${release.hidden ? "hidden" : "shown"}</span>`}${html`<span>${plural(total, "download")}</span>`}${release.downloads.map(64 ([platform, n]) => html`<span class="muted">${platform} ${n}</span>`,65)}66</p>67<form class="issues-form" method="post" action="/admin/releases/${repo}/${release.version}">68${formField(c)}69<label class="issues-label" for="${id}-notes">notes70${release.notes !== release.recordNotes ? html` <span class="muted">(edited)</span>` : ""}</label>71<textarea class="issues-textarea" id="${id}-notes" name="notes" rows="3"72maxlength="${MAX_CHARS}">${release.notes}</textarea>73<label class="issues-label"><input type="checkbox" name="hidden" value="1"${release.hidden ? html` checked` : ""}> hidden</label>74<button class="issues-submit" type="submit">save ${release.version}</button>75</form>76</article>`;77}