From dbd74fe38eade567396d0f1071cdb4328352fcf7 Mon Sep 17 00:00:00 2001 From: "molly.computer" Date: Mon, 10 Aug 2026 00:14:37 -0400 Subject: [PATCH] Connect frontend to world snapshot --- web/src/components/MetroGame.tsx | 21 ++++++++++--- web/src/lib/api.test.ts | 25 +++++++++++++++- web/src/lib/api.ts | 51 ++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/web/src/components/MetroGame.tsx b/web/src/components/MetroGame.tsx index a7979f2..a14337b 100644 --- a/web/src/components/MetroGame.tsx +++ b/web/src/components/MetroGame.tsx @@ -1,6 +1,11 @@ import { useEffect, useState } from "react"; -import { getHealth, type HealthResponse } from "../lib/api"; +import { + getHealth, + getWorld, + type HealthResponse, + type WorldResponse, +} from "../lib/api"; type LoadState = "loading" | "ready" | "error"; @@ -19,13 +24,18 @@ function PetMarker() { export default function MetroGame() { const [state, setState] = useState("loading"); const [health, setHealth] = useState(null); + const [world, setWorld] = useState(null); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); - void getHealth(controller.signal) - .then((response) => { + void Promise.all([ + getHealth(controller.signal), + getWorld("A01", controller.signal), + ]) + .then(([response, snapshot]) => { setHealth(response); + setWorld(snapshot); setState("ready"); }) .catch((requestError: unknown) => { @@ -113,7 +123,10 @@ export default function MetroGame() {
{state === "loading" &&

Checking the local signal...

} {state === "ready" && ( -

Rust backend connected. WMATA data is not configured yet.

+

+ Rust backend connected. {world?.trains.length ?? 0} fixture + trains are moving. +

)} {state === "error" && ( <> diff --git a/web/src/lib/api.test.ts b/web/src/lib/api.test.ts index 5f24592..29098a5 100644 --- a/web/src/lib/api.test.ts +++ b/web/src/lib/api.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { getHealth } from "./api"; +import { getHealth, getWorld } from "./api"; afterEach(() => vi.restoreAllMocks()); @@ -55,4 +55,27 @@ describe("getHealth", () => { signal: controller.signal, }); }); + + it("requests a world snapshot for a station", async () => { + const fetchMock = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + generatedAt: "2026-08-10T12:00:00Z", + stale: false, + trains: [], + predictions: [], + incidents: [], + elevatorOutages: [], + lineActivity: {}, + events: [], + }), + ), + ); + vi.stubGlobal("fetch", fetchMock); + + await expect(getWorld("A01")).resolves.toMatchObject({ stale: false }); + expect(fetchMock).toHaveBeenCalledWith("/api/v1/world?station=A01", { + signal: undefined, + }); + }); }); diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 399c3eb..cc97612 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -7,6 +7,41 @@ export interface HealthResponse { lastSuccessfulUpdate: string | null; } +export interface WorldResponse { + generatedAt: string; + stale: boolean; + trains: Array<{ + id: string; + lineCode: string; + stationCode: string; + destination: string; + progress: number; + }>; + predictions: Array<{ + stationCode: string; + lineCode: string; + destination: string; + minutes: number; + }>; + incidents: Array<{ + id: string; + description: string; + affectedLines: string[]; + }>; + elevatorOutages: Array<{ + stationCode: string; + equipment: string; + description: string; + }>; + lineActivity: Record; + events: Array<{ + id: string; + kind: string; + stationCode: string | null; + lineCode: string | null; + }>; +} + export async function getHealth(signal?: AbortSignal): Promise { const response = await fetch("/api/v1/health", { signal }); @@ -16,3 +51,19 @@ export async function getHealth(signal?: AbortSignal): Promise { return response.json() as Promise; } + +export async function getWorld( + station: string, + signal?: AbortSignal, +): Promise { + const response = await fetch( + `/api/v1/world?station=${encodeURIComponent(station)}`, + { signal }, + ); + + if (!response.ok) { + throw new Error(`World request failed (${response.status})`); + } + + return response.json() as Promise; +} -- 2.51.2