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

A signed-out visitor's command palette offers only what they can reach, and Sign in #7927

Merged opened this

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

Merged

scout/forge:scout/7920-a-signed-out-visitor-s-command-palette-o → main 3 commits fork

  • Merged. scout/7920-a-signed-out-visitor-s-command-palette-o was merged into main as 07bb0e1.

Review

Sign in to review this change.
3 files changed +61 −6
README.md +2
@@ -25,6 +25,8 @@ child's own data, and `/forges` says how many are running and why any is not. Ea
2525 with why (also in the terminal and the MCP `forges`): "Not reporting" (no word
2626 in 24 hours), "App down", or "Unclaimed" three days after it was sent; the rest
2727 follow by last activity.
28 A signed-out visitor's command palette (Cmd-K) lists only places they can reach (Home, Apps, Explore, Front page, Search, public repositories) plus **Sign in**, which goes where the topbar's button does (`signInHref` in `src/web/layout.ts`: `/signin`, `/signup` for an edge stranger, the edge's login when an edge token is active); no Create or Settings rows. After signing in from the palette the visitor lands on the default page, not the one they were on. The terminal's guest palette already hides the signed-in verbs (New repository, Migrate, New forge and the rest) and offers Sign in.
29
2830 In the terminal an admin's palette offers "Forges you sent": each forge with its
2931 claim state and the same apps-running note, never a claim link. Both carry the
3032 crew's spend today against the daily ceiling that pauses it (`$3.20 of $10 today`), as the
src/web/layout.ts +21 −6
@@ -314,6 +314,9 @@ const STATIC_PALETTE: PaletteItem[] = [
314314 { t: "Origins", h: "/settings/origins", c: "Settings" },
315315 ];
316316
317 /** What a signed-out visitor can reach: places to go, not Create and Settings rows that end at a sign-in wall. */
318 const VISITOR_PALETTE = new Set(["Home", "Apps", "Explore", "Front page", "Search"]);
319
317320 /** First path segments that are the forge's own pages, never an owner: no repository read for them. */
318321 const NOT_AN_OWNER = new Set([
319322 ...STATIC_PALETTE.map((p) => p.h.split("/")[1] ?? ""),
@@ -410,7 +413,16 @@ function paletteIndex(ctx: Ctx, extra: PaletteItem[]): Html {
410413 }),
411414 };
412415 }),
413 ...STATIC_PALETTE,
416 ...(ctx.auth.user
417 ? STATIC_PALETTE
418 : [
419 ...STATIC_PALETTE.filter((p) => VISITOR_PALETTE.has(p.t)),
420 {
421 t: "Sign in",
422 h: signInHref(ctx),
423 c: "Go to",
424 },
425 ]),
414426 ...(sendsForges(ctx)
415427 ? [{ t: "New forge", h: "/forges/new", c: "Create" }]
416428 : []),
@@ -509,6 +521,13 @@ function flashToast(flash: Flash): Html {
509521
510522 // ---- the chrome ----
511523
524 /** Where a signed-out visitor's way in leads: the topbar button and the palette's Sign in share it. */
525 export function signInHref(ctx: Ctx): string {
526 const token = activeEdgeToken(ctx.app);
527 if (edgeStrangerLogin(ctx.request, token, ctx.auth.user) !== undefined) return "/signup";
528 return token === undefined ? "/signin" : edgeLoginUrl(ctx.url.pathname + ctx.url.search);
529 }
530
512531 /** The skip link and the topbar: the wordmark, the crumbs, the tools. */
513532 function topbar(
514533 ctx: Ctx,
@@ -552,11 +571,7 @@ function topbar(
552571 : { label: "Sign out", post: "/logout", icon: "signOut" },
553572 )}`
554573 : html`${guestMenu(theme)}${btn("Sign in", {
555 href: edgeStrangerLogin(ctx.request, activeEdgeToken(ctx.app), ctx.auth.user) !== undefined
556 ? "/signup"
557 : activeEdgeToken(ctx.app) === undefined
558 ? "/signin"
559 : edgeLoginUrl(ctx.url.pathname + ctx.url.search),
574 href: signInHref(ctx),
560575 })}`}
561576 </span>
562577 </header>`;
test/palette-repos.test.ts +38
@@ -106,3 +106,41 @@ test("a private app is not known to a stranger by search, and is to its owner",
106106 expect(await flag(zed)).toBe(false);
107107 expect(await flag(undefined)).toBe(false);
108108 });
109
110 test("a visitor's palette holds places to go and Sign in; a signed-in one holds the full list", async () => {
111 const rows = async (headers: Record<string, string>, path: string) => {
112 const html = await (await fetch(`${servers.url}${path}`, { headers })).text();
113 const m = /<script type="application\/json" id="palette-index">([\s\S]*?)<\/script>/.exec(html)!;
114 return (JSON.parse(m[1]!) as { items: { t: string; h: string; c?: string }[] }).items.filter((i) => i.c !== "Repositories");
115 };
116 const out = await rows({}, "/explore?q=a%26b");
117 expect(out.map((i) => i.t)).toEqual(["Home", "Apps", "Explore", "Front page", "Search", "Sign in"]);
118 expect(out.find((i) => i.t === "Sign in")!.h).toBe("/signin");
119 const inn = (await rows({ cookie }, "/explore")).map((i) => i.t);
120 for (const t of ["New app", "New repository", "Migrate a repository", "New organization", "What you asked for", "Account settings", "Access tokens", "SSH keys", "Origins", "Map"]) expect(inn).toContain(t);
121 expect(inn).not.toContain("Sign in");
122 });
123
124 test("the palette's Sign in leads where the topbar's button does, on an edge-login forge too", async () => {
125 const edgeDir = await mkdtemp(join(tmpdir(), "forge-palette-edge-"));
126 const edgeApp = createApp({ dataDir: edgeDir, port: 0, edgeToken: "edge-secret-for-palette" });
127 const edgeServers = await serve(edgeApp);
128 try {
129 const items = async (headers: Record<string, string> = {}) => {
130 const body = await (await fetch(`${edgeServers.url}/explore?q=a`, { headers })).text();
131 const index = JSON.parse(body.match(/id="palette-index">([^<]*)</)![1]!) as { items: { t: string; h: string }[] };
132 const button = body.match(/<a\s+href="([^"]*)"[^>]*>\s*Sign in\s*<\/a/)![1]!;
133 return { palette: index.items.find((i) => i.t === "Sign in")!.h, button: button.replaceAll("&amp;", "&") };
134 };
135 const visitor = await items();
136 expect(visitor.palette).toBe("/_plat/login?rd=%2Fexplore%3Fq%3Da");
137 expect(visitor.button).toBe(visitor.palette);
138 const stranger = await items({ "x-plat-user": "newcomer", "x-plat-edge-token": "edge-secret-for-palette" });
139 expect(stranger.palette).toBe("/signup");
140 expect(stranger.button).toBe("/signup");
141 } finally {
142 edgeServers.stop();
143 edgeApp.close();
144 await rm(edgeDir, { recursive: true, force: true });
145 }
146 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs