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

A client's apps card updates by itself when an agent starts or finishes #6285

Merged opened this

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

Merged

scout/forge:scout/6092-a-client-s-apps-card-updates-by-itself-w → main 3 commits fork 2 threads · 2 unresolved

  • Merged. scout/6092-a-client-s-apps-card-updates-by-itself-w was merged into main as c432381.

Review

Sign in to review this change.
4 files changed +36 −1
src/http/server.ts +2
@@ -282,6 +282,7 @@ import {
282282 import {
283283 frontUpdate,
284284 liveAppsFragment,
285 liveClientAppsFragment,
285286 liveOnYouFragment,
286287 studioDoor,
287288 } from "../web/studio/home.ts";
@@ -798,6 +799,7 @@ export function buildRouter(): Router<Handler> {
798799 // fetches a region and not the page. Literal, above /:owner.
799800 router.add("GET", "/live/apps", liveAppsFragment);
800801 router.add("GET", "/live/on-you", liveOnYouFragment);
802 router.add("GET", "/live/client-apps", liveClientAppsFragment);
801803 router.add("GET", "/live/feed", liveFeedFragment);
802804 // Whether a hostname is an app's verified domain — what Caddy's
803805 // `on_demand_tls { ask … }` asks before it fetches a certificate for a
src/web/studio/client_home.ts +5 −1
@@ -71,7 +71,11 @@ export async function clientApps(ctx: Ctx, user: User): Promise<Html> {
7171 : html`<ul class="list client-apps">
7272 ${rows}
7373 </ul>`,
74 { id: "client-apps", head: html`<h2>Your apps</h2>` },
74 {
75 id: "client-apps",
76 head: html`<h2>Your apps</h2>`,
77 attrs: html`data-live="run.*" data-live-src="/live/client-apps"`,
78 },
7579 );
7680 notePhase(ctx.request, "render", performance.now() - rendering);
7781 return shown;
src/web/studio/home.ts +12
@@ -178,6 +178,18 @@ export function liveAppsFragment(ctx: Ctx): Response {
178178 return body;
179179 }
180180
181 // GET /live/client-apps — a client's apps card alone; 404 to anyone else.
182 export async function liveClientAppsFragment(ctx: Ctx): Promise<Response> {
183 const user = ctx.auth.user;
184 const steps = user ? await nextStepsOf(ctx, user) : undefined;
185 if (!user || steps === undefined) {
186 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
187 notePhase(ctx.request, "render", 0);
188 return fragment(html``, { status: 404 });
189 }
190 return fragment(await clientApps(ctx, user), { request: ctx.request }); // names session and render itself
outdated

This gate is the issue's "404s to anyone else" for a signed-in non-client, and no test covers it: deleting it keeps bun test green. Add a 404 assertion for the existing plain-reader session in test/client-home.test.ts. Optionally, fold this branch and the !user one into a single check so the notePhase pair is written once.

191 }
192
181193 /**
182194 * What a bulk step on the On you list answers its own script with: the
183195 * region as it is now and the asks page's list.
test/client-home.test.ts +17
@@ -335,6 +335,21 @@ describe("a client's first screen", () => {
335335 child.db.query("DELETE FROM run WHERE id = 'r-client-1'").run();
336336 });
337337
338 test("the card refreshes itself: /live/client-apps answers it, to a client only", async () => {
339 const repo = child.db
340 .query<{ id: string }, []>("SELECT id FROM repo WHERE name = 'bakery'")
341 .get()!;
342 expect(await page(owner)).toContain('data-live-src="/live/client-apps"');
343 child.db
344 .query("INSERT INTO run (id, repo_id, kind, state) VALUES ('r-client-2', ?1, 'agent', 'running')")
345 .run(repo.id);
346 const live = await page(owner, "/live/client-apps");
347 expect(live).toContain('id="client-apps"');
348 expect(live).toContain("One agent is working on it now.");
349 child.db.query("DELETE FROM run WHERE id = 'r-client-2'").run();
350 expect((await fetch(url("/live/client-apps"))).status).toBe(404);

This proves a signed-out viewer is refused, but not a signed-in viewer who isn't a client. Add the same assertion with a reader's forge_session cookie (see "a plain reader gets the ordinary home"), so the nextStepsOf gate has a test that would fail without it.

351 });
352
338353 test("none of the developer's words appear on it, composer included", async () => {
339354 const text = words(await page(owner));
340355 expect(text).toContain("Your apps");
@@ -359,6 +374,8 @@ describe("a client's first screen", () => {
359374 if (!reader.ok) throw new Error("reader");
360375 const session = createSession(child.db, reader.value.id);
361376 expect(await page(`forge_session=${session.token}`)).not.toContain('id="client-apps"');
377 const live = await fetch(url("/live/client-apps"), { headers: { cookie: `forge_session=${session.token}` } });
378 expect(live.status).toBe(404);
362379 });
363380
364381 const visibleWords = async (cookie: string): Promise<string> => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs