73424a9e6ac2

restore Kepr 2 hierarchy and actions in repo islands (v3.0.120)

time
2026-09-29 20:54 UTC · 1 week ago
machine
Junes-MacBook-Pro.local
agent
gpt-6-sol
session
restore repo islands
parent
9af784d30135
9 files package.json+1 −1public/css/kepr.css+49 −6src/issues/pages.ts+23 −4src/pages/archives.ts+7 −2src/pages/releases.ts+5 −1tasks/todo.md+1 −1tests/issues.test.ts+7 −0tests/releases.test.ts+3 −0tests/tarball.test.ts+6 −0
package.json+1 −1
@@ -1,6 +1,6 @@11 {22 	"name": "kepr",3-	"version": "3.0.119",3+	"version": "3.0.120",44 	"private": true,55 	"type": "module",66 	"scripts": {

view file

public/css/kepr.css+49 −6
@@ -296,7 +296,7 @@296296 	display: none;297297 }298298 /* Inline, archives and builds show about five entries and scroll; their own pages show all. */299-.repo-panel .repo-section:has(> .archive-row, > .release) {299+.repo-panel .repo-section:has(> .archive-row, > .release-latest) {300300 	max-height: 420px;301301 	overflow-y: auto;302302 	scrollbar-width: thin;@@ -1235,6 +1235,10 @@12351235 	color: var(--accent);12361236 }12371237 .archive-links {1238+	display: flex;1239+	flex-wrap: wrap;1240+	align-items: center;1241+	gap: 12px;12381242 	margin: 6px 0;12391243 	font-size: 0.75rem;12401244 }@@ -1244,6 +1248,10 @@12441248 .archive-row .install-line {12451249 	justify-content: space-between;12461250 }1251+.archive-row--head {1252+	background: var(--accent-tint-04);1253+	border-top: 1px solid var(--accent-dim);1254+}12471255 12481256 /* ----- phone -------------------------------------------------------------- */12491257 @@ -1320,6 +1328,21 @@13201328 	font-size: 1rem;13211329 	color: var(--accent);13221330 }1331+.release-latest .release-version {1332+	font-size: clamp(2rem, 5vw, 3.5rem);1333+	letter-spacing: -0.06em;1334+}1335+.release-latest > p {1336+	font-size: 0.6875rem;1337+	margin: 16px 8px 0;1338+}1339+.release-older > summary {1340+	padding: 16px 8px;1341+	font-size: 0.75rem;1342+	color: var(--text-muted);1343+	cursor: pointer;1344+	border-bottom: 1px solid var(--border);1345+}13231346 .release-head .history-id:hover {13241347 	color: var(--accent);13251348 }@@ -1328,7 +1351,7 @@13281351 }13291352 .release-file {13301353 	padding: 10px 0;1331-	border-top: 1px dashed var(--border);1354+	border-top: 1px solid var(--border);13321355 }13331356 .release-file-head {13341357 	display: flex;@@ -1391,11 +1414,11 @@13911414 	color: var(--text-muted);13921415 }13931416 .issue-row {1394-	display: flex;1395-	flex-wrap: wrap;1417+	display: grid;1418+	grid-template-columns: auto minmax(0, 1fr) auto;13961419 	align-items: baseline;13971420 	gap: 2px 10px;1398-	padding: 10px 8px;1421+	padding: 16px 8px;13991422 	border-bottom: 1px solid var(--border);14001423 }14011424 .issue-row:hover {@@ -1403,7 +1426,8 @@14031426 	background: var(--accent-tint-04);14041427 }14051428 .issue-title {1406-	flex: 1;1429+	grid-column: 2 / -1;1430+	overflow-wrap: anywhere;14071431 	color: var(--text-heading);14081432 }14091433 .issues-new:hover,@@ -1421,6 +1445,20 @@14211445 .issue-symbol--closed {14221446 	color: var(--text-faint);14231447 }1448+.issue-row .issue-meta {1449+	grid-column: 2;1450+}1451+.issue-reporter {1452+	font-size: 0.6875rem;1453+	color: var(--text-muted);1454+	overflow-wrap: anywhere;1455+}1456+.issues-filter {1457+	display: flex;1458+	flex-wrap: wrap;1459+	gap: 8px;1460+	margin-bottom: 16px;1461+}14241462 .issue-post {14251463 	margin: 18px 0;14261464 	padding: 14px 18px;@@ -1483,6 +1521,11 @@14831521 .issues-textarea:focus {14841522 	outline: none;14851523 	border-color: var(--accent);1524+}1525+.issues-filter .issues-input {1526+	width: auto;1527+	min-width: 0;1528+	flex: 1 1 140px;14861529 }14871530 .issues-submit {14881531 	align-self: flex-start;

view file

src/issues/pages.ts+23 −4
@@ -41,7 +41,7 @@4141 		if (!repo) return notFound(c, ctx);4242 		const status = c.req.query("status") === "closed" ? "closed" : "open";4343 		cacheFor(c, false);44-		return c.html(listPage(ctx, repo, status));44+		return c.html(listPage(ctx, repo, status, c.req.query("q") ?? "", c.req.query("type") ?? ""));4545 	});4646 4747 	app.get("/:repo/issues/new", (c) => {@@ -170,10 +170,21 @@170170 <a href="/${repo}">${repo}</a> / <a href="/${repo}/issues">issues</a></p>`;171171 }172172 173-function listPage(ctx: PageContext, repo: PublicRepo, status: "open" | "closed"): string {173+function listPage(174+	ctx: PageContext,175+	repo: PublicRepo,176+	status: "open" | "closed",177+	query: string,178+	type: string,179+): string {174180 	const name = repo.settings.name;175181 	const counts = issueCounts(ctx.kepr, name);176-	const issues = listIssues(ctx.kepr, name, status);182+	const filter = ISSUE_TYPES.includes(type as IssueType) ? type : "";183+	const issues = listIssues(ctx.kepr, name, status).filter(184+		(issue) =>185+			(!filter || issue.type === filter) &&186+			issue.title.toLowerCase().includes(query.trim().toLowerCase()),187+	);177188 	const tab = (which: "open" | "closed") =>178189 		html`<a class="issues-tab${which === status ? " active" : ""}"179190 			href="/${name}/issues${which === "closed" ? "?status=closed" : ""}">@@ -186,6 +197,13 @@186197 <h1 class="file-page-title">issues</h1>187198 <p class="issues-bar mono">${tab("open")}${tab("closed")}188199 <a class="issues-new" href="/${name}/issues/new">report an issue →</a></p>200+<form class="issues-filter" method="get" action="/${name}/issues">201+<input type="hidden" name="status" value="${status}">202+<input class="issues-input" name="q" type="search" aria-label="search issue titles" placeholder="search issues" value="${query}">203+<select class="issues-input" name="type" aria-label="issue type">204+<option value="">all types</option>205+${ISSUE_TYPES.map((value) => html`<option value="${value}"${filter === value ? html` selected` : ""}>${value}</option>`)}206+</select><button class="issues-submit" type="submit">filter</button></form>189207 <section class="repo-section">190208 ${191209 	issues.length > 0@@ -196,9 +214,10 @@196214 ${issue.status === "open" ? "◉" : "◈"}</span>197215 <span class="issue-title">${issue.title}</span>198216 ${/* this line stays whole: splitting it changes the text tests/issues.test.ts asserts */ ""}<span class="issue-meta mono">#${issue.number} · ${issue.type}${issue.version && ` · v${issue.version}`} · ${relativeTime(issue.filed_at)}</span>217+<span class="issue-reporter mono">${issue.reporter_name || "anonymous"}</span>199218 </a>`,200219 			)201-		: html`<p class="muted mono save-empty">no ${status} issues</p>`220+		: html`<p class="muted mono save-empty">${query || filter ? "no matching issues" : `no ${status} issues`}</p>`202221 }203222 </section>`,204223 	});

view file

src/pages/archives.ts+7 −2
@@ -51,18 +51,23 @@5151 	const file = isHead ? "HEAD" : save.id;5252 	const tarball = `/${name}/archive/${file}.tar.gz`;5353 	const nix = `nix profile add https://${ctx.domain}${tarball}`;54+	const curl = `curl -fL https://${ctx.domain}${tarball} | tar xz`;55+	const koh = `koh steal ${ctx.domain}/${name}`;5456 	const hasFlake = fileAt(repo.mirror, save.id, "flake.nix") !== null;5557 	// HEAD can be a checkpoint, which has no page of its own (§21.4).5658 	const id = save.is_checkpoint5759 		? html`<span class="history-id">${save.id}</span>`5860 		: html`<a class="history-id" href="/${name}/saves/${save.id}">${save.id}</a>`;59-	return html`<div class="archive-row">61+	return html`<div class="archive-row${isHead ? " archive-row--head" : ""}">6062 <p class="archive-save">${6163 		isHead && html`<span class="archive-head mono">HEAD</span>`6264 	}${id}${html`<span class="history-msg archive-msg">${save.message.split("\n")[0]}</span>`}${html`<span class="history-when">${relativeTime(save.created_at)}</span>`}</p>6365 <p class="archive-links mono"><a href="${6466 		tarball65-	}">↓ ${name}-${isHead ? "HEAD" : save.id.slice(0, 8)}.tar.gz</a></p>67+	}">↓ ${name}-${isHead ? "HEAD" : save.id.slice(0, 8)}.tar.gz</a>68+<button type="button" class="copy-btn" data-copy="${save.id}" aria-label="copy save ${save.id}" hidden>hash</button>69+<button type="button" class="copy-btn" data-copy="${koh}" aria-label="copy Koh steal command" hidden>koh</button>70+<button type="button" class="copy-btn" data-copy="${curl}" aria-label="copy archive download command" hidden>curl</button></p>6671 ${6772 	hasFlake &&6873 	html`<div class="install-line"><code>${nix}</code><button type="button" class="copy-btn" data-copy="${nix}" hidden>copy</button></div>`

view file

src/pages/releases.ts+5 −1
@@ -46,7 +46,11 @@4646 <section class="repo-section">4747 ${4848 	releases.length > 049-		? releases.map((release) => releaseBlock(repo, release, counts))49+		? releases.map((release, index) =>50+				index === 051+					? html`<div class="release-latest"><p class="muted mono">latest release</p>${releaseBlock(repo, release, counts)}</div>`52+					: html`<details class="release-older"><summary class="mono">${index === 1 ? "earlier releases · " : ""}<span>v${release.version}</span> · ${formatDate(release.publishedAt)}</summary>${releaseBlock(repo, release, counts)}</details>`,53+			)5054 		: html`<p class="muted mono save-empty">no releases yet</p>`5155 }5256 </section>`,

view file

tasks/todo.md+1 −1
@@ -4,4 +4,4 @@44 - [x] Useful overview and focused, searchable repo management.55 - [ ] Resolve notification delivery scope and implement the approved inbox.66 - [ ] Gate, browser verification, continuity and atomic Koh saves.7-- [ ] Match the issues, builds and archives islands to Kepr 2's relevant designs.7+- [x] Match the issues, builds and archives islands to Kepr 2's relevant designs.

view file

tests/issues.test.ts+7 −0
@@ -204,6 +204,13 @@204204 		const list = await page("/app/issues");205205 		expect(list).toContain("Crash on &lt;start&gt;");206206 		expect(list).toContain("#1 · bug · v1.2.0");207+		expect(list).toContain('name="q"');208+		expect(list).toContain('name="type"');209+		expect(list).toContain("Ada");210+		expect(await page("/app/issues?q=missing")).not.toContain('href="/app/issues/1"');211+		expect(await page("/app/issues?type=question")).not.toContain('href="/app/issues/1"');212+		expect(await page("/app/issues?q=CRASH&type=bug")).toContain('href="/app/issues/1"');213+		expect(list).not.toContain("[email protected]");207214 		const thread = await page("/app/issues/1");208215 		expect(thread).toContain("It &lt;b&gt;crashed&lt;/b&gt;.");209216 		expect(thread).toContain('<span class="issue-author">Ada</span>');

view file

tests/releases.test.ts+3 −0
@@ -176,6 +176,9 @@176176 		const order = ["v0.11.0", "v0.10.0", "v0.9.0"].map((v) => body.indexOf(`>${v}<`));177177 		expect(order).toEqual([...order].sort((x, y) => x - y));178178 		expect(order[0]).toBeGreaterThan(0);179+		expect(body).toContain('class="release-latest"');180+		expect(body).toContain('class="release-older"');181+		expect(body).toContain("earlier releases");179182 		expect(body).toContain(`href="/tool/saves/${saves.tool}"`);180183 		expect(body).toContain("2026-09-22 18:00");181184 		expect(body).toContain("<h2>Fixes</h2>");

view file

tests/tarball.test.ts+6 −0
@@ -287,6 +287,12 @@287287 		const ids = [...page.matchAll(/class="history-id"[^>]*>([0-9a-f]{12})</g)].map((m) => m[1]);288288 		expect(ids).toEqual([head, fx.saveIds[0], fx.saveIds[2], fx.saveIds[3]]);289289 		expect(page).toContain('<span class="archive-head mono">HEAD</span>');290+		expect(page).toContain('class="archive-row archive-row--head"');291+		expect(page).toContain(`data-copy="${head}"`);292+		expect(page).toContain('data-copy="koh steal kepr.example/tarry"');293+		expect(page).toContain(294+			'data-copy="curl -fL https://kepr.example/tarry/archive/HEAD.tar.gz | tar xz"',295+		);290296 		expect(page).toContain('href="/tarry/archive/HEAD.tar.gz">↓ tarry-HEAD.tar.gz</a>');291297 		expect(page).toContain(292298 			'data-copy="nix profile add https://kepr.example/tarry/archive/HEAD.tar.gz"',

view file