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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/11587-forges-links-to-its-refused-first-asks-i → main 5 commits fork 1 thread · 1 unresolved

  • Merged. scout/11587-forges-links-to-its-refused-first-asks-i was merged into main as 82444c2.

Review

Sign in to review this change.
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[
5656 .all(n, now - KEEP_MS) as { id: number; at: number; actor_id: string | null; ask: string; words: string }[];
5757 return rows.map((r) => ({ id: r.id, at: r.at, actorId: r.actor_id, ask: r.ask, words: r.words }));
5858 }
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";
2222 import { listChildren } from "../../actions/spawn.ts";
2323 import { ownCode, removeForge, stopForge, type Managed } from "../../spawn/manage.ts";
2424 import { readOwnCode } from "../../spawn/own_code.ts";
25 import { countRefusals, mayReadRefusals } from "../../core/first_ask_refusals.ts";
2526 import { claimLinkBody } from "./new-forge.ts";
2627
2728
@@ -141,6 +142,13 @@ export function sideBySide(rows: readonly ClientRow[]): Html {
141142 </div>`;
142143 }
143144
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
144152 // GET /forges — an admin's only.
145153 export async function forgesPage(ctx: Ctx): Promise<Response> {
146154 const user = ctx.auth.user;
@@ -179,7 +187,7 @@ export async function forgesPage(ctx: Ctx): Promise<Response> {
179187 </ul>
180188 </section>`}
181189 ${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>
183191 </div>`,
184192 });
185193 notePhase(ctx.request, "render", performance.now() - rendering);
test/forges-refused.test.ts +18
@@ -82,3 +82,21 @@ describe("doors to /forges/refused", () => {
8282 expect(as({ id: "b", isAdmin: false })).toEqual([]);
8383 });
8484 });
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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs