29a59bb8b319

give backups their own operator sidebar section (v3.0.129)

time
2026-09-29 21:33 UTC · 1 week ago
machine
Junes-MacBook-Pro.local
agent
gpt-6-sol
session
dedicated backup workspace
parent
88d31110724c
4 files package.json+1 −1src/admin/console.ts+27 −11tests/admin.test.ts+5 −0tests/backup.test.ts+1 −1
package.json+1 −1
@@ -1,6 +1,6 @@11 {22 	"name": "kepr",3-	"version": "3.0.128",3+	"version": "3.0.129",44 	"private": true,55 	"type": "module",66 	"scripts": {

view file

src/admin/console.ts+27 −11
@@ -17,6 +17,7 @@1717 	["/admin/repos", "repos"],1818 	["/admin/issues", "issues"],1919 	["/admin/releases", "releases"],20+	["/admin?section=backups", "backups"],2021 	["/admin/site?section=profile", "profile"],2122 	["/admin/site", "site"],2223 ] as const;@@ -44,9 +45,11 @@4445 	const tab = ([href, name]: (typeof SECTIONS)[number]) =>4546 		html`<a href="${href}"${4647 			(47-				c.req.path === "/admin/site"48-					? (c.req.query("section") === "profile" ? "profile" : "site") === name49-					: c.req.path === href || (href !== "/admin" && c.req.path.startsWith(`${href}/`))48+				c.req.path === "/admin"49+					? (c.req.query("section") === "backups" ? "backups" : "overview") === name50+					: c.req.path === "/admin/site"51+						? (c.req.query("section") === "profile" ? "profile" : "site") === name52+						: c.req.path === href || (href !== "/admin" && c.req.path.startsWith(`${href}/`))5053 			)5154 				? html` aria-current="page"`5255 				: ""@@ -112,6 +115,26 @@112115 /** §24's Status section at `/admin`: the bucket, each repo's format, backups, the running version. */113116 export function registerStatus(app: Hono, ctx: AdminContext): void {114117 	app.get("/admin", async (c) => {118+		if (c.req.query("section") === "backups") {119+			const newest = backups(ctx.dataDir).at(-1);120+			const backup = newest121+				? html`<p>Last backup: ${newest.replace(122+						/^(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)Z$/,123+						"$1-$2-$3 $4:$5:$6 UTC",124+					)}. <a href="/admin/backup">download backup</a></p>`125+				: html`<p>No backup yet.</p>`;126+			return c.html(127+				adminPage(128+					c,129+					ctx,130+					"backups",131+					html`${backup}132+<p>Kepr keeps the last 14 daily backups of its database and assets.</p>133+<p class="muted">Backups stay on this server. Keep a copy elsewhere, along with kepr.toml.</p>134+<p class="muted">Sessions and bucket credentials are not included.</p>`,135+				),136+			);137+		}115138 		let bucket: Html;116139 		try {117140 			const repos = await listRepos(ctx.sync.bucket);@@ -166,20 +189,13 @@166189 	)}167190 ${repos.length === 0 && html`<p class="muted">No repositories yet. Offer one with Koh, then sync it here.</p>`}168191 </section>`;169-		const backup = newest170-			? html`<p>Last backup: ${newest.replace(171-					/^(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)Z$/,172-					"$1-$2-$3 $4:$5:$6 UTC",173-				)}. <a href="/admin/backup">download backup</a></p>`174-			: html`<p>No backup yet.</p>`;175192 		return c.html(176193 			adminPage(177194 				c,178195 				ctx,179196 				"overview",180197 				html`${overview}<section class="repo-section markdown">181-<h2 class="section-label mono">backup</h2>${backup}182-<p class="muted">Backups stay on this server. Keep a copy elsewhere, along with kepr.toml.</p>198+<p><a href="/admin?section=backups">manage backups →</a></p>183199 <details><summary class="mono">bucket and runtime</summary>${bucket}184200 ${ctx.canConnect ? connectedMachines(ctx) : ""}185201 <p>Running kepr ${version}: ${lines} lines of TypeScript.</p></details></section>`,

view file

tests/admin.test.ts+5 −0
@@ -70,6 +70,11 @@7070 		expect(text).toContain("Bucket reachable: 1 repo.");7171 		expect(text).toContain("format ok");7272 		expect(text).toMatch(/Running kepr 3\.0\.\d+: [\d,]+ lines of TypeScript/);73+		expect(text).toContain('href="/admin?section=backups"');74+		const backupPage = await (await browser.get("/admin?section=backups")).text();75+		expect(backupPage).toContain("<h1>backups</h1>");76+		expect(backupPage).toContain("Backups stay on this server.");77+		expect(backupPage).not.toContain("Bucket reachable:");7378 	});7479 7580 	test("a wrong password, a foreign origin or a missing sign-in cookie is refused", async () => {

view file

tests/backup.test.ts+1 −1
@@ -118,7 +118,7 @@118118 		expect(modes).toEqual([0o600, 0o600]);119119 		expect(extracted).toEqual(["assets", `assets/${signature}`, "kepr.db"]);120120 		expect(fresh.kepr.query("SELECT COUNT(*) AS n FROM sessions").get()).toEqual({ n: 0 });121-		expect(await (await operator.get("/admin")).text()).toContain(121+		expect(await (await operator.get("/admin?section=backups")).text()).toContain(122122 			'Last backup: 2026-09-26 12:01:00 UTC. <a href="/admin/backup">download backup</a>',123123 		);124124 		const stale = new Browser(fresh.app);

view file