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
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6092-a-client-s-apps-card-updates-by-itself-w → main 3 commits fork 2 threads · 2 unresolved
4 files
changed +36
−1
src/http/server.ts +2
| @@ -282,6 +282,7 @@ import { | ||
| 282 | 282 | import { |
| 283 | 283 | frontUpdate, |
| 284 | 284 | liveAppsFragment, |
| 285 | liveClientAppsFragment, | |
| 285 | 286 | liveOnYouFragment, |
| 286 | 287 | studioDoor, |
| 287 | 288 | } from "../web/studio/home.ts"; |
| @@ -798,6 +799,7 @@ export function buildRouter(): Router<Handler> { | ||
| 798 | 799 | // fetches a region and not the page. Literal, above /:owner. |
| 799 | 800 | router.add("GET", "/live/apps", liveAppsFragment); |
| 800 | 801 | router.add("GET", "/live/on-you", liveOnYouFragment); |
| 802 | router.add("GET", "/live/client-apps", liveClientAppsFragment); | |
| 801 | 803 | router.add("GET", "/live/feed", liveFeedFragment); |
| 802 | 804 | // Whether a hostname is an app's verified domain — what Caddy's |
| 803 | 805 | // `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> { | ||
| 71 | 71 | : html`<ul class="list client-apps"> |
| 72 | 72 | ${rows} |
| 73 | 73 | </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 | }, | |
| 75 | 79 | ); |
| 76 | 80 | notePhase(ctx.request, "render", performance.now() - rendering); |
| 77 | 81 | return shown; |
src/web/studio/home.ts +12
| @@ -178,6 +178,18 @@ export function liveAppsFragment(ctx: Ctx): Response { | ||
| 178 | 178 | return body; |
| 179 | 179 | } |
| 180 | 180 | |
| 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 | |
| 191 | } | |
| 192 | ||
| 181 | 193 | /** |
| 182 | 194 | * What a bulk step on the On you list answers its own script with: the |
| 183 | 195 | * 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", () => { | ||
| 335 | 335 | child.db.query("DELETE FROM run WHERE id = 'r-client-1'").run(); |
| 336 | 336 | }); |
| 337 | 337 | |
| 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 |
||
| 351 | }); | |
| 352 | ||
| 338 | 353 | test("none of the developer's words appear on it, composer included", async () => { |
| 339 | 354 | const text = words(await page(owner)); |
| 340 | 355 | expect(text).toContain("Your apps"); |
| @@ -359,6 +374,8 @@ describe("a client's first screen", () => { | ||
| 359 | 374 | if (!reader.ok) throw new Error("reader"); |
| 360 | 375 | const session = createSession(child.db, reader.value.id); |
| 361 | 376 | 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); | |
| 362 | 379 | }); |
| 363 | 380 | |
| 364 | 381 | const visibleWords = async (cookie: string): Promise<string> => { |
new above
offline
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 testgreen. Add a 404 assertion for the existing plain-reader session in test/client-home.test.ts. Optionally, fold this branch and the!userone into a single check so the notePhase pair is written once.