4ffd21d3611b
remove install lines and card order; repos sort by name and old kepr.db columns are dropped (v3.0.140)
- time
- 2026-09-30 13:43 UTC · 1 week ago
- machine
- Junes-MacBook-Pro.local
- agent
- claude-opus-5-5
- session
- admin repo settings
- parent
- 3c4ab82be15c
↓ kepr-4ffd21d3.tar.gzatlas at this save
files changed11 files
package.json+1 −1src/admin/repos.ts+0 −14src/admin/store.ts+3 −19src/pages/repo.ts+1 −5src/server/db.ts+19 −20tests/backup.test.ts+1 −1tests/console.test.ts+3 −7tests/notifications.test.ts+1 −1tests/pages.test.ts+2 −4tests/repoworkspace.test.ts+17 −1tests/triage.test.ts+2 −3package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "kepr",3- "version": "3.0.139",3+ "version": "3.0.140",44 "private": true,55 "type": "module",66 "scripts": {src/admin/repos.ts+0 −14
@@ -12,7 +12,6 @@1212 1313 const MAX_FIELD = 500;1414 const HTTP_URL = /^https?:\/\/[^\s]+$/i;15-const SORT_ORDER = /^-?\d{1,6}$/;1615 const MAX_LANGUAGES = 5;1716 const MAX_LANGUAGE = 30;1817 @@ -43,10 +42,6 @@4342 public: fields.public === "1" ? 1 : 0,4443 category: text("category"),4544 homepage: text("homepage"),46- sort_order: Number(text("sort_order") || "0"),47- install1: text("install1"),48- install2: text("install2"),49- install3: text("install3"),5045 // Joined with the separator `publicRepo` splits on, so a name never contains it.5146 languages: languages.join(", "),5247 issues_paused: fields.issues_paused === "1" ? 1 : 0,@@ -58,10 +53,6 @@5853 if (edit.homepage !== "" && !HTTP_URL.test(edit.homepage)) {5954 return refuse("the homepage must be an http or https address.");6055 }61- if (!SORT_ORDER.test(text("sort_order") || "0")) return refuse("the order must be a number.");62- if ([edit.install1, edit.install2, edit.install3].some((line) => /[\r\n]/.test(line))) {63- return refuse("an install line is one line.");64- }6556 if (languages.length > MAX_LANGUAGES) return refuse("up to five languages.");6657 if (languages.some((name) => name.length > MAX_LANGUAGE)) {6758 return refuse(`a language name is at most ${MAX_LANGUAGE} characters.`);@@ -166,11 +157,6 @@166157 pause new issues</label>167158 ${input("category", "category")}168159 ${input("homepage", "homepage")}169-${input("sort_order", "card order")}170-<p class="issue-meta mono">Lower numbers appear first. Repositories with the same number sort by name.</p>171-${input("install1", "install line 1")}172-${input("install2", "install line 2")}173-${input("install3", "install line 3")}174160 ${input("languages", "shown languages")}175161 <p class="issue-meta mono">Up to five, separated by commas. Leave blank to show the detected one.</p>176162 <button class="issues-submit" type="submit">save ${repo.name}</button>src/admin/store.ts+3 −19
@@ -6,10 +6,6 @@66 public: number;77 category: string;88 homepage: string;9- sort_order: number;10- install1: string;11- install2: string;12- install3: string;139 /** Shown languages, joined by ", "; '' shows the detected one. */1410 languages: string;1511 issues_paused: number;@@ -20,7 +16,7 @@2016 export type RepoEdit = Omit<AdminRepo, "name" | "last_sync_at" | "last_sync_error">;2117 2218 export function allRepos(db: Database): AdminRepo[] {23- return db.query<AdminRepo, []>("SELECT * FROM repos ORDER BY sort_order, name").all();19+ return db.query<AdminRepo, []>("SELECT * FROM repos ORDER BY name").all();2420 }2521 2622 export function findRepo(db: Database, name: string): AdminRepo | null {@@ -29,21 +25,9 @@2925 3026 export function updateRepo(db: Database, name: string, edit: RepoEdit): void {3127 db.query(32- `UPDATE repos SET public = ?, category = ?, homepage = ?, sort_order = ?, install1 = ?,33- install2 = ?, install3 = ?, languages = ?, issues_paused = ?28+ `UPDATE repos SET public = ?, category = ?, homepage = ?, languages = ?, issues_paused = ?3429 WHERE name = ?`,35- ).run(36- edit.public,37- edit.category,38- edit.homepage,39- edit.sort_order,40- edit.install1,41- edit.install2,42- edit.install3,43- edit.languages,44- edit.issues_paused,45- name,46- );30+ ).run(edit.public, edit.category, edit.homepage, edit.languages, edit.issues_paused, name);4731 }4832 4933 /** Writes site settings in one transaction; an empty value removes the key, so defaults apply. */src/pages/repo.ts+1 −5
@@ -93,7 +93,7 @@9393 }9494 9595 function header(ctx: PageContext, repo: PublicRepo, intro: Html | null): Html {96- const { name, homepage, installLines, languages } = repo.settings;96+ const { name, homepage, languages } = repo.settings;9797 const steal = `koh steal ${ctx.domain}/${name}`;9898 const stats = repoStats(repo.mirror);9999 const handle = setting(ctx.kepr, "handle");@@ -127,10 +127,6 @@127127 )}</nav>128128 <div class="install-lines">129129 <button type="button" class="repo-steal-btn" data-copy="${steal}">${steal}</button>130-${installLines.map(131- (line) =>132- html`<div class="install-line"><code>${line}</code>${html`<button type="button" class="copy-btn" data-copy="${line}" hidden>copy</button>`}</div>`,133-)}134130 </div>135131 </div>136132 <div class="repo-panel" hidden></div>src/server/db.ts+19 −20
@@ -13,10 +13,6 @@1313 public INTEGER NOT NULL DEFAULT 0,1414 category TEXT NOT NULL DEFAULT '',1515 homepage TEXT NOT NULL DEFAULT '',16- sort_order INTEGER NOT NULL DEFAULT 0,17- install1 TEXT NOT NULL DEFAULT '',18- install2 TEXT NOT NULL DEFAULT '',19- install3 TEXT NOT NULL DEFAULT '',2016 languages TEXT NOT NULL DEFAULT '',2117 issues_paused INTEGER NOT NULL DEFAULT 0,2218 last_sync_at INTEGER,@@ -131,13 +127,19 @@131127 ["repos", "issues_paused", "INTEGER NOT NULL DEFAULT 0"],132128 ["issue_entries", "hidden", "INTEGER NOT NULL DEFAULT 0"],133129 ];130+// Columns an older kepr.db still has; card order and install lines were removed (June, 2026-09-30).131+const DROPPED_COLUMNS: [table: string, column: string][] = [132+ ["repos", "sort_order"],133+ ["repos", "install1"],134+ ["repos", "install2"],135+ ["repos", "install3"],136+];134137 135138 /** `description` and `details` are the operator's Markdown, or '' for the README's. */136139 export type RepoSettings = {137140 name: string;138141 category: string;139142 homepage: string;140- installLines: string[];141143 /** The operator's shown languages (§24), or none for the detected one. */142144 languages: string[];143145 /** Filing is paused (§23); the list, threads and follow-ups carry on. */@@ -152,9 +154,6 @@152154 name: string;153155 category: string;154156 homepage: string;155- install1: string;156- install2: string;157- install3: string;158157 languages: string;159158 issues_paused: number;160159 description: string | null;@@ -167,13 +166,18 @@167166 db.run("PRAGMA journal_mode = WAL");168167 db.run("PRAGMA busy_timeout = 5000");169168 db.run(SCHEMA);169+ // Every name below comes from the two lists above, never from input.170+ const has = (table: string, column: string) =>171+ db172+ .query<{ name: string }, []>(`PRAGMA table_info(${table})`)173+ .all()174+ .some((existing) => existing.name === column);170175 for (const [table, column, definition] of ADDED_COLUMNS) {171- // Both names come from the list above, never from input.172- const columns = db.query<{ name: string }, []>(`PRAGMA table_info(${table})`).all();173- if (!columns.some((existing) => existing.name === column)) {174- db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);175- }176+ if (!has(table, column)) db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);176177 }178+ for (const [table, column] of DROPPED_COLUMNS) {179+ if (has(table, column)) db.run(`ALTER TABLE ${table} DROP COLUMN ${column}`);180+ }177181 return db;178182 }179183 @@ -238,9 +242,7 @@238242 239243 export function publicRepoNames(db: Database): string[] {240244 return db241- .query<{ name: string }, []>(242- "SELECT name FROM repos WHERE public = 1 ORDER BY sort_order, name",243- )245+ .query<{ name: string }, []>("SELECT name FROM repos WHERE public = 1 ORDER BY name")244246 .all()245247 .map((row) => row.name);246248 }@@ -249,19 +251,16 @@249251 export function publicRepo(db: Database, name: string): RepoSettings | null {250252 const row = db251253 .query<RepoRow, [string]>(252- `SELECT name, category, homepage, install1, install2, install3, languages, issues_paused,253- description, details254+ `SELECT name, category, homepage, languages, issues_paused, description, details254255 FROM repos LEFT JOIN repo_text ON repo_text.repo = repos.name255256 WHERE name = ? AND public = 1`,256257 )257258 .get(name);258259 if (!row) return null;259- const installLines = [row.install1, row.install2, row.install3].filter((line) => line !== "");260260 return {261261 name: row.name,262262 category: row.category,263263 homepage: row.homepage,264- installLines,265264 languages: row.languages === "" ? [] : row.languages.split(", "),266265 issuesPaused: row.issues_paused === 1,267266 description: row.description ?? "",tests/backup.test.ts+1 −1
@@ -56,7 +56,7 @@5656 old = await instance(bucketDir);5757 operator = new Browser(old.app);5858 await operator.signIn();59- await operator.post("/admin/repos/app", { public: "1", category: "tools", sort_order: "3" });59+ await operator.post("/admin/repos/app", { public: "1", category: "tools" });6060 const site = new FormData();6161 for (const [key, value] of Object.entries({6262 site_name: "restored",tests/console.test.ts+3 −7
@@ -52,7 +52,7 @@5252 };5353 const text = async (path: string) => (await inst.app.request(path)).text();5454 const publish = (repo: string, fields: Record<string, string>) =>55- browser.post(`/admin/repos/${repo}`, { sort_order: "0", ...fields });55+ browser.post(`/admin/repos/${repo}`, fields);5656 5757 beforeAll(async () => {5858 setSystemTime(new Date(clock));@@ -96,8 +96,6 @@9696 test("bad settings are refused and change nothing", async () => {9797 for (const fields of <Record<string, string>[]>[9898 { homepage: "javascript:alert(1)" },99- { sort_order: "first" },100- { install1: "one\ntwo" },10199 { category: "x".repeat(501) },102100 ]) {103101 expect((await publish("app", { public: "1", ...fields })).status).toBe(400);@@ -112,13 +110,11 @@112110 public: "1",113111 category: "tools",114112 homepage: "https://app.example",115- install1: "nix profile add app",116113 });117114 expect(response.status).toBe(303);118115 const lines = await statuses();119116 expect(lines.filter((line) => line.endsWith(" 404"))).toEqual([]);120117 expect(await text("/app")).toContain("https://app.example");121- expect(await text("/app")).toContain("nix profile add app");122118 expect(await text("/")).toContain('href="/app"');123119 expect(await text("/browse")).toContain('href="/app"');124120 expect(await text("/@june")).toContain('href="/app"');@@ -171,7 +167,7 @@171167 inst = await instance(repos.bucketDir);172168 operator = new Browser(inst.app);173169 await operator.signIn();174- await operator.post("/admin/repos/app", { public: "1", sort_order: "0" });170+ await operator.post("/admin/repos/app", { public: "1" });175171 // A reporter files through the public form, as a browser would.176172 const reporter = new Browser(inst.app, "198.51.100.40");177173 const form = await (await reporter.request("/app/issues/new")).text();@@ -281,7 +277,7 @@281277 inst = await instance(repos.bucketDir);282278 operator = new Browser(inst.app);283279 await operator.signIn();284- await operator.post("/admin/repos/app", { public: "1", sort_order: "0" });280+ await operator.post("/admin/repos/app", { public: "1" });285281 await inst.app.request("/app/releases/1.1.0/linux-x86_64");286282 });287283 tests/notifications.test.ts+1 −1
@@ -31,7 +31,7 @@3131 inst = await instance(repos.bucketDir);3232 operator = new Browser(inst.app);3333 await operator.signIn();34- await operator.post("/admin/repos/app", { public: "1", sort_order: "0" });34+ await operator.post("/admin/repos/app", { public: "1" });3535 clear();3636 const reporter = new Browser(inst.app, "198.51.100.41");3737 const form = await (await reporter.request("/app/issues/new")).text();tests/pages.test.ts+2 −4
@@ -64,9 +64,7 @@6464 const bucket = openBucket({ kind: "folder", path: bucketDir });6565 await syncAll({ bucket, kepr, mirrorDir, tarballDir: join(dataDir, "tarballs") });6666 }67- kepr.run(68- "UPDATE repos SET public = 1, install1 = 'nix profile add x', homepage = 'https://demo.example' WHERE name = 'demo'",69- );67+ kepr.run("UPDATE repos SET public = 1, homepage = 'https://demo.example' WHERE name = 'demo'");7068 kepr.run("UPDATE repos SET public = 1 WHERE name IN ('large', 'tail')");7169 app = createApp(7270 {@@ -110,7 +108,7 @@110108 expect(page).toContain('href="https://demo.example"');111109 expect(page).toContain('<span class="lang-badge">TypeScript</span>');112110 expect(page).toContain("3 saves");113- expect(page).toContain('data-copy="nix profile add x"');111+ expect(page).not.toContain('class="install-line"');114112 expect(page.match(/class="dot d\d"/g)?.length).toBe(3);115113 expect(page).not.toContain("density = activity");116114 expect(historyIds(page)).toEqual([head(), second(), first()]);tests/repoworkspace.test.ts+17 −1
@@ -15,7 +15,7 @@1515 let browser: Browser;1616 const workspace = async () => (await browser.get("/admin/repos?repo=app")).text();1717 const save = (fields: Record<string, string>) =>18- browser.post("/admin/repos/app", { public: "1", sort_order: "0", ...fields });18+ browser.post("/admin/repos/app", { public: "1", ...fields });1919 2020 beforeAll(async () => {2121 const repos = await makeRepos(["app"]);@@ -119,4 +119,20 @@119119 expect(db.query<{ languages: string }, []>("SELECT languages FROM repos").get()?.languages).toBe(120120 "",121121 );122+});123+124+test("an older kepr.db loses card order and install lines, keeping each repo", async () => {125+ const dir = await mkdtemp(join(tmpdir(), "kepr-old-db-"));126+ const old = new Database(join(dir, "kepr.db"));127+ old.run(`CREATE TABLE repos (128+ name TEXT PRIMARY KEY, public INTEGER NOT NULL DEFAULT 0, sort_order INTEGER NOT NULL DEFAULT 0,129+ install1 TEXT NOT NULL DEFAULT '', install2 TEXT NOT NULL DEFAULT '',130+ install3 TEXT NOT NULL DEFAULT '')`);131+ old.run("INSERT INTO repos (name, public, install1) VALUES ('app', 1, 'nix profile add app')");132+ old.close();133+ const db = openKeprDb(dir);134+ const columns = db.query<{ name: string }, []>("PRAGMA table_info(repos)").all();135+ expect(columns.map((column) => column.name)).not.toContain("install1");136+ expect(columns.map((column) => column.name)).not.toContain("sort_order");137+ expect(db.query("SELECT name, public FROM repos").all()).toEqual([{ name: "app", public: 1 }]);122138 });tests/triage.test.ts+2 −3
@@ -40,7 +40,7 @@4040 operator = new Browser(inst.app);4141 await operator.signIn();4242 for (const repo of ["app", "tools"]) {43- await operator.post(`/admin/repos/${repo}`, { public: "1", sort_order: "0" });43+ await operator.post(`/admin/repos/${repo}`, { public: "1" });4444 }4545 file("app", "Crash on start", "A segfault in main", "bug");4646 file("app", "Dark mode", "Please", "idea");@@ -111,7 +111,6 @@111111 test("pausing new issues stops filing but keeps the list and follow-ups", async () => {112112 const pause = await operator.post("/admin/repos/app", {113113 public: "1",114- sort_order: "0",115114 issues_paused: "1",116115 });117116 expect(pause.status).toBe(303);@@ -131,7 +130,7 @@131130 expect(await text("/app/issues")).not.toContain("report an issue →");132131 expect(await text("/app/issues")).toContain('href="/app/issues/1"');133132 expect(await text(`/app/issues/3?token=${token}`)).toContain('name="body"');134- await operator.post("/admin/repos/app", { public: "1", sort_order: "0" });133+ await operator.post("/admin/repos/app", { public: "1" });135134 expect(await text("/app/issues/new")).toContain('name="title"');136135 });137136 });