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

Home's Now line and the account menu's Repositories go where they say #8757

Merged opened this in answer to

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

Merged

scout/forge:scout/8743-home-s-now-line-and-the-account-menu-s-r → main 5 commits fork

  • Merged. scout/8743-home-s-now-line-and-the-account-menu-s-r was merged into main as 22e565e.

Review

Sign in to review this change.
18 files changed +546 −7
docs/design/audit.md +1
@@ -65,6 +65,7 @@ Machine routes are not pages and have no entry: `/audit.csv`,
6565 - `/pipeline` — `pages/pipeline.ts` · the four steps a change follows (check, review, land, live), for a newcomer · a pull page's step line, `/flow` · none seen.
6666 - `/map` — `pages/map.ts` (1,806 lines) · the dependency map of work across the forge · `/board`, issue dependency lists, `/:owner/:name/map` · colours 3, sizes 12: the largest private vocabulary.
6767 - `/audit` — `pages/audit.ts` · every app's contract, who can do what · the app page's one-line policy · none seen. Names the audit of apps, not this document.
68 - `/repos` — `pages/my-repos.ts` · the repositories you may write, from the account menu · `/` (your apps), `/:owner` (a profile's repositories) · none seen.
6869 - `/releases` — `pages/releases.ts` · the forge's kept releases and rollback · `/settings/backups` (both are "go back") · none seen.
6970 - `/changelog` — `pages/changelog.ts` · the last 30 releases with their dates and pulls, public · `/releases` (the signed-in kept releases) · none seen.
7071 - `/api` — `pages/api.ts` · the forge's OpenAPI operations by area, with search and Try it, public · the footer, the OpenAPI document, the terminal's API screen · none seen. An app's own spec is its `/:owner/:name/api` tab.
docs/features/now-pages.md added +4
@@ -0,0 +1,4 @@
1 # Now pages and Repositories
2
3 - Account menu → **Repositories** opens `/repos`: the repositories you may write, newest first.
4 - Home's Now line (site admins) links each word to what it counts: `/now/check`, `/now/review`, `/now/land`, `/now/live`. The list is `readNowRows` (`src/flow/now_rows.ts`), the same predicates `pipeline.ts` tallies, so the length is the number on home. `test/now-links.test.ts` walks both.
forge.json +1 −1
@@ -3,7 +3,7 @@
33 "api.anthropic.com"
44 ],
55 "app": {
6 "looks": ["/", "/forges", "/family", "/front", "/explore", "/board", "/boards", "/notes", "/feed", "/review", "/search", "/new", "/audit", "/releases", "/changelog", "/apps", "/migrate", "/map", "/system", "/talk", "/contribute", "/flow", "/resilience", "/settings"],
6 "looks": ["/", "/forges", "/family", "/front", "/explore", "/board", "/boards", "/notes", "/feed", "/review", "/search", "/new", "/audit", "/releases", "/changelog", "/apps", "/repos", "/map", "/system", "/talk", "/contribute", "/flow", "/resilience", "/settings"],
77 "run": "bun run src/main.ts",
88 "install": "",
99 "health": "/healthz",
src/core/reserved.ts +1
@@ -91,6 +91,7 @@ export const SYSTEM_NAMES: readonly string[] = [
9191 "oauth",
9292 "og",
9393 "organizations",
94 "now",
9495 "resilience",
9596 "origins",
9697 "robots.txt",
src/flow/now_rows.ts added +116
@@ -0,0 +1,116 @@
1 /**
2 * What each word of home's Now line counts, as the rows themselves: check,
3 * review, land and live. The same predicates as `pipeline.ts` tallies, so a
4 * list's length is the number on home. Plain data out, forge-wide: the
5 * caller has checked the viewer is a site admin, as home does.
6 */
7
8 import type { Database } from "bun:sqlite";
9
10 export const NOW_STAGES = ["check", "review", "land", "live"] as const;
11 export type NowStage = (typeof NOW_STAGES)[number];
12
13 export const NOW_WORDS: Record<NowStage, string> = {
14 check: "Check",
15 review: "Review",
16 land: "Land",
17 live: "Live",
18 };
19
20 /** What the word counts, in a line under the heading. */
21 export const NOW_HOLDS: Record<NowStage, string> = {
22 check: "Checks queued or running, across every repository.",
23 review: "Reviews queued or running on open pulls.",
24 land: "Open pulls approved and waiting to land.",
25 live: "Production deploys that went live in the last 24 hours.",
26 };
27
28 export interface NowRow {
29 readonly title: string;
30 readonly href: string;
31 /** `owner/name`. */
32 readonly repo: string;
33 readonly at: string;
34 /** What this row is, in a word: "pull #3", "main", "deploy". */
35 readonly note: string;
36 }
37
38 interface Raw {
39 owner: string;
40 name: string;
41 number: number | null;
42 title: string | null;
43 run_id: string | null;
44 env: string;
45 at: string;
46 }
47
48 const DAY_MS = 24 * 3_600_000;
49
50 // A queued run paused on a limit (`not_before` ahead) is not waiting for a worker: `runTally`'s rule.
51 const RUNS = (kind: string, where: string): string => `
52 SELECT u.username AS owner, r.name AS name, i.number AS number, i.title AS title,
53 x.id AS run_id, x.env AS env, x.created_at AS at
54 FROM run x JOIN repo r ON r.id = x.repo_id JOIN user u ON u.id = r.owner_id
55 LEFT JOIN issue i ON i.id = x.issue_id
56 WHERE x.kind = '${kind}' AND ${where}
57 AND (x.state = 'running' OR x.not_before IS NULL OR x.not_before <= ?1)
58 ORDER BY x.created_at DESC, x.id DESC`;
59
60 const SQL: Record<NowStage, string> = {
61 check: RUNS("check", "x.state IN ('queued','running')"),
62 review: RUNS("agent", "x.reason = 'review' AND x.state IN ('queued','running')"),
63 land: `
64 SELECT u.username AS owner, r.name AS name, i.number AS number, i.title AS title,
65 NULL AS run_id, '' AS env, v.created_at AS at
66 FROM issue i JOIN pull p ON p.issue_id = i.id
67 JOIN review v ON v.id = (SELECT id FROM review WHERE issue_id = i.id AND state != 'commented'
68 ORDER BY created_at DESC, id DESC LIMIT 1)
69 JOIN repo r ON r.id = i.repo_id JOIN user u ON u.id = r.owner_id
70 WHERE i.state = 'open' AND v.state = 'approved'
71 ORDER BY v.created_at DESC`,
72 live: `
73 SELECT u.username AS owner, r.name AS name, NULL AS number, NULL AS title,
74 x.id AS run_id, x.env AS env, x.serving_at AS at
75 FROM run x JOIN repo r ON r.id = x.repo_id JOIN user u ON u.id = r.owner_id
76 WHERE x.kind = 'deploy' AND x.env = 'production' AND x.serving_at >= ?1
77 ORDER BY x.serving_at DESC, x.id DESC`,
78 };
79
80 /** Rows read and listed: the queue can grow, a page of it cannot. */
81 export const NOW_LISTED = 200;
82
83 /** Whether the viewer is shown the Now line and its lists: a site admin who is a person, as home decides it. */
84 export const mayReadNow = (user: { isAdmin: boolean; kind: string } | undefined): boolean =>
85 user !== undefined && user.isAdmin && user.kind !== "agent";
86
87 export interface NowList {
88 /** What home counts for the stage. */
89 readonly total: number;
90 /** The newest `NOW_LISTED` of them; fewer than `total` only when the list is cut off. */
91 readonly rows: NowRow[];
92 }
93
94 /** The stage's items, newest first: `check` and `review` are runs; `land` is pulls; `live` is deploys. */
95 export function readNowRows(db: Database, stage: NowStage, now = new Date()): NowList {
96 const args: string[] = stage === "land" ? [] : [stage === "live" ? new Date(now.getTime() - DAY_MS).toISOString() : now.toISOString()];
97 const counted = db.query<{ n: number }, string[]>(`SELECT COUNT(*) AS n FROM (${SQL[stage]})`).get(...args);
98 const rows = db.query<Raw, string[]>(`${SQL[stage]} LIMIT ${NOW_LISTED}`).all(...args);
99 return {
100 total: counted?.n ?? 0,
101 rows: rows.map((r) => {
102 const repo = `${r.owner}/${r.name}`;
103 const base = `/${r.owner}/${r.name}`;
104 if (r.number !== null && stage === "land")
105 return { title: r.title ?? "", href: `${base}/pulls/${r.number}`, repo, at: r.at, note: `pull #${r.number}` };
106 const where = r.number !== null ? `pull #${r.number}` : r.env === "" ? "main" : r.env;
107 return {
108 title: r.title ?? (stage === "live" ? "Deploy to production" : `Check of ${where}`),
109 href: r.run_id === null ? base : `${base}/runs/${r.run_id}`,
110 repo,
111 at: r.at,
112 note: where,
113 };
114 }),
115 };
116 }
src/http/server.ts +4
@@ -25,6 +25,8 @@ import { settingsRoutes } from "./settings_routes.ts";
2525 import { boardsPage } from "../web/pages/boards.ts";
2626 import { boardNew, boardPin, boardPost, pinPost } from "../web/pages/boards-edit.ts";
2727 import { boardsHoldPost } from "../web/pages/boards-holds.ts";
28 import { nowPage } from "../web/pages/now.ts";
29 import { myReposPage } from "../web/pages/my-repos.ts";
2830 import { boardLive, boardShow, boardsList } from "../web/pages/boards-view.ts";
2931 import { actingVia } from "../core/acting_via.ts";
3032 import {
@@ -751,6 +753,8 @@ export function buildRouter(): Router<Handler> {
751753 // is registered before /:owner/:name can claim the shape.
752754 router.add("GET", "/-/confirm", confirmPage);
753755 router.add("GET", "/new", newRepoPage);
756 router.add("GET", "/repos", myReposPage);
757 router.add("GET", "/now/:stage", nowPage);
754758 router.add("GET", "/audit", auditPage);
755759 router.add("GET", "/releases", releasesPage);
756760 router.add("GET", "/changelog", changelogPage);
src/tui/screens/entries.ts +4
@@ -28,6 +28,8 @@ import { entry as limits } from "./limits.ts";
2828 import { modelCeilingEntry, modelGrantEntry } from "./model.ts";
2929 import { entry as orgGrants } from "./orggrants.ts";
3030 import { entry as orgTeams } from "./orgteams.ts";
31 import { entry as now } from "./nowlist.ts";
32 import { entry as repos } from "./repolist.ts";
3133 import { entry as owner } from "./owner.ts";
3234 import { entry as people } from "./people.ts";
3335 import { entry as pinned } from "./pinned.ts";
@@ -95,6 +97,8 @@ export const ENTRIES: readonly PaletteEntry[] = [
9597 owner,
9698 forges,
9799 profile,
100 now,
101 repos,
98102 ];
99103
100104 /** Put each entry the session is offered into `commands`, after the command it names or at the end. */
src/tui/screens/nowlist.ts added +66
@@ -0,0 +1,66 @@
1 /**
2 * Home's Now line as a screen, a site admin's (/now/:stage on the web): check,
3 * review, land and live, `1`–`4` to switch, each listing what the word counts
4 * from `readNowRows`, so the number is home's. Enter opens the row on the
5 * web. Anyone else, and an agent, is offered nothing.
6 */
7
8 import { mayReadNow, NOW_HOLDS, NOW_LISTED, NOW_STAGES, NOW_WORDS, readNowRows } from "../../flow/now_rows.ts";
9 import type { Ctx, Screen } from "../app/app.ts";
10 import { binding } from "../app/keys.ts";
11 import { style } from "../term/style.ts";
12 import { truncate } from "../term/width.ts";
13 import type { Session } from "../session.ts";
14 import { fill, len, split } from "../ui/layout.ts";
15 import { ListState, drawList, openOnWeb, type ListRow } from "./common.ts";
16 import type { PaletteEntry } from "./entries.ts";
17
18 export function nowScreen(f: Session, ctx: Ctx, first: (typeof NOW_STAGES)[number] = "check"): Screen {
19 const state = new ListState();
20 let stage = first;
21 const read = () => (mayReadNow(f.user) ? readNowRows(f.db, stage) : { total: 0, rows: [] });
22 const screen: Screen = {
23 title: "Now",
24 keys: [binding(["j", "k"], "move cursor"), binding(["1", "2", "3", "4"], "check, review, land, live"), binding(["enter"], "open on the web")],
25 render(area, s) {
26 const c = ctx.theme.c;
27 const { total, rows } = read();
28 const [head, list] = split(area, "v", [len(1), fill(1)]);
29 const words = NOW_STAGES.map((w) => (w === stage ? `[${NOW_WORDS[w]}]` : NOW_WORDS[w])).join(" ");
30 const cut = total > rows.length ? ` Newest ${NOW_LISTED} shown.` : "";
31 const text = mayReadNow(f.user) ? `${words} ${total} · ${NOW_HOLDS[stage]}${cut}` : "Now is a site admin's.";
32 s.text(head!.x, head!.y, truncate(text, head!.w), style(c.mutedFg, c.bg), head!.x + head!.w);
33 const shown: ListRow[] = rows.map((r) => ({ title: r.title, secondary: `${r.repo} · ${r.note}`, meta: r.at.slice(0, 16).replace("T", " ") }));
34 drawList(s, list!, c, state, shown);
35 },
36 key(e) {
37 if (!mayReadNow(f.user)) return false;
38 const { rows } = read();
39 if (state.key(e, rows.length)) return true;
40 if (e.ctrl || e.alt) return false;
41 const picked = NOW_STAGES[Number(e.key) - 1];
42 if (picked !== undefined) {
43 stage = picked;
44 state.index = 0;
45 state.offset = 0;
46 return true;
47 }
48 if (e.key === "enter" && rows[state.index] !== undefined) {
49 openOnWeb(f, ctx, rows[state.index]!.href, "That row");
50 return true;
51 }
52 return false;
53 },
54 };
55 return screen;
56 }
57
58 /** The Now line's lists, as /now/:stage has them: a site admin's. */
59 export const entry: PaletteEntry = {
60 id: "now",
61 title: "Now",
62 hint: "what check, review, land and live count, with the rows",
63 after: ["boards"],
64 offered: (session) => mayReadNow(session.user),
65 run: (session, ctx) => ctx.push(nowScreen(session, ctx)),
66 };
src/tui/screens/repolist.ts added +52
@@ -0,0 +1,52 @@
1 /**
2 * Your repositories (/repos on the web): every repository the viewer may
3 * write, from `listReposWritableTo`. Enter opens one as the dashboard does.
4 */
5
6 import { listReposWritableTo, type Repo } from "../../core/repos.ts";
7 import type { Ctx, Screen } from "../app/app.ts";
8 import { binding } from "../app/keys.ts";
9 import { style } from "../term/style.ts";
10 import { truncate } from "../term/width.ts";
11 import type { Session } from "../session.ts";
12 import { fill, len, split } from "../ui/layout.ts";
13 import { ListState, drawList, type ListRow } from "./common.ts";
14 import type { PaletteEntry } from "./entries.ts";
15 import { repoScreen } from "./repo.ts";
16 import { local } from "./where.ts";
17
18 export function repoListScreen(f: Session, ctx: Ctx): Screen {
19 const state = new ListState();
20 const read = (): Repo[] => listReposWritableTo(f.db, f.user, { limit: 200, withoutArchived: true });
21 return {
22 title: "Your repositories",
23 keys: [binding(["j", "k"], "move cursor"), binding(["enter"], "open")],
24 render(area, s) {
25 const c = ctx.theme.c;
26 const repos = read();
27 const [head, list] = split(area, "v", [len(1), fill(1)]);
28 const text = repos.length === 0 ? "No repositories yet." : `${repos.length} repositories you may write`;
29 s.text(head!.x, head!.y, truncate(text, head!.w), style(c.mutedFg, c.bg), head!.x + head!.w);
30 const rows: ListRow[] = repos.map((r) => ({ title: `${r.ownerName}/${r.name}` }));
31 drawList(s, list!, c, state, rows);
32 },
33 key(e) {
34 const repos = read();
35 if (state.key(e, repos.length)) return true;
36 const repo = repos[state.index];
37 if (e.ctrl || e.alt || e.key !== "enter" || repo === undefined) return false;
38 ctx.push(repoScreen(f, ctx, local(f, repo)));
39 return true;
40 },
41 };
42 }
43
44 /** Your repositories, as /repos lists them: anyone signed in. */
45 export const entry: PaletteEntry = {
46 id: "repos",
47 title: "Repositories",
48 hint: "the repositories you may write",
49 after: ["profile"],
50 offered: (session) => session.user !== undefined,
51 run: (session, ctx) => ctx.push(repoListScreen(session, ctx)),
52 };
src/web/layout.ts +1 −1
@@ -237,7 +237,7 @@ function userMenu(
237237 ): Html {
238238 const items: (MenuItem | Html)[] = [
239239 { label: "Your profile", href: `/${user.username}`, icon: "user" },
240 { label: "Your repositories", href: "/", icon: "repo" },
240 { label: "Repositories", href: "/repos", icon: "repo" },
241241 { label: "Apps", href: "/apps", icon: "rocket" },
242242 { label: "Notes", href: "/notes", icon: "comment" },
243243 { label: "Talk", href: "/talk", icon: "comment" },
src/web/pages/my-repos.ts added +35
@@ -0,0 +1,35 @@
1 /**
2 * Your repositories: every repository the viewer may write, their own and
3 * their organizations', newest first, each a link to it.
4 */
5
6 import { listReposWritableTo } from "../../core/repos.ts";
7 import { notePhase, takeSessionTime } from "../../core/route_timing.ts";
8 import type { Ctx } from "../../http/context.ts";
9 import { repoBase, signInFirst } from "../chrome.ts";
10 import { html } from "../html.ts";
11 import { page } from "../layout.ts";
12 import { btn, empty, listCard, row } from "../ui.ts";
13
14 // GET /repos
15 export function myReposPage(ctx: Ctx): Response {
16 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
17 const user = ctx.auth.user;
18 if (user === undefined) return signInFirst(ctx);
19 const reading = performance.now();
20 const repos = listReposWritableTo(ctx.app.db, user, { limit: 200, withoutArchived: true });
21 const body =
22 repos.length === 0
23 ? empty({ icon: "repo", title: "No repositories yet", action: btn("New repository", { href: "/new", variant: "primary" }) })
24 : listCard(
25 repos.map((r) =>
26 row({ href: repoBase(r), nav: true, title: html`<span class="truncate">${r.ownerName}/${r.name}</span>` }),
27 ),
28 );
29 notePhase(ctx.request, "render", performance.now() - reading);
30 return page({
31 ctx,
32 title: "Your repositories",
33 body: html`<div class="page-head"><h1>Your repositories</h1>${repos.length === 0 ? null : btn("New repository", { href: "/new" })}</div>${body}`,
34 });
35 }
src/web/pages/now.ts added +50
@@ -0,0 +1,50 @@
1 /**
2 * The four words of home's Now line, each opening what it counts: the checks,
3 * the reviews, the approved pulls waiting to land, the deploys that are live.
4 * The list is `readNowRows`, so its length is the number on home. A site
5 * admin's, as the line is: the counts cover every repository.
6 */
7
8 import { mayReadNow, NOW_HOLDS, NOW_LISTED, NOW_STAGES, NOW_WORDS, readNowRows, type NowStage } from "../../flow/now_rows.ts";
9 import { notePhase, takeSessionTime } from "../../core/route_timing.ts";
10 import type { Ctx } from "../../http/context.ts";
11 import { notFoundPage, signInFirst } from "../chrome.ts";
12 import { html } from "../html.ts";
13 import { page } from "../layout.ts";
14 import { empty, listCard, row, timeAgo } from "../ui.ts";
15
16 /** Where a word of the Now line goes. */
17 export const nowHref = (stage: NowStage): string => `/now/${stage}`;
18
19 // GET /now/:stage
20 export function nowPage(ctx: Ctx): Response {
21 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
22 const user = ctx.auth.user;
23 if (user === undefined) return signInFirst(ctx);
24 const stage = NOW_STAGES.find((s) => s === ctx.params["stage"]);
25 if (stage === undefined || !mayReadNow(user)) return notFoundPage(ctx);
26 const reading = performance.now();
27 const { total, rows } = readNowRows(ctx.app.db, stage);
28 const body =
29 total === 0
30 ? empty({ icon: "activity", title: `Nothing in ${NOW_WORDS[stage].toLowerCase()} right now`, note: NOW_HOLDS[stage] })
31 : listCard(
32 rows.map((r) =>
33 row({
34 href: r.href,
35 nav: true,
36 title: html`<span class="truncate">${r.title}</span>`,
37 meta: html`${r.repo} · ${r.note}`,
38 trail: html`<span class="muted small">${timeAgo(r.at)}</span>`,
39 }),
40 ),
41 );
42 notePhase(ctx.request, "render", performance.now() - reading);
43 return page({
44 ctx,
45 title: NOW_WORDS[stage],
46 body: html`<div class="page-head"><h1>${NOW_WORDS[stage]}</h1></div>
47 <p class="muted small" id="now-count"><strong>${String(total)}</strong> · ${NOW_HOLDS[stage]}${total > rows.length ? ` Showing the newest ${NOW_LISTED}.` : ""}</p>
48 ${body}`,
49 });
50 }
src/web/studio/living.ts +6 −4
@@ -16,6 +16,8 @@ import { html, type Html } from "../html.ts";
1616 import { fragment } from "../layout.ts";
1717 import { btn, listCard, row, timeAgo } from "../ui.ts";
1818 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
19 import type { NowStage } from "../../flow/now_rows.ts";
20 import { nowHref } from "../pages/now.ts";
1921 import { writableAppsOf } from "./asks.ts";
2022 import { FORGE_TARGET, sharedDraft } from "./feed.ts";
2123 import { repoBase } from "../chrome.ts";
@@ -35,18 +37,18 @@ export function pipelineNow(db: Database, at = Date.now()): Now {
3537 return now;
3638 }
3739
38 /** `check 10 › review 1 › land 3 › live`: each word links to the boards where the same stage is. */
40 /** `check 10 › review 1 › land 3 › live`: each word opens what it counts (`/now/:stage`). */
3941 export function nowLine(now: Now, boards = true): Html {
4042 // An agent admin has no boards to open: the word is then plain.
41 const part = (word: string, n: number) =>
42 boards ? html`<a href="/boards"><strong>${String(n)}</strong> ${word}</a>` : html`<span><strong>${String(n)}</strong> ${word}</span>`;
43 const part = (stage: NowStage, n: number) =>
44 boards ? html`<a href="${nowHref(stage)}"><strong>${String(n)}</strong> ${stage}</a>` : html`<span><strong>${String(n)}</strong> ${stage}</span>`;
4345 return html`<section aria-label="Now">
4446 <h2 class="small muted">Now</h2>
4547 <p class="bar" id="now-line">
4648 ${part("check", now.check)} <span class="muted" aria-hidden="true">›</span>
4749 ${part("review", now.review)} <span class="muted" aria-hidden="true">›</span>
4850 ${part("land", now.land)} <span class="muted" aria-hidden="true">›</span>
49 <a href="/releases"><strong>${String(now.live)}</strong> live</a>
51 ${part("live", now.live)}
5052 </p>
5153 </section>`;
5254 }
test/forge-looks.test.ts +1
@@ -51,6 +51,7 @@ const NOT_A_PAGE: Record<string, string> = {
5151 "/apple-touch-icon.png": "an icon, not a page",
5252 "/apple-touch-icon-precomposed.png": "an icon, not a page",
5353 "/events": "a live event stream that never ends",
54 "/migrate": "an import form, nothing to picture until a job runs; the look slot went to /repos",
5455 };
5556
5657 let dir: string;
test/home-living.test.ts +1 −1
@@ -117,7 +117,7 @@ describe("home", () => {
117117 const page = await home(rootCookie);
118118 expect(page).toContain('id="now-line"');
119119 for (const word of ["check", "review", "land"]) expect(page).toContain(`</strong> ${word}</a>`);
120 expect(page).toMatch(/href="\/releases"><strong>\d+<\/strong> live<\/a>/);
120 expect(page).toMatch(/href="\/now\/live"><strong>\d+<\/strong> live<\/a>/);
121121 });
122122
123123 test("the forge-wide pipeline counts never reach a viewer who is not a site admin", async () => {
test/palette-roster.test.ts +4
@@ -48,6 +48,7 @@ test("a site-admin person is offered the whole roster", () => {
4848 ["home", "Home", "your repositories"],
4949 ["explore", "Explore", "on the web"],
5050 ["profile", "Open a profile", "username or org"],
51 ["repos", "Repositories", "the repositories you may write"],
5152 ["repo", "Open a repository", "owner/name"],
5253 ["issues", "Issues in a repository", "owner/name"],
5354 ["held", "Held by the Later gate", "owner/name"],
@@ -75,6 +76,7 @@ test("a site-admin person is offered the whole roster", () => {
7576 ["sentinel", "Sentinel", "the forge's open faults"],
7677 ["boards", "Boards", "the forge's boards and their panels"],
7778 ["home-pins", "Pinned panels", "the panels pinned to home: reorder and unpin"],
79 ["now", "Now", "what check, review, land and live count, with the rows"],
7880 ["origin-apps", "Origin apps", "your OAuth apps for connecting origins; a adds, d removes"],
7981 ["system-memory", "System memory", "RSS beside the JS heap, hourly for 24 h"],
8082 ["flow", "Flow", "the pipeline's bottleneck and throughput"],
@@ -134,6 +136,7 @@ test("a site-admin agent is offered the roster without the person-only entries",
134136 ["home", "Home", "your repositories"],
135137 ["explore", "Explore", "on the web"],
136138 ["profile", "Open a profile", "username or org"],
139 ["repos", "Repositories", "the repositories you may write"],
137140 ["repo", "Open a repository", "owner/name"],
138141 ["issues", "Issues in a repository", "owner/name"],
139142 ["held", "Held by the Later gate", "owner/name"],
@@ -192,6 +195,7 @@ test("a member person is offered the signed-in roster without the admin entries"
192195 ["home", "Home", "your repositories"],
193196 ["explore", "Explore", "on the web"],
194197 ["profile", "Open a profile", "username or org"],
198 ["repos", "Repositories", "the repositories you may write"],
195199 ["repo", "Open a repository", "owner/name"],
196200 ["issues", "Issues in a repository", "owner/name"],
197201 ["held", "Held by the Later gate", "owner/name"],
test/security-floor-routes.test.ts +2
@@ -42,6 +42,7 @@ const FLEET = "needs a fleet operation, started by an operator action";
4242 const SECRET = "needs a secret scope an operator has set";
4343 const GO = "needs a short code minted by a share; the seed shares nothing";
4444 const ARCH = "needs an archived-home operation id; the seed archives nothing";
45 const NOW = "the stage is one of check, review, land or live; the seed fills no word with a name it cannot guess";
4546 const GATE = "needs a kept gate log, written only when a full gate runs on land";
4647 const HOOK = "needs a webhook, which the seed does not register";
4748
@@ -84,6 +85,7 @@ const UNSEEDED: Record<string, string> = {
8485 "/go/:code": GO,
8586 "/home/archived/:op": ARCH,
8687 "/migrate/jobs/:job": MIGRATE,
88 "/now/:stage": NOW,
8789 "/origins/:id/r/:owner/:name": ORIGIN,
8890 "/origins/:id/r/:owner/:name/blob/:ref/*path": ORIGIN,
8991 "/origins/:id/r/:owner/:name/branches": ORIGIN,

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs