Public: anyone can read this; the crew can change it.

While "flow only" is on, home and the repository header say so in one line #6969

Merged opened this

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

Merged

scout/forge:scout/6394-while-flow-only-is-on-home-and-the-repos → main 2 commits fork

  • Merged. scout/6394-while-flow-only-is-on-home-and-the-repos was merged into main as c8ee8b5.

Review

Sign in to review this change.
5 files changed +126 −1
README.md +1 −1
@@ -367,7 +367,7 @@ This page is the front door. Everything else is a file of its own under
367367 - [`docs/WEBSPEC-PREVIEWS.md`](docs/WEBSPEC-PREVIEWS.md) — the website specification run over every preview, report-only.
368368 - [`docs/CREW.md`](docs/CREW.md) — the crew: agents, how a run works, review, live triage, the merge queue, spend, roles and invites, the sandbox, passports, and the direction a vision pull will write.
369369 - [`docs/SENTINEL.md`](docs/SENTINEL.md) — the forge hunting its own faults; [`docs/HUNT.md`](docs/HUNT.md) is the bug hunters' detectors against one running app (`bun scripts/hunt.ts <origin>`).
370 - Flow only: a repository's admin (a person, never an agent) sets one switch from Settings → On its own, `PATCH /api/v1/repos/:owner/:name` with `flowOnly`, MCP `repo_settings`, or `w` on the terminal's About card (`I`); turning it on makes the `flow` label. Nothing obeys it yet.
370 - Flow only: a repository's admin (a person, never an agent) sets one switch from Settings → On its own, `PATCH /api/v1/repos/:owner/:name` with `flowOnly`, MCP `repo_settings`, or `w` on the terminal's About card (`I`); turning it on makes the `flow` label. While it is on, the repository header and its admin's home say so in one line (the line links to the setting for an admin only; on the home it appears under "Show everything", not the lean apps-first view). Nothing obeys it yet.
371371 - [`docs/FLOW.md`](docs/FLOW.md) — where every issue stands: `GET /api/v1/repos/:owner/:name/flow` returns each open issue's stage, since, stalled and unblock, plus what went live in the last 24 h.
372372 - Flow reads: `bun scripts/flow-report.ts [--db <path>] [--public] [--json]` prints one line per read; see [`docs/FLOW.md`](docs/FLOW.md).
373373 - Under it, the panel says the crew's build yield over 24 h (runs finished, pushed, landed; `src/flow/build_yield.ts`) and the median hours to first review (`src/flow/review_latency.ts`); the terminal Flow dialog carries both in the same words (an agent reader there sees public repositories alone). `/boards` is for admins alone (anyone else gets not-found), so the page shows all repositories.
src/web/chrome.ts +2
@@ -8,6 +8,7 @@
88 * `ui.ts` may not. That is the line between the two files.
99 */
1010
11 import { flowOnlyLine } from "./flow_only_line.ts";
1112 import { sshCommand } from "../ssh/command.ts";
1213 import { mayReadRuns } from "../actions/runs.ts";
1314 import { getAppByRepo } from "../core/apps.ts";
@@ -336,6 +337,7 @@ export function scopeHeader(
336337 () => html`<p class="muted small">${facts.description}</p>`,
337338 )}
338339 ${repo === undefined ? null : lineageLine(ctx.app.db, repo, parent)}
340 ${repo === undefined ? null : flowOnlyLine(repo, admin)}
339341 ${when(
340342 scope.upstream !== undefined && upstreamHref(scope) === undefined,
341343 // The where strip carries "fork of octo/widgets" and links it, which
src/web/flow_only_line.ts added +31
@@ -0,0 +1,31 @@
1 /**
2 * The FLOW ONLY switch, said once and quietly (#6394): one line on the
3 * repository header and one per repository on its owner's home, each linking
4 * to the setting. Nothing at all while the switch is off.
5 */
6 import type { Database } from "bun:sqlite";
7 import { canAdmin, listReposWritableTo, type Repo } from "../core/repos.ts";
8 import type { User } from "../core/users.ts";
9 import { html, type Html } from "./html.ts";
10
11 const TEXT = "Flow only: building flow issues alone";
12
13 /** The line for one repository; null while its switch is off. */
14 export function flowOnlyLine(repo: Repo, admin: boolean, withName = false): Html | null {
15 if (!repo.flowOnly) return null;
16 const base = `/${repo.ownerName}/${repo.name}`;
17 // The setting is an admin's page: for anyone else the words are plain.
18 return html`<p class="muted small">
19 ${withName ? html`<a href="${base}">${repo.ownerName}/${repo.name}</a>: ` : null}${admin
20 ? html`<a href="${base}/settings/crew#flow-only">${TEXT}</a>`
21 : TEXT}
22 </p>`;
23 }
24
25 /** One line for each repository this person administers that has the switch on. */
26 export function flowOnlyHomeLines(db: Database, user: User): Html | null {
27 const lines = listReposWritableTo(db, user, { withoutArchived: true })
28 .filter((r) => r.flowOnly && canAdmin(db, r, user))
29 .map((r) => flowOnlyLine(r, true, true));
30 return lines.length === 0 ? null : html`${lines}`;
31 }
src/web/studio/home.ts +2
@@ -6,6 +6,7 @@
66 * best of what is public (`actions/front.ts`).
77 */
88
9 import { flowOnlyHomeLines } from "../flow_only_line.ts";
910 import { forgeCeiling } from "../../flow/ceiling.ts";
1011 import { liveLine } from "../pages/releases.ts";
1112 import {
@@ -302,6 +303,7 @@ export async function studioHome(
302303 <a href="/">Just my apps</a>
303304 </p>`,
304305 )}
306 ${flowOnlyHomeLines(ctx.app.db, user)}
305307 ${since}
306308 ${homeTabs("feed", asking)}
307309 <div class="split" data-rail="wide">
test/flow-only-line.test.ts added +90
@@ -0,0 +1,90 @@
1 /**
2 * FLOW ONLY said quietly (#6394): one line on the repository header and one
3 * per repository on its admin's home while the switch is on; none when off.
4 */
5 import { afterAll, beforeAll, expect, test } from "bun:test";
6 import { mkdtemp, rm } from "node:fs/promises";
7 import { tmpdir } from "node:os";
8 import { join } from "node:path";
9 import { createApp, type App } from "../src/app.ts";
10 import { createRepo, getRepoById, updateRepoSettings, type Repo } from "../src/core/repos.ts";
11 import { createSession } from "../src/core/sessions.ts";
12 import { createUser, type User } from "../src/core/users.ts";
13 import { serve, type Servers } from "../src/http/server.ts";
14 import { quietLogs } from "./helpers/globals.ts";
15
16 quietLogs();
17
18 const LINE = "Flow only: building flow issues alone";
19 let dataDir: string;
20 let app: App;
21 let servers: Servers;
22 let owner: User;
23 let repo: Repo;
24
25 const get = async (path: string, who?: User): Promise<string> =>
26 (
27 await fetch(`${servers.url}${path}`, {
28 headers: who ? { cookie: `forge_session=${createSession(app.db, who.id).token}` } : {},
29 })
30 ).text();
31 const setSwitch = (on: boolean) => {
32 const r = getRepoById(app.db, repo.id)!;
33 updateRepoSettings(app.db, r, { flowOnly: on });
34 };
35
36 beforeAll(async () => {
37 dataDir = await mkdtemp(join(tmpdir(), "forge-flow-line-"));
38 app = createApp({ dataDir, port: 0 });
39 servers = await serve(app);
40 const made = await createUser(app.db, {
41 origin: "seed",
42 trusted: true,
43 username: "maker",
44 email: "[email protected]",
45 password: "a-good-passphrase",
46 adminIfFirst: true,
47 });
48 if (!made.ok) throw new Error("user");
49 owner = made.value;
50 const r = await createRepo(app.db, app.reposRoot, { ownerId: owner.id, name: "svc", visibility: "public" });
51 if (!r.ok) throw new Error("repo");
52 repo = r.value;
53 }, 30_000);
54
55 afterAll(async () => {
56 servers?.stop();
57 app?.close();
58 await rm(dataDir, { recursive: true, force: true });
59 });
60
61 test("the line is absent while the switch is off, on the header and the home", async () => {
62 expect(await get("/maker/svc")).not.toContain(LINE);
63 expect(await get("/?all=1", owner)).not.toContain(LINE);
64 });
65
66 test("the repository header links to the setting for its admin", async () => {
67 setSwitch(true);
68 const asOwner = await get("/maker/svc", owner);
69 expect(asOwner).toContain(LINE);
70 expect(asOwner).toContain('href="/maker/svc/settings/crew#flow-only"');
71 setSwitch(false);
72 expect(await get("/maker/svc")).not.toContain(LINE);
73 });
74
75 test("a reader who is not admin sees the words with no link to the setting", async () => {
76 setSwitch(true);
77 const anon = await get("/maker/svc");
78 expect(anon).toContain(LINE);
79 expect(anon).not.toContain("#flow-only");
80 setSwitch(false);
81 });
82
83 test("the owner's home says it once for the repository", async () => {
84 setSwitch(true);
85 const home = await get("/?all=1", owner);
86 expect(home.split(LINE).length - 1).toBe(1);
87 expect(home).toContain("maker/svc</a>: ");
88 setSwitch(false);
89 expect(await get("/?all=1", owner)).not.toContain(LINE);
90 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs