console.ts

3.2 KB · raw

1import type { Context, Hono } from "hono";2import { checkFormat, listRepos } from "../bucket/repo.ts";3import type { SyncTarget } from "../bucket/sync.ts";4import { type Html, html } from "../pages/html.ts";5import { type PageContext, page } from "../pages/layout.ts";6import { formToken } from "../server/login.ts";78export type AdminContext = PageContext & { sync: SyncTarget; dataDir: string };910const SECTIONS = [11	["/admin", "status"],12	["/admin/repos", "repos"],13	["/admin/issues", "issues"],14	["/admin/releases", "releases"],15	["/admin/site", "site"],16] as const;1718/** The hidden same-site token (§25) that every admin form posts back. */19export function formField(c: Context): Html {20	return html`<input type="hidden" name="form" value="${formToken(c)}">`;21}2223/** An admin page: the site's own look, with the console's sections and sign-out on top. */24export function adminPage(c: Context, ctx: AdminContext, title: string, body: Html): string {25	const tab = ([href, name]: (typeof SECTIONS)[number]) =>26		html`<a class="issues-tab${c.req.path === href ? " active" : ""}" href="${href}">${name}</a>`;27	return page(ctx, {28		title: `${title} · admin`,29		description: "",30		path: c.req.path,31		body: html`<form class="issues-bar mono" method="post" action="/admin/sign-out">${SECTIONS.map(tab)}${formField(c)}<button class="issues-tab issues-new" type="submit">sign out</button></form>32<h1 class="file-page-title">${title}</h1>33${body}`,34	});35}3637// ponytail: counted once per process by walking the source, since there is no build step.38let runningLines: number | null = null;39async function sourceLines(): Promise<number> {40	if (runningLines !== null) return runningLines;41	let total = 0;42	const root = `${import.meta.dir}/../..`;43	for await (const path of new Bun.Glob("{src,scripts}/**/*.ts").scan(root)) {44		if (path.endsWith(".test.ts")) continue;45		const text = await Bun.file(`${root}/${path}`).text();46		total += text.split("\n").length - (text.endsWith("\n") ? 1 : 0);47	}48	runningLines = total;49	return total;50}5152/** §24's Status section at `/admin`: the bucket, each repo's format, and the running version. */53export function registerStatus(app: Hono, ctx: AdminContext): void {54	app.get("/admin", async (c) => {55		let bucket: Html;56		try {57			const repos = await listRepos(ctx.sync.bucket);58			const formats = await Promise.all(59				repos.map(async (repo) => {60					try {61						await checkFormat(ctx.sync.bucket, repo);62						return html`<li><span class="mono">${repo}</span> <span class="muted">format ok</span></li>`;63					} catch (error) {64						return html`<li><span class="mono">${repo}</span> <span class="issues-error">${String(error)}</span></li>`;65					}66				}),67			);68			bucket = html`<p>Bucket reachable: ${repos.length} repos.</p><ul>${formats}</ul>`;69		} catch (error) {70			bucket = html`<p class="issues-error" role="alert">Bucket unreachable: ${String(error)}</p>`;71		}72		const { version } = (await Bun.file(`${import.meta.dir}/../../package.json`).json()) as {73			version: string;74		};75		const lines = (await sourceLines()).toLocaleString("en-US");76		return c.html(77			adminPage(78				c,79				ctx,80				"status",81				html`<section class="repo-section markdown">${bucket}82<p>Running kepr ${version}: ${lines} lines of TypeScript.</p></section>`,83			),84		);85	});86}