From a75a39e01f69aa606d9e4dfaddfcdc0956d7a0ae Mon Sep 17 00:00:00 2001 From: fireshaper Date: Sun, 11 Oct 2026 10:45:32 -0400 Subject: [PATCH] Step 21.7: client: board and logbook A 'Yours for' chip in the first-look window, a 'Catching up on your fleet' indicator for slow catch-ups, and a per-pilot logbook with company totals on the away sheet (CR-021). Co-Authored-By: Claude Opus 5.5 --- DECISIONS.md | 7 + apps/web/src/GameRoot.tsx | 16 +- apps/web/src/components/AwaySummary.tsx | 8 +- apps/web/src/components/PilotLogbook.tsx | 208 ++++++++++++++++ apps/web/src/platform/game.ts | 15 +- apps/web/src/screens/LocationBoards.tsx | 18 ++ apps/web/test/away-goals.test.tsx | 1 + apps/web/test/away-summary.test.tsx | 3 + apps/web/test/board-logbook.test.tsx | 239 +++++++++++++++++++ apps/web/test/dashboard.test.tsx | 1 + apps/web/test/deal-modal.test.tsx | 1 + apps/web/test/pilot-share.test.tsx | 1 + apps/web/test/rest-summary-hq.test.tsx | 1 + apps/web/test/tutorial-board.test.tsx | 1 + docs/screenshots/21-7-first-look-desktop.png | Bin 0 -> 728166 bytes docs/screenshots/21-7-logbook-desktop.png | Bin 0 -> 192881 bytes docs/screenshots/21-7-logbook-phone.png | Bin 0 -> 88690 bytes memory-bank/progress.md | 7 +- packages/engine/src/game/state.ts | 1 + packages/engine/src/loop.ts | 4 + packages/schema/src/entities.ts | 2 + packages/schema/test/schemas.test.ts | 1 + 22 files changed, 527 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/components/PilotLogbook.tsx create mode 100644 apps/web/test/board-logbook.test.tsx create mode 100644 docs/screenshots/21-7-first-look-desktop.png create mode 100644 docs/screenshots/21-7-logbook-desktop.png create mode 100644 docs/screenshots/21-7-logbook-phone.png diff --git a/DECISIONS.md b/DECISIONS.md index 7308224..66e1dbc 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1722,3 +1722,10 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **"Anywhere you know" is `destinationIds: null`**, so places discovered later are included. Unticking it starts from every place known now. - **The Fleet badge sits on the pilot line**: beside the status pill on a phone it truncated the ship's name. - **The spending floor's button is "Save floor"**, distinct from the company name's "Save". + +## Step 21.7 + +- **The "Yours for" chip appears only when a pilot is Independent**: without one, nobody competes for the job and the chip would be noise. First contacts never get it; they're always the player's. +- **The logbook replaces per-trip cards for pilots' own trips**: an hour away is a dozen trips per pilot. Each pilot is a collapsible section, open when there's only one. +- **A logbook job's payout is credits + pilot share + loan repayment**, rebuilt from the summary: what the contract paid before deductions. +- **The indicator follows the loop's own pass**, via `onBusy`, not a separate catch-up call, so it covers the first pass on opening and each resume. diff --git a/apps/web/src/GameRoot.tsx b/apps/web/src/GameRoot.tsx index 4741aad..4625645 100644 --- a/apps/web/src/GameRoot.tsx +++ b/apps/web/src/GameRoot.tsx @@ -3,6 +3,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { useEffect, useMemo, useState, type ReactNode } from "react"; import { createEngineClient } from "./api/engine-client"; import { ApiProvider } from "./api/context"; +import { CatchingUp } from "./components/PilotLogbook"; import { GameContext } from "./platform/context"; import { startResolutionLoop, type Game } from "./platform/game"; import { PLAYER_STATE_KEY } from "./state/player"; @@ -26,6 +27,8 @@ export function GameRoot({ game, children }: { game: Game; children: ReactNode } const queryClient = useQueryClient(); const api = useMemo(() => createEngineClient(game.engine), [game]); const [status, setStatus] = useState("starting"); + // A slow catch-up of Independent pilots shows "Catching up on your fleet…" (CR-021). + const [catchingUp, setCatchingUp] = useState(false); useEffect(() => { game.lock.onLost(() => setStatus("elsewhere")); @@ -40,10 +43,14 @@ export function GameRoot({ game, children }: { game: Game; children: ReactNode } useEffect(() => { if (status !== "here") return; - const loop = startResolutionLoop(game, () => { - void queryClient.invalidateQueries({ queryKey: PLAYER_STATE_KEY }); - void queryClient.invalidateQueries({ queryKey: ["board"] }); - }); + const loop = startResolutionLoop( + game, + () => { + void queryClient.invalidateQueries({ queryKey: PLAYER_STATE_KEY }); + void queryClient.invalidateQueries({ queryKey: ["board"] }); + }, + setCatchingUp, + ); if (game.devTools) { window.deadhead = { call: (name, input) => game.engine.call(name, input as never) }; } @@ -77,6 +84,7 @@ export function GameRoot({ game, children }: { game: Game; children: ReactNode } return ( {children} + ); } diff --git a/apps/web/src/components/AwaySummary.tsx b/apps/web/src/components/AwaySummary.tsx index fd4f91d..699b504 100644 --- a/apps/web/src/components/AwaySummary.tsx +++ b/apps/web/src/components/AwaySummary.tsx @@ -7,6 +7,7 @@ import { formatCredits, locationName, toLocation } from "../format"; import { formatLocalTime } from "../screens/DispatchSheet"; import { useGameAction, useLoadedPlayer } from "../state/actions"; import { Button, Icon, Sheet } from "../ui"; +import { PilotLogbook, pilotLogbook } from "./PilotLogbook"; /** "While you were away": deliveries resolved since the player last looked (Step 9.2). */ export function AwaySummary() { @@ -39,10 +40,15 @@ export function AwaySummary() { acknowledge.mutate({ dispatchIds: ids, ...(shipIds.length > 0 ? { shipIds } : {}) }); }; + // Independent pilots' own trips (CR-021) are listed by pilot, not one card each. + const logbook = pilotLogbook(state, summaries); + const yours = summaries.filter((s) => !s.independent); + return ( +