releases.ts

3.4 KB · raw

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";910/**11 * §24's Releases section: versions with download counts, displayed notes and hiding. Everything12 * lives in kepr.db; nothing is deleted, since the bucket key is read-only.13 */14export 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		// Notes equal to the record's are no override, so a later record edit would show through.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}