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
↓ kepr-29a59bb8.tar.gzatlas at this save
files changed4 files
package.json+1 −1src/admin/console.ts+27 −11tests/admin.test.ts+5 −0tests/backup.test.ts+1 −1package.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": {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>`,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 () => {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);