e2328c2de1b6
show red open-issue counts beside amber handle links in repo headers (v3.1.19)
- time
- 2026-10-08 15:47 UTC · 4 days ago
- machine
- Junes-MacBook-Pro.local
- agent
- gpt-6
- session
- repo header issue indicators
- parent
- 542f2ac04155
↓ kepr-e2328c2d.tar.gzatlas at this save
files changed4 files
package.json+1 −1public/css/kepr.css+5 −5src/pages/repo.ts+10 −1tests/pages.test.ts+31 −1package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "kepr",3- "version": "3.1.18",3+ "version": "3.1.19",44 "private": true,55 "type": "module",66 "scripts": {public/css/kepr.css+5 −5
@@ -306,13 +306,9 @@306306 .repo-contributor {307307 margin: 4px 0 0;308308 font-size: 0.8125rem;309- color: var(--text-secondary);309+ color: var(--accent);310310 animation: fadeUp 280ms 40ms ease both;311311 }312-.repo-contributor a:hover {313- color: var(--accent);314- text-decoration-color: var(--accent);315-}316312 .repo-desc,317313 .repo-homepage {318314 margin: 0 0 10px;@@ -390,6 +386,10 @@390386 .repo-tab.active {391387 color: var(--accent);392388 border-bottom-color: var(--accent);389+}390+.repo-issue-count {391+ margin-left: 0.5em;392+ color: var(--red);393393 }394394 .repo-panel {395395 padding: 10px 0 16px;src/pages/repo.ts+10 −1
@@ -1,6 +1,7 @@11 import type { Hono } from "hono";22 import { isSafePath } from "../bucket/repo.ts";33 import { describeFile } from "../describe/describe.ts";4+import { issueCounts } from "../issues/store.ts";45 import {56 fileAt,67 filesAt,@@ -133,6 +134,7 @@133134 const steal = `koh steal ${ctx.domain}/${name}`;134135 const stats = repoStats(repo.mirror);135136 const handle = setting(ctx.kepr, "handle");137+ const openIssues = issueCounts(ctx.kepr, name).open;136138 // §21.4: the version is the newest release; with none, the version the project declares, by137139 // Koh's rule, at its newest save.138140 const version =@@ -162,7 +164,14 @@162164 <div class="repo-tabbar">163165 <nav class="repo-tabs" aria-label="Repository panels">${PANELS.map(164166 ([label, path]) =>165- html`<a class="repo-tab" href="/${name}/${path}" data-panel="${path}">${label}</a>`,167+ html`<a class="repo-tab" href="/${name}/${path}" data-panel="${path}">${label}${168+ path === "issues" &&169+ openIssues > 0 &&170+ html`<span class="repo-issue-count" aria-label="${plural(171+ openIssues,172+ "open issue",173+ )}">${openIssues}</span>`174+ }</a>`,166175 )}</nav>167176 <div class="install-lines">168177 <button type="button" class="repo-steal-btn" data-copy="${steal}">${steal}</button>tests/pages.test.ts+31 −1
@@ -1,3 +1,4 @@1+import type { Database } from "bun:sqlite";12 import { beforeAll, describe, expect, test } from "bun:test";23 import { mkdir, mkdtemp, writeFile } from "node:fs/promises";34 import { tmpdir } from "node:os";@@ -14,6 +15,7 @@1415 1516 let fx: Fixture;1617 let app: Hono;18+let kepr: Database;1719 let mirrorDir: string;1820 let tailSaves: string[];1921 const get = (path: string) => app.request(path);@@ -58,7 +60,7 @@5860 const dataDir = await mkdtemp(join(tmpdir(), "kepr-pages-"));5961 await mkdir(join(dataDir, "assets"));6062 await writeFile(join(dataDir, "assets", "plain.txt"), "asset\n");61- const kepr = openKeprDb(dataDir);63+ kepr = openKeprDb(dataDir);6264 mirrorDir = join(dataDir, "mirror");6365 for (const bucketDir of [fx.bucketDir, secretRepo.bucketDir, large.bucketDir, tail.bucketDir]) {6466 const bucket = openBucket({ kind: "folder", path: bucketDir });@@ -114,6 +116,34 @@114116 expect(historyIds(page)).toEqual([head(), second(), first()]);115117 expect(page).toContain('data-copy="koh steal kepr.example/demo"');116118 expect(page).toContain('href="/demo/releases" data-panel="releases">builds</a>');119+ });120+121+ test("the issues tab counts only this repo's visible open issues and hides zero", async () => {122+ kepr.run("BEGIN");123+ try {124+ kepr.run(`INSERT INTO issues (repo, number, title, body, type, filed_at, hidden, status) VALUES125+('demo', 1, 'one', 'body', 'bug', 1, 0, 'open'),126+('demo', 2, 'two', 'body', 'bug', 1, 0, 'open'),127+('demo', 3, 'three', 'body', 'bug', 1, 0, 'open'),128+('demo', 4, 'hidden', 'body', 'bug', 1, 1, 'open'),129+('demo', 5, 'closed', 'body', 'bug', 1, 0, 'closed'),130+('large', 1, 'another repo', 'body', 'bug', 1, 0, 'open')`);131+ expect(await body("/demo")).toContain(132+ '<span class="repo-issue-count" aria-label="3 open issues">3</span>',133+ );134+ kepr.run("UPDATE issues SET status = 'closed' WHERE repo = 'demo' AND number = 1");135+ expect(await body("/demo")).toContain(136+ '<span class="repo-issue-count" aria-label="2 open issues">2</span>',137+ );138+ kepr.run("UPDATE issues SET status = 'closed' WHERE repo = 'demo' AND number = 2");139+ expect(await body("/demo")).toContain(140+ '<span class="repo-issue-count" aria-label="1 open issue">1</span>',141+ );142+ kepr.run("UPDATE issues SET status = 'closed' WHERE repo = 'demo'");143+ expect(await body("/demo")).not.toContain('class="repo-issue-count"');144+ } finally {145+ kepr.run("ROLLBACK");146+ }117147 });118148 119149 test("a page past the last, or too large to bind, is page 1", async () => {