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

`/agents` points people to the home page's form, and an anonymous "Sign up" on the top forge leads there #11305

Merged opened this

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

Merged

scout/forge:scout/11241-agents-points-people-to-the-home-page-s → main 4 commits fork 3 threads · 3 unresolved

  • Merged. scout/11241-agents-points-people-to-the-home-page-s was merged into main as 6bfa20a.

Review

Sign in to review this change.
4 files changed +127 −2
src/settings/signup.ts +15
@@ -44,6 +44,19 @@ export function getSignup(db: Database): SignupMode {
4444 return parsed.ok ? parsed.value : SIGNUP_DEFAULT;
4545 }
4646
47 /** The mode last read or written per database: the header asks on every anonymous page, and only the writers below change it. */
48 const known = new WeakMap<Database, SignupMode>();
49
50 /** `getSignup` without a query once known; every write here forgets it. */
51 export function signupModeKnown(db: Database): SignupMode {
52 let mode = known.get(db);
53 if (mode === undefined) {
54 mode = getSignup(db);
55 known.set(db, mode);
56 }
57 return mode;
58 }
59
4760 /** The actor kept for the value a new forge starts with. */
4861 export const FRESH_ACTOR = "fresh";
4962
@@ -57,6 +70,7 @@ export const FRESH_ACTOR = "fresh";
5770 export function seedSignup(db: Database, fresh: boolean): void {
5871 if (!fresh) return;
5972 const stored = db.query("SELECT 1 FROM forge_setting WHERE key = ?1").get(STORE_KEY);
73 known.delete(db);
6074 if (stored === null) writeSetting(db, STORE_KEY, "invite", SIGNUP_DEFAULT, "invite", FRESH_ACTOR);
6175 }
6276
@@ -65,6 +79,7 @@ export function setSignup(db: Database, value: SignupMode, actorId: string): voi
6579 const from = getSignup(db);
6680 if (from === value) return;
6781 writeSetting(db, STORE_KEY, value, from, value, actorId);
82 known.delete(db);
6883 }
6984
7085 /** `FORGE_SIGNUP` from the environment: unset is no opinion, a bad value is a message. */
src/web/layout.ts +18 −1
@@ -28,6 +28,8 @@ import {
2828 listReposVisibleTo,
2929 } from "../core/repos.ts";
3030 import { mayDirectCrew } from "../crew/direct.ts";
31 import { signupModeKnown } from "../settings/signup.ts";
32 import { parentIssuerOf } from "../core/federation.ts";
3133 import type { User } from "../core/users.ts";
3234 import type { Ctx } from "../http/context.ts";
3335 import { activeEdgeToken, edgeLoginUrl, edgeLogoutUrl, edgeStrangerLogin } from "../http/edge.ts";
@@ -630,6 +632,21 @@ export function signInHref(ctx: Ctx): string {
630632 return token === undefined ? "/signin" : edgeLoginUrl(ctx.url.pathname + ctx.url.search);
631633 }
632634
635 /**
636 * Where an anonymous visitor's "Sign up" leads on the top forge: `/signup`
637 * while signup there is open, the home page's form (`/`) while it is closed.
638 * Undefined where the button is not offered: behind a platform edge, or on a
639 * forge that has a parent (its people come in through it).
640 */
641 export function signUpHref(ctx: Ctx): string | undefined {
642 if (ctx.auth.user !== undefined) return undefined;
643 // The token is read from config first: no edge, no query.
644 if (ctx.app.config.edgeToken !== undefined && activeEdgeToken(ctx.app) !== undefined) return undefined;
645 if (parentIssuerOf(ctx.app.config) !== undefined) return undefined;
646 const mode = signupModeKnown(ctx.app.db);
647 return mode === "open" || mode === "request" ? "/signup" : "/";

Extra DB query on every anonymous page; the pins in did-you-mean and signin-timing had to be raised to hide it. Fold this read into one already made, and revert the pins.

648 }
649
633650 /** The Talk button; the count of confirms waiting on the viewer's yes is asked only here, where the button renders. */
634651 function talkButton(ctx: Ctx, user: User, about?: string): Html {
635652 const waiting = waitingCount(ctx, user);
@@ -684,7 +701,7 @@ function topbar(
684701 : { label: "Sign out", post: "/logout", icon: "signOut" },
685702 about,
686703 )}`
687 : html`${guestMenu(ctx, theme)}${btn("Sign in", {
704 : html`${guestMenu(ctx, theme)}${when(signUpHref(ctx), (href) => btn("Sign up", { href }))}${btn("Sign in", {
688705 href: signInHref(ctx),
689706 })}`}`}
690707 </span>
src/web/pages/agents.ts +1 −1
@@ -70,7 +70,7 @@ export function agentsPage(ctx: Ctx): Response {
7070 description:
7171 "What this forge is and how an AI assistant can help you use it: build one of its public apps, report a problem, or get a forge of your own.",
7272 body: html`${pageHead("For AI assistants", {
73 sub: html`What this forge is. Plain text: <a href="/llms.txt">/llms.txt</a>`,
73 sub: html`People start at <a href="/">home</a>. Assistants: <a href="/llms.txt">/llms.txt</a>`,
7474 })}
7575 <section class="stack" aria-labelledby="agent-forge-h">
outdated

Principle 1: a second sentence above the form explains what the head sub could carry; put the home-page link in the sub and keep the WORDS_CEILING at 56.

7676 <h2 id="agent-forge-h">Get a forge of your own</h2>
test/header-signup.test.ts added +93
@@ -0,0 +1,93 @@
1 /** The anonymous header's "Sign up" on the top forge, and /agents pointing people home. */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { serve, type Servers } from "../src/http/server.ts";
8 import { setCapacity } from "../src/core/capacity.ts";
9 import { setSignup } from "../src/settings/signup.ts";
10 import { quietLogs } from "./helpers/globals.ts";
11
12 quietLogs();
13
14 let dataDir: string;
15 let app: App;
16 let servers: Servers;
17
18 beforeAll(async () => {
19 dataDir = await mkdtemp(join(tmpdir(), "forge-header-signup-"));
20 app = createApp({ dataDir, port: 0 });
21 servers = await serve(app);
22 });
23 afterAll(async () => {
24 await servers.stop?.();
25 app.close();
26 await rm(dataDir, { recursive: true, force: true });
27 });
28
29 const header = async (path: string): Promise<string> => {
30 const page = await (await fetch(`${servers.url}${path}`)).text();
31 return page.slice(page.indexOf('<header class="topbar"'), page.indexOf("</header>"));
32 };
33 const signUpLink = (head: string) => head.match(/<a\s[^>]*href="([^"]*)"[^>]*>Sign up<\/a/)?.[1];
34
35 test("signup open: the header's Sign up leads to /signup", async () => {
36 setSignup(app.db, "open", "test");
37 expect(signUpLink(await header("/explore"))).toBe("/signup");
38 });
outdated

Add tests for request and invite modes, a forge with a parent, and an edge token: each is a branch of signUpHref with no test.

39
40 test("signup closed: the header's Sign up leads to the home page's form", async () => {
41 setSignup(app.db, "closed", "test");
42 expect(signUpLink(await header("/explore"))).toBe("/");
43 });
44
45 test("request opens /signup, invite and closed lead home", async () => {
46 for (const [mode, href] of [["request", "/signup"], ["invite", "/"], ["closed", "/"], ["open", "/signup"]] as const) {
47 setSignup(app.db, mode, "test");
48 expect(signUpLink(await header("/explore"))).toBe(href);
49 }
50 });
51
52 test("a forge with a parent offers no Sign up", async () => {
53 setSignup(app.db, "open", "test");
54 const dir = await mkdtemp(join(tmpdir(), "forge-header-parent-"));
55 const child = createApp({ dataDir: dir, port: 0, parentUrl: "https://parent.example" });
56 const s = await serve(child);
57 try {
58 const page = await (await fetch(`${s.url}/explore`)).text();
59 const head = page.slice(page.indexOf('<header class="topbar"'), page.indexOf("</header>"));
60 expect(signUpLink(head)).toBeUndefined();
61 expect(head).toContain(">Sign in<");
62 } finally {
63 await s.stop?.();
64 child.close();
65 await rm(dir, { recursive: true, force: true });
66 }
67 });
68
69 test("behind a platform edge there is no Sign up, and with the edge switched off there is", async () => {
70 const dir = await mkdtemp(join(tmpdir(), "forge-header-edge-"));
71 const edge = createApp({ dataDir: dir, port: 0, edgeToken: "edge-secret" });
72 const s = await serve(edge);
73 const headOf = async () => {
74 const page = await (await fetch(`${s.url}/explore`)).text();
75 return page.slice(page.indexOf('<header class="topbar"'), page.indexOf("</header>"));
76 };
77 try {
78 setSignup(edge.db, "open", "test");
79 expect(signUpLink(await headOf())).toBeUndefined();
80 setCapacity(edge.db, "signin.edge", "off", "test");
81 expect(signUpLink(await headOf())).toBe("/signup");
82 } finally {
83 await s.stop?.();
84 edge.close();
85 await rm(dir, { recursive: true, force: true });
86 }
87 });
88
89 test("/agents keeps its form and says people start on the home page", async () => {
90 const page = await (await fetch(`${servers.url}/agents`)).text();
91 expect(page).toContain('id="agent-forge"');
92 expect(page).toMatch(/People start at <a href="\/">home<\/a>/);
93 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs