10e65d3e89e7

wip: put each repo's whole console workspace on one page behind one tab per repo, gate green but Codex review, §30 entry and deploy still to do; record the campaign in continuity (v3.0.198)

time
2026-10-03 02:22 UTC · 1 week ago
machine
Junes-MacBook-Pro.local
agent
claude-opus-5-5
session
lightning quick kepr
parent
9b19186bb949
7 files package.json+1 −1public/css/kepr.css+9 −13src/admin/repos.ts+44 −71tests/console.test.ts+14 −14tests/hidden.test.ts+1 −1tests/repoworkspace.test.ts+23 −27tests/triage.test.ts+1 −1
package.json+1 −1
@@ -1,6 +1,6 @@11 {22 	"name": "kepr",3-	"version": "3.0.197",3+	"version": "3.0.198",44 	"private": true,55 	"type": "module",66 	"scripts": {

view file

public/css/kepr.css+9 −13
@@ -2333,23 +2333,19 @@23332333 .admin-releases li {23342334 	padding: 4px 0;23352335 }2336-.admin-repos {2337-	display: grid;2338-	grid-template-columns: minmax(120px, 0.7fr) minmax(0, 1.3fr);2339-	gap: 24px;2340-	margin-top: 24px;2341-}2342-.admin-repos .issue-row {2336+.admin-repos-bar {2337+	display: flex;23432338 	flex-wrap: wrap;2339+	align-items: flex-start;2340+	justify-content: space-between;2341+	gap: 12px;2342+	margin-bottom: 16px;23442343 }2345-.admin-repos [aria-current] {2346-	border-left: 2px solid var(--accent);2347-	padding-left: 12px;2344+.admin-repos-bar .issues-bar {2345+	flex: 1 1 auto;2346+	padding-bottom: 0;23482347 }23492348 @media (max-width: 680px) {2350-	.admin-repos {2351-		grid-template-columns: minmax(0, 1fr);2352-	}23532349 	.admin-layout {23542350 		grid-template-columns: minmax(0, 1fr);23552351 		gap: 0;

view file

src/admin/repos.ts+44 −71
@@ -5,7 +5,7 @@55 import { operatorIssues } from "../issues/store.ts";66 import { languageLines, repoStats } from "../mirror/queries.ts";77 import { openMirror, quarantinedPaths } from "../mirror/store.ts";8-import { html } from "../pages/html.ts";8+import { type Html, html } from "../pages/html.ts";99 import { formatDate, plural } from "../pages/layout.ts";1010 import {1111 	deleteRelease,@@ -28,8 +28,6 @@2828 const MAX_LANGUAGES = 5;2929 const MAX_LANGUAGE = 30;3030 const MAX_HIDDEN = 100;31-const TABS = ["overview", "settings", "builds", "archives"] as const;32-type Tab = (typeof TABS)[number];3331 3432 /** Shown languages as typed, comma-separated; blanks between commas are dropped. */3533 function languageNames(value: string): string[] {@@ -42,8 +40,7 @@4240 /** §24's Repos section: visibility (§20), card settings, texts, builds, sync status and "sync now". */4341 export function registerRepos(app: Hono, ctx: AdminContext): void {4442 	app.get("/admin/repos", (c) => {45-		const tab = TABS.find((t) => t === c.req.query("tab")) ?? "overview";46-		return c.html(reposPage(c, ctx, "", c.req.query("repo"), tab));43+		return c.html(reposPage(c, ctx, "", c.req.query("repo")));4744 	});4845 4946 	app.post("/admin/sync", async (c) => {@@ -71,7 +68,7 @@7168 			hidden_paths: hidden.join("\n"),7269 		};7370 		const refuse = (error: string) =>74-			c.html(reposPage(c, ctx, `${repo.name}: ${error}`, repo.name, "settings"), 400);71+			c.html(reposPage(c, ctx, `${repo.name}: ${error}`, repo.name), 400);7572 		const texts = (["description", "details"] as const).map((field): [RepoText, string] => [7673 			field,7774 			(fields[field] ?? "").replace(/\r\n/g, "\n").trim(),@@ -101,7 +98,7 @@10198 		for (const [field, value] of texts) setRepoText(ctx.kepr, repo.name, field, value);10299 		// The sync hands the list to Koh (`handHidden`); the mirror has it before the response.103100 		if (edit.hidden_paths !== repo.hidden_paths) await ctx.syncer.applyHidden([repo.name]);104-		return c.redirect(`/admin/repos?repo=${repo.name}&tab=settings&saved=1`, 303);101+		return c.redirect(`/admin/repos?repo=${repo.name}&saved=1`, 303);105102 	});106103 107104 	// A build's displayed notes and hiding live in kepr.db; the record in the bucket is untouched.@@ -114,13 +111,13 @@114111 		if (notes.length > MAX_CHARS) {115112 			const limit = MAX_CHARS.toLocaleString("en-US");116113 			const error = `${repo.name} ${release.version}: notes are over ${limit} characters.`;117-			return c.html(reposPage(c, ctx, error, repo.name, "builds"), 400);114+			return c.html(reposPage(c, ctx, error, repo.name), 400);118115 		}119116 		// Notes equal to the record's are no override, so a later record edit would show through.120117 		const override = notes === release.recordNotes ? null : notes;121118 		setRelease(ctx.kepr, repo.name, release.version, fields.hidden === "1", override);122119 		return c.redirect(123-			`/admin/repos?repo=${repo.name}&tab=builds&saved=1#${repo.name}-${release.version}`,120+			`/admin/repos?repo=${repo.name}&saved=1#${repo.name}-${release.version}`,124121 			303,125122 		);126123 	});@@ -131,10 +128,10 @@131128 		const to = ((await textFields(c)).name ?? "").trim();132129 		const problem = await renameProblem(ctx, repo.name, to);133130 		if (problem) {134-			return c.html(reposPage(c, ctx, `${repo.name}: ${problem}`, repo.name, "settings"), 400);131+			return c.html(reposPage(c, ctx, `${repo.name}: ${problem}`, repo.name), 400);135132 		}136133 		renameRepo(ctx, repo.name, to);137-		return c.redirect(`/admin/repos?repo=${repo.name}&tab=settings`, 303);134+		return c.redirect(`/admin/repos?repo=${repo.name}#settings`, 303);138135 	});139136 140137 	app.post("/admin/repos/:repo/tarballs/delete", async (c) => {@@ -145,9 +142,9 @@145142 		} catch (error) {146143 			if (!(error instanceof BucketWriteError)) throw error;147144 			const message = `${repo.name}: ${error.message}`;148-			return c.html(reposPage(c, ctx, message, repo.name, "archives"), 503);145+			return c.html(reposPage(c, ctx, message, repo.name), 503);149146 		}150-		return c.redirect(`/admin/repos?repo=${repo.name}&tab=archives&saved=1`, 303);147+		return c.redirect(`/admin/repos?repo=${repo.name}&saved=1#archives`, 303);151148 	});152149 153150 	app.post("/admin/releases/:repo/:version/delete", async (c) => {@@ -155,10 +152,7 @@155152 		if (!found) return c.notFound();156153 		const { repo, mirror, release } = found;157154 		const refuse = (status: 400 | 503, error: string) =>158-			c.html(159-				reposPage(c, ctx, `${repo.name} ${release.version}: ${error}`, repo.name, "builds"),160-				status,161-			);155+			c.html(reposPage(c, ctx, `${repo.name} ${release.version}: ${error}`, repo.name), status);162156 		if ((await textFields(c)).confirm !== "1") return refuse(400, "tick the box to delete it.");163157 		try {164158 			await deleteRelease(ctx.sync.bucket, ctx.kepr, mirror, repo.name, release.version);@@ -166,7 +160,7 @@166160 			if (error instanceof BucketWriteError) return refuse(503, error.message);167161 			throw error;168162 		}169-		return c.redirect(`/admin/repos?repo=${repo.name}&tab=builds&saved=1`, 303);163+		return c.redirect(`/admin/repos?repo=${repo.name}&saved=1#builds`, 303);170164 	});171165 172166 	app.post("/admin/releases/:repo/:version/:platform/delete", async (c) => {@@ -176,13 +170,7 @@176170 		const { repo, mirror, release } = found;177171 		const refuse = (status: 400 | 503, error: string) =>178172 			c.html(179-				reposPage(180-					c,181-					ctx,182-					`${repo.name} ${release.version} ${platform}: ${error}`,183-					repo.name,184-					"builds",185-				),173+				reposPage(c, ctx, `${repo.name} ${release.version} ${platform}: ${error}`, repo.name),186174 				status,187175 			);188176 		if ((await textFields(c)).confirm !== "1") return refuse(400, "tick the box to delete it.");@@ -201,7 +189,7 @@201189 			throw error;202190 		}203191 		return c.redirect(204-			`/admin/repos?repo=${repo.name}&tab=builds&saved=1#${repo.name}-${release.version}`,192+			`/admin/repos?repo=${repo.name}&saved=1#${repo.name}-${release.version}`,205193 			303,206194 		);207195 	});@@ -218,32 +206,26 @@218206 	return repo && mirror && release ? { repo, mirror, release } : null;219207 }220208 221-function reposPage(222-	c: Context,223-	ctx: AdminContext,224-	error: string,225-	chosen: string | undefined,226-	tab: Tab,227-) {209+function reposPage(c: Context, ctx: AdminContext, error: string, chosen: string | undefined) {228210 	const repos = allRepos(ctx.kepr);229211 	const selected = repos.find((repo) => repo.name === chosen) ?? repos[0];212+	// One click per repo (June, 2026-10-02): the repos are the tabs, and each holds all of its own.213+	const tab = (repo: AdminRepo) => {214+		const current = repo.name === selected?.name;215+		return html`<a class="issues-tab${current ? " active" : ""}" href="/admin/repos?repo=${repo.name}"${216+			current ? html` aria-current="page"` : ""217+		}>${repo.name}${repo.public ? "" : html` <span class="muted">private</span>`}${218+			repo.last_sync_error ? html` <span class="issues-error">sync failed</span>` : ""219+		}</a>`;220+	};230221 	const body = html`${notice(error)}231-<p class="admin-intro">Choose a repository to manage its visibility and public page.</p>222+<div class="admin-repos-bar">223+<nav class="issues-bar mono" aria-label="repositories">${repos.map(tab)}</nav>232224 <form method="post" action="/admin/sync">233225 ${formField(c)}<button class="issues-submit" type="submit">sync now</button></form>226+</div>234227 ${repos.length === 0 && html`<p class="muted mono save-empty">no repos found in the bucket yet</p>`}235-<div class="admin-repos">236-<nav aria-label="repositories">237-${repos.map(238-	(repo) => html`<a class="issue-row" id="repo-${repo.name}"239-href="/admin/repos?repo=${repo.name}#repo-editor"${selected?.name === repo.name ? html` aria-current="page"` : ""}>240-<span>${repo.name}</span><span>${repo.public ? "public" : "private"}</span>241-<span class="issue-meta mono">${repo.last_sync_at === null ? "never synced" : `last sync ${formatDate(repo.last_sync_at)} UTC`}</span>242-${repo.last_sync_error && html`<span class="issues-error">${repo.last_sync_error}</span>`}</a>`,243-)}244-</nav>245-${selected && repoForm(c, ctx, selected, tab)}246-</div>`;228+${selected && repoWorkspace(c, ctx, selected)}`;247229 	return adminPage(c, ctx, "repos", body);248230 }249231 @@ -275,30 +257,25 @@275257 `;276258 }277259 278-function repoForm(c: Context, ctx: AdminContext, repo: AdminRepo, tab: Tab) {260+function repoWorkspace(c: Context, ctx: AdminContext, repo: AdminRepo) {279261 	const synced =280262 		repo.last_sync_at === null ? "never synced" : `last sync ${formatDate(repo.last_sync_at)} UTC`;281-	const link = (t: Tab) =>282-		html`<a class="issues-tab${t === tab ? " active" : ""}" href="/admin/repos?repo=${repo.name}&amp;tab=${t}"${283-			t === tab ? html` aria-current="page"` : ""284-		}>${t}</a>`;285-	const content = {286-		overview: () => overviewTab(ctx, repo),287-		settings: () => settingsTab(c, ctx, repo),288-		builds: () => buildsTab(c, ctx, repo),289-		archives: () => archivesTab(c, repo),290-	}[tab]();263+	const section = (id: string, body: Html | Html[]) =>264+		html`<section class="repo-section" id="${id}"><h2 class="section-label mono">${id}</h2>265+${body}</section>`;291266 	return html`<article class="issue-post" id="repo-editor">292267 <p class="issue-post-head mono">293268 <span class="issue-author">294269 ${repo.public ? html`<a href="/${repo.name}">${repo.name}</a>` : repo.name}</span>295270 <span>${repo.public ? "public" : "private"}</span><span class="history-when">${synced}</span></p>296-<nav class="issues-bar mono" aria-label="${repo.name} workspace">${TABS.map(link)}</nav>297-${content}271+${section("overview", overviewSection(ctx, repo))}272+${section("settings", settingsSection(c, ctx, repo))}273+${section("builds", buildsSection(c, ctx, repo))}274+${section("archives", archivesSection(c, repo))}298275 </article>`;299276 }300277 301-function overviewTab(ctx: AdminContext, repo: AdminRepo) {278+function overviewSection(ctx: AdminContext, repo: AdminRepo) {302279 	const quarantined = quarantinedPaths(openMirror(ctx.mirrorDir, repo.name));303280 	return html`${workspace(ctx, repo)}304281 ${@@ -310,7 +287,7 @@310287 }`;311288 }312289 313-function settingsTab(c: Context, ctx: AdminContext, repo: AdminRepo) {290+function settingsSection(c: Context, ctx: AdminContext, repo: AdminRepo) {314291 	const own = repoText(ctx.kepr, repo.name);315292 	const input = (name: keyof RepoEdit, label: string) =>316293 		html`<label class="issues-label" for="${repo.name}-${name}">${label}</label>@@ -355,24 +332,20 @@355332 </form>`;356333 }357334 358-function buildsTab(c: Context, ctx: AdminContext, repo: AdminRepo) {335+function buildsSection(c: Context, ctx: AdminContext, repo: AdminRepo) {359336 	const releases = operatorReleases(ctx.kepr, openMirror(ctx.mirrorDir, repo.name), repo.name);360-	return html`<section class="repo-section" id="builds">361-${362-	releases.length > 0337+	return releases.length > 0363338 		? releases.map((release) => releaseForm(c, repo.name, release))364-		: html`<p class="muted mono save-empty">no builds yet</p>`365-}</section>`;339+		: html`<p class="muted mono save-empty">no builds yet</p>`;366340 }367341 368-function archivesTab(c: Context, repo: AdminRepo) {369-	return html`<section class="repo-section">370-<form class="issues-form" method="post" action="/admin/repos/${repo.name}/tarballs/delete">342+function archivesSection(c: Context, repo: AdminRepo) {343+	return html`<form class="issues-form" method="post" action="/admin/repos/${repo.name}/tarballs/delete">371344 ${formField(c)}372345 <p class="issue-meta mono">Tarballs are kept in the bucket once downloaded, and rebuilt on the next download.373346 Each one is listed under <a href="/admin/storage">storage</a>.</p>374347 <button class="issues-submit" type="submit">delete kept tarballs</button>375-</form></section>`;348+</form>`;376349 }377350 378351 function textArea(repo: string, name: RepoText, label: string, rows: number, value: string) {

view file

tests/console.test.ts+14 −14
@@ -76,21 +76,21 @@7676 	afterAll(() => setSystemTime());7777 7878 	test("every repo the sync found is listed, private, with its last sync", async () => {79-		const page = await (await browser.get("/admin/repos?tab=settings")).text();80-		expect(page).toContain('id="repo-app"');81-		expect(page).toContain('id="repo-other"');79+		const page = await (await browser.get("/admin/repos")).text();80+		expect(page).toContain('href="/admin/repos?repo=app"');81+		expect(page).toContain('href="/admin/repos?repo=other"');8282 		expect(page.match(/action="\/admin\/repos\/[^/"]+"/g)?.length).toBe(1);8383 		expect(page).toContain("last sync 2026-09-26 12:00 UTC");8484 		expect((await statuses()).every((line) => line.endsWith(" 404"))).toBe(true);8585 	});8686 8787 	test("repo selection focuses one settings form without a search form", async () => {88-		const page = await (await browser.get("/admin/repos?repo=other&tab=settings")).text();88+		const page = await (await browser.get("/admin/repos?repo=other")).text();8989 		expect(page).toContain('action="/admin/repos/other"');90-		expect(page).toContain('id="repo-app"');90+		expect(page).toContain('href="/admin/repos?repo=app"');9191 		expect(page).not.toContain('type="search"');9292 		const saved = await publish("other", {});93-		expect(saved.headers.get("location")).toBe("/admin/repos?repo=other&tab=settings&saved=1");93+		expect(saved.headers.get("location")).toBe("/admin/repos?repo=other&saved=1");9494 		expect(await (await browser.get("/admin/repos?repo=other&saved=1")).text()).toContain(9595 			"Changes saved.",9696 		);@@ -136,7 +136,7 @@136136 		expect(await text("/")).not.toContain("tools");137137 		expect(await stat()).toEqual(["1", "1", "0", "0"]);138138 		// Settings survive the flip.139-		expect(await (await browser.get("/admin/repos?tab=settings")).text()).toContain(139+		expect(await (await browser.get("/admin/repos")).text()).toContain(140140 			'value="tools"',141141 		);142142 	});@@ -287,7 +287,7 @@287287 	});288288 289289 	test("every version is listed with its download counts and notes", async () => {290-		const page = await (await operator.get("/admin/repos?repo=app&tab=builds")).text();290+		const page = await (await operator.get("/admin/repos?repo=app")).text();291291 		expect(page).toContain('id="app-1.1.0"');292292 		expect(page).toContain('id="app-1.0.0"');293293 		expect(page.indexOf('id="app-1.1.0"')).toBeLessThan(page.indexOf('id="app-1.0.0"'));@@ -298,11 +298,11 @@298298 	test("edited notes replace the record's everywhere; the record's own text clears the edit", async () => {299299 		const saved = await edit("1.1.0", { notes: "Better <notes>." });300300 		expect(saved.headers.get("location")).toBe(301-			"/admin/repos?repo=app&tab=builds&saved=1#app-1.1.0",301+			"/admin/repos?repo=app&saved=1#app-1.1.0",302302 		);303303 		expect(JSON.parse(await text("/api/app/releases/latest")).notes).toBe("Better <notes>.");304304 		expect(await text("/app/releases")).toContain("Better &lt;notes&gt;.");305-		expect(await (await operator.get("/admin/repos?repo=app&tab=builds")).text()).toContain(305+		expect(await (await operator.get("/admin/repos?repo=app")).text()).toContain(306306 			"(edited)",307307 		);308308 		expect((await edit("1.1.0", { notes: "notes for 1.1.0" })).status).toBe(303);@@ -318,7 +318,7 @@318318 		expect(await text("/api/app/releases/history")).not.toContain("1.1.0");319319 		expect(await text("/app/releases")).not.toContain("1.1.0");320320 		expect((await inst.app.request("/app/releases/1.1.0/linux-x86_64")).status).toBe(404);321-		const page = await (await operator.get("/admin/repos?repo=app&tab=builds")).text();321+		const page = await (await operator.get("/admin/repos?repo=app")).text();322322 		expect(page).toContain('id="app-1.1.0"');323323 		expect(page).toContain('value="1" checked> hidden');324324 		expect((await edit("1.1.0", { notes: "notes for 1.1.0" })).status).toBe(303);@@ -361,7 +361,7 @@361361 362362 	test("the newest build goes, latest falls back, and a sync does not bring it back", async () => {363363 		const response = await remove("1.1.0");364-		expect(response.headers.get("location")).toBe("/admin/repos?repo=app&tab=builds&saved=1");364+		expect(response.headers.get("location")).toBe("/admin/repos?repo=app&saved=1#builds");365365 		expect(await Bun.file(join(bucketDir, "app/releases/1.1.0/release.json")).exists()).toBe(false);366366 		expect(await Bun.file(join(bucketDir, "app/releases/1.1.0/linux-x86_64/app")).exists()).toBe(367367 			false,@@ -370,7 +370,7 @@370370 		expect(JSON.parse(await text("/api/app/releases/latest")).version).toBe("1.0.0");371371 		expect(inst.kepr.query("SELECT * FROM downloads WHERE version = '1.1.0'").all()).toEqual([]);372372 		await operator.post("/admin/sync", {});373-		const page = await (await operator.get("/admin/repos?repo=app&tab=builds")).text();373+		const page = await (await operator.get("/admin/repos?repo=app")).text();374374 		expect(page).not.toContain('id="app-1.1.0"');375375 		expect(page).toContain('id="app-1.0.0"');376376 	});@@ -418,7 +418,7 @@418418 	test("the file leaves the record, the bucket, the counts and the public routes", async () => {419419 		const response = await removeFile("macos-aarch64");420420 		expect(response.headers.get("location")).toBe(421-			"/admin/repos?repo=app&tab=builds&saved=1#app-1.0.0",421+			"/admin/repos?repo=app&saved=1#app-1.0.0",422422 		);423423 		const files = (await record()).files.map((file: { platform: string }) => file.platform);424424 		expect(files).toEqual(["linux-x86_64"]);

view file

tests/hidden.test.ts+1 −1
@@ -75,7 +75,7 @@7575 		expect(listed).toContain("main.ts");7676 		expect(listed).not.toContain("tool.py");7777 		expect(await handed()).toBe("tool.py\nprivate\n");78-		expect(await (await browser.get("/admin/repos?repo=app&tab=settings")).text()).toContain(78+		expect(await (await browser.get("/admin/repos?repo=app")).text()).toContain(7979 			">tool.py\nprivate</textarea>",8080 		);8181 	});

view file

tests/repoworkspace.test.ts+23 −27
@@ -14,8 +14,7 @@1414 	let inst: Instance;1515 	let browser: Browser;1616 	let bucketDir: string;17-	const workspace = async (tab = "") =>18-		(await browser.get(`/admin/repos?repo=app${tab && `&tab=${tab}`}`)).text();17+	const workspace = async () => (await browser.get("/admin/repos?repo=app")).text();1918 	const save = (fields: Record<string, string>) =>2019 		browser.post("/admin/repos/app", { public: "1", ...fields });2120 @@ -57,27 +56,24 @@5756 		expect(page).toMatch(/<strong>2<\/strong><span class="mono">downloads<\/span>/);5857 	});5958 60-	test("splits into overview, settings, builds and archives tabs; overview is the default", async () => {61-		const tabs = ["overview", "settings", "builds", "archives"];62-		const marks: Record<string, string> = {63-			overview: 'class="admin-vitals"',64-			settings: 'name="hidden_paths"',65-			builds: 'action="/admin/releases/app/1.1.0"',66-			archives: 'action="/admin/repos/app/tarballs/delete"',67-		};68-		for (const tab of tabs) {69-			const page = await workspace(tab);70-			expect(page).toContain(`href="/admin/repos?repo=app&amp;tab=${tab}" aria-current="page"`);71-			for (const other of tabs) {72-				expect(page.includes(marks[other] as string)).toBe(other === tab);73-			}59+	test("one click per repo: its tab holds overview, settings, builds and archives together", async () => {60+		const page = await workspace();61+		expect(page).toContain('href="/admin/repos?repo=app" aria-current="page"');62+		for (const mark of [63+			'class="admin-vitals"',64+			'name="hidden_paths"',65+			'action="/admin/releases/app/1.1.0"',66+			'action="/admin/repos/app/tarballs/delete"',67+		]) {68+			expect(page).toContain(mark);7469 		}75-		expect(await workspace()).toContain(marks.overview as string);76-		expect(await workspace("nowhere")).toContain(marks.overview as string);70+		for (const id of ["overview", "settings", "builds", "archives"]) {71+			expect(page).toContain(`<section class="repo-section" id="${id}">`);72+		}7773 		const saved = await save({});78-		expect(saved.headers.get("location")).toBe("/admin/repos?repo=app&tab=settings&saved=1");74+		expect(saved.headers.get("location")).toBe("/admin/repos?repo=app&saved=1");7975 		const refused = await save({ languages: "a, b, c, d, e, f" });80-		expect(await refused.text()).toContain(marks.settings as string);76+		expect(await refused.text()).toContain('name="hidden_paths"');8177 	});8278 8379 	test("lists the detected languages with their lines", async () => {@@ -87,12 +83,12 @@8783 	});8884 8985 	test("edits the description and details; blank hands them back to the README", async () => {90-		expect(await workspace("settings")).toMatch(86+		expect(await workspace()).toMatch(9187 			/<textarea[^>]*name="description"[^>]*><\/textarea>/,9288 		);9389 		const saved = await save({ description: "Own *words*", details: "## Usage\r\nRun it." });9490 		expect(saved.status).toBe(303);95-		const page = await workspace("settings");91+		const page = await workspace();9692 		expect(page).toContain("Own *words*</textarea>");9793 		expect(page).toContain("## Usage\nRun it.</textarea>");9894 		const repo = await (await inst.app.request("/app")).text();@@ -112,7 +108,7 @@112108 		inst.kepr.run(113109 			"INSERT INTO release_settings (repo, version, hidden) VALUES ('app', '1.0.0', 1)",114110 		);115-		const page = await workspace("builds");111+		const page = await workspace();116112 		expect(page).toContain('action="/admin/releases/app/1.1.0"');117113 		expect(page).toContain("<span>shown</span><span>2 downloads</span>");118114 		expect(page).toContain("<span>hidden</span><span>0 downloads</span>");@@ -126,7 +122,7 @@126122 		const shown = await header();127123 		expect(shown).toContain('<span class="lang-badge">Zig</span>');128124 		expect(shown).toContain('<span class="lang-badge">TypeScript</span>');129-		expect(await workspace("settings")).toContain('value="Zig, TypeScript"');125+		expect(await workspace()).toContain('value="Zig, TypeScript"');130126 		expect((await save({ languages: "" })).status).toBe(303);131127 		const detected = await header();132128 		expect(detected).not.toContain('<span class="lang-badge">Zig</span>');@@ -144,7 +140,7 @@144140 			expect(response.status).toBe(400);145141 			expect(await response.text()).toContain(error);146142 		}147-		expect(await workspace("settings")).not.toContain('value="Zig');143+		expect(await workspace()).not.toContain('value="Zig');148144 	});149145 150146 	test("deletes the repo's kept tarballs from the bucket", async () => {@@ -154,9 +150,9 @@154150 		const kept = new Bun.Glob("**/*.tar.gz");155151 		const keptIn = () => Array.fromAsync(kept.scan(join(bucketDir, ".kepr", "tarballs", "app")));156152 		expect(await keptIn()).toHaveLength(1);157-		expect(await workspace("archives")).toContain('action="/admin/repos/app/tarballs/delete"');153+		expect(await workspace()).toContain('action="/admin/repos/app/tarballs/delete"');158154 		const deleted = await browser.post("/admin/repos/app/tarballs/delete", {});159-		expect(deleted.headers.get("location")).toBe("/admin/repos?repo=app&tab=archives&saved=1");155+		expect(deleted.headers.get("location")).toBe("/admin/repos?repo=app&saved=1#archives");160156 		expect(await keptIn()).toEqual([]);161157 		expect((await inst.app.request(head ?? "")).status).toBe(200);162158 	});

view file

tests/triage.test.ts+1 −1
@@ -114,7 +114,7 @@114114 			issues_paused: "1",115115 		});116116 		expect(pause.status).toBe(303);117-		expect(await (await operator.get("/admin/repos?repo=app&tab=settings")).text()).toMatch(117+		expect(await (await operator.get("/admin/repos?repo=app")).text()).toMatch(118118 			/name="issues_paused" value="1" checked/,119119 		);120120 		const form = await inst.app.request("/app/issues/new");

view file