notifications.ts

3.1 KB · raw

1import type { Hono } from "hono";2import { html } from "../pages/html.ts";3import { relativeTime } from "../pages/layout.ts";4import { NOTIFICATION_KINDS, type NotificationKind, setting } from "../server/db.ts";5import { type AdminContext, adminPage, formField, textFields } from "./console.ts";6import { saveSettings } from "./store.ts";78type Notification = { id: number; text: string; href: string; created_at: number; read: number };910export const LABELS: Record<NotificationKind, string> = {11	issue_filed: "an issue is filed",12	issue_followup: "a reporter adds a follow-up",13	sync_failed: "a repo's sync starts failing",14	backup_failed: "a backup starts failing",15};1617/** §24's Notifications section: the list, opening one, mark all read, and which kinds to raise. */18export function registerNotifications(app: Hono, ctx: AdminContext): void {19	app.get("/admin/notifications", (c) => {20		const rows = ctx.kepr21			.query<Notification, []>(22				"SELECT id, text, href, created_at, read FROM notifications ORDER BY id DESC",23			)24			.all();25		const row = (n: Notification) =>26			html`<a class="issue-row${n.read ? "" : " notification-unread"}" href="/admin/notifications/${String(n.id)}">27<span class="issue-title">${n.text}</span>28<span class="issue-meta mono">${n.read ? "" : "unread · "}${relativeTime(n.created_at)}</span></a>`;29		const kind = (k: NotificationKind) =>30			html`<label class="issues-label"><input type="checkbox" name="${k}" value="1"${31				setting(ctx.kepr, `notify_off_${k}`) ? "" : html` checked`32			}> ${LABELS[k]}</label>`;33		const body = html`<form method="post" action="/admin/notifications">34${formField(c)}<input type="hidden" name="action" value="read_all">35<button class="issues-submit" type="submit">mark all read</button></form>36<section class="repo-section">37${rows.length > 0 ? rows.map(row) : html`<p class="muted mono save-empty">nothing yet</p>`}38</section>39<section class="repo-section"><h2 class="section-label mono">tell me when</h2>40<form class="issues-form" method="post" action="/admin/notifications">41${formField(c)}<input type="hidden" name="action" value="preferences">42${NOTIFICATION_KINDS.map(kind)}43<button class="issues-submit" type="submit">save</button>44</form></section>`;45		return c.html(adminPage(c, ctx, "notifications", body));46	});4748	app.get("/admin/notifications/:id{[0-9]{1,9}}", (c) => {49		const found = ctx.kepr50			.query<{ href: string }, [number]>(51				"UPDATE notifications SET read = 1 WHERE id = ? RETURNING href",52			)53			.get(Number(c.req.param("id")));54		return found ? c.redirect(found.href, 303) : c.notFound();55	});5657	app.post("/admin/notifications", async (c) => {58		const fields = await textFields(c);59		if (fields.action === "read_all") {60			ctx.kepr.run("UPDATE notifications SET read = 1 WHERE read = 0");61		} else if (fields.action === "preferences") {62			// A box left unticked is a kind switched off; the setting records only that.63			saveSettings(64				ctx.kepr,65				Object.fromEntries(66					NOTIFICATION_KINDS.map((k) => [`notify_off_${k}`, fields[k] === "1" ? "" : "1"]),67				),68			);69		} else return c.text("Unknown action.", 400);70		return c.redirect("/admin/notifications?saved=1", 303);71	});72}