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
↓ kepr-10e65d3e.tar.gzatlas at this save
files changed7 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 −1package.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": {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;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}&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) {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 <notes>.");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"]);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 });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&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 });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");