Public: anyone can read this; the crew can change it.
/forges links to its refused first asks in one line, and the docs say where they are read #11710
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/11587-forges-links-to-its-refused-first-asks-i → main 5 commits fork 1 thread · 1 unresolved
4 files
changed +40
−1
docs/features/first-ask.md added +8
| @@ -0,0 +1,8 @@ | ||
| 1 | # Refused first asks (#11525) | |
| 2 | ||
| 3 | When the forge refuses a first ask, it keeps one row: who asked, the first 200 characters and the refusal's words. No claim code, token or email is kept. | |
| 4 | ||
| 5 | - **Where they are read:** `/forges/refused` (the newest 20, newest first), linked from `/forges` (with the count, once there is one) and from `/system`; the same rows are in the terminal. Only a forge admin who is a person may read them; anyone else gets what `/forges` gives. | |
| 6 | - **Capped at 20 per person per hour:** past that, the next refusal is dropped silently. | |
| 7 | - **Kept 30 days:** older rows are pruned on write and never listed. | |
| 8 | - Code: `src/core/first_ask_refusals.ts`, `src/web/pages/forges-refused.ts`; tests in `test/forges-refused.test.ts`. |
src/core/first_ask_refusals.ts +5
| @@ -56,3 +56,8 @@ export function listRefusals(db: Database, limit: number, now: number): Refusal[ | ||
| 56 | 56 | .all(n, now - KEEP_MS) as { id: number; at: number; actor_id: string | null; ask: string; words: string }[]; |
| 57 | 57 | return rows.map((r) => ({ id: r.id, at: r.at, actorId: r.actor_id, ask: r.ask, words: r.words })); |
| 58 | 58 | } |
| 59 | ||
| 60 | /** How many refusals are kept (none older than `KEEP_MS`): one COUNT, never the list. */ | |
| 61 | export function countRefusals(db: Database, now: number): number { | |
| 62 | return (db.query("SELECT COUNT(*) AS n FROM first_ask_refusals WHERE at >= ?1").get(now - KEEP_MS) as { n: number }).n; | |
| 63 | } |
src/web/pages/forges.ts +9 −1
| @@ -22,6 +22,7 @@ import { diskWords } from "../../spawn/disk_mount.ts"; | ||
| 22 | 22 | import { listChildren } from "../../actions/spawn.ts"; |
| 23 | 23 | import { ownCode, removeForge, stopForge, type Managed } from "../../spawn/manage.ts"; |
| 24 | 24 | import { readOwnCode } from "../../spawn/own_code.ts"; |
| 25 | import { countRefusals, mayReadRefusals } from "../../core/first_ask_refusals.ts"; | |
| 25 | 26 | import { claimLinkBody } from "./new-forge.ts"; |
| 26 | 27 | |
| 27 | 28 | |
| @@ -141,6 +142,13 @@ export function sideBySide(rows: readonly ClientRow[]): Html { | ||
| 141 | 142 | </div>`; |
| 142 | 143 | } |
| 143 | 144 | |
| 145 | /** The link to the refused first asks, only when there are any, with their count; it shares the paragraph of the other link so the page gains no block. */ | |
| 146 | function refusedLink(ctx: Ctx): Html | null { | |
| 147 | if (!mayReadRefusals(ctx.auth.user)) return null; | |
| 148 | const n = countRefusals(ctx.app.db, Date.now()); | |
| 149 | return n === 0 ? null : html` <a href="/forges/refused">Refused first asks (${n})</a>`; | |
| 150 | } | |
| 151 | ||
| 144 | 152 | // GET /forges — an admin's only. |
| 145 | 153 | export async function forgesPage(ctx: Ctx): Promise<Response> { |
| 146 | 154 | const user = ctx.auth.user; |
| @@ -179,7 +187,7 @@ export async function forgesPage(ctx: Ctx): Promise<Response> { | ||
| 179 | 187 | </ul> |
| 180 | 188 | </section>`} |
| 181 | 189 | ${await ownCodeSection(ctx)} |
| 182 | <p class="mt"><a href="/fleet/graph">Fleet graph</a></p> | |
| 190 | <p class="mt"><a href="/fleet/graph">Fleet graph</a>${refusedLink(ctx)}</p> | |
| 183 | 191 | </div>`, |
| 184 | 192 | }); |
| 185 | 193 | notePhase(ctx.request, "render", performance.now() - rendering); |
test/forges-refused.test.ts +18
| @@ -82,3 +82,21 @@ describe("doors to /forges/refused", () => { | ||
| 82 | 82 | expect(as({ id: "b", isAdmin: false })).toEqual([]); |
| 83 | 83 | }); |
| 84 | 84 | }); |
| 85 | ||
| 86 | describe("the link on /forges", () => { | |
| 87 | test("an admin sees one link, with the count, only when there are refusals", async () => { | |
| 88 | const app = fresh(); | |
| 89 | const admin = { id: "a", isAdmin: true, kind: "user" }; | |
| 90 | let body = await (await forgesPage(ctxOf(app, admin))).text(); | |
| 91 | expect(body).not.toContain('href="/forges/refused"'); | |
| 92 | recordRefusal(app.db, { actorId: "u1", ask: "x", words: "no" }, Date.now()); | |
| 93 | recordRefusal(app.db, { actorId: "u2", ask: "y", words: "no" }, Date.now()); | |
| 94 | body = await (await forgesPage(ctxOf(app, admin))).text(); | |
| 95 | expect(body.match(/href="\/forges\/refused"/g)).toHaveLength(1); | |
| 96 | expect(body).toContain("Refused first asks (2)"); | |
| 97 | }); | |
| 98 | test("a non-admin sees no link", async () => { | |
| 99 | const body = await (await forgesPage(ctxOf(fresh(), { id: "b", isAdmin: false }))).text(); | |
| 100 | expect(body).not.toContain("/forges/refused"); | |
| 101 | }); | |
| 102 | }); |
new above
offline