diff --git a/DECISIONS.md b/DECISIONS.md
index a6e1c6d..221bc33 100644
--- a/DECISIONS.md
+++ b/DECISIONS.md
@@ -806,3 +806,10 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
- **The resting line is computed from the clock each render** ("Resting — 12:05"), so it counts down while the sheet is open, and it is the first problem listed.
- **Duty after uses the same sim functions the server uses on arrival** (`tripGameHours` from departure and arrival, `dutyAfterTrip`, `needsRest`), so the warning matches what will happen.
- **The deadhead picker also respects rest** (it validates with the pilot's real `restEndsAt`), disabling every destination with "Pilot is resting".
+
+## Step 10D.8
+
+- **The countdown uses the board's own `periodEndsAt`** from the server, and the board query refetches half a second after it, so a pilot who wakes up sees the new board without reloading.
+- **"Eligible" means the dispatch sheet would allow it:** a ship is stuck when `pairOptions` finds no contract on this board with no problems for that ship and its pilot. Uncrewed ships and resting pilots get no prompt (resting pilots already show their countdown elsewhere).
+- **One prompt per stuck ship**, naming the ship and pilot, so a location with several idle ships stays clear.
+- **`Countdown` takes an optional `format`**; the board refresh uses `mm:ss` (`formatClock`) as the CR asks.
diff --git a/apps/web/src/components/Countdown.tsx b/apps/web/src/components/Countdown.tsx
index 5e4f5d1..9f99d83 100644
--- a/apps/web/src/components/Countdown.tsx
+++ b/apps/web/src/components/Countdown.tsx
@@ -2,8 +2,16 @@ import { useClock } from "../clock/clock";
import { formatDuration } from "../format";
/** Live time remaining until `to` (server clock); shows `done` once it has passed. */
-export function Countdown({ to, done = "Arriving…" }: { to: number; done?: string }) {
+export function Countdown({
+ to,
+ done = "Arriving…",
+ format = formatDuration,
+}: {
+ to: number;
+ done?: string;
+ format?: (ms: number) => string;
+}) {
const now = useClock((s) => s.now);
const remaining = to - now;
- return {remaining > 0 ? formatDuration(remaining) : done};
+ return {remaining > 0 ? format(remaining) : done};
}
diff --git a/apps/web/src/screens/Fleet.tsx b/apps/web/src/screens/Fleet.tsx
index 3cd5808..bfd03f4 100644
--- a/apps/web/src/screens/Fleet.tsx
+++ b/apps/web/src/screens/Fleet.tsx
@@ -124,7 +124,7 @@ const DEADHEAD_REASONS: Partial> = {
* Fly an idle ship empty to any other discovered location, paying fuel only
* (CR-002). The sim decides which destinations are possible, as the server will.
*/
-function DeadheadPicker({ ship, onClose }: { ship: Ship; onClose: () => void }) {
+export function DeadheadPicker({ ship, onClose }: { ship: Ship; onClose: () => void }) {
const state = useLoadedPlayer();
const now = useClock((s) => s.now);
const reposition = useGameAction("createDispatch");
diff --git a/apps/web/src/screens/LocationBoards.tsx b/apps/web/src/screens/LocationBoards.tsx
index 4ea010e..2814c65 100644
--- a/apps/web/src/screens/LocationBoards.tsx
+++ b/apps/web/src/screens/LocationBoards.tsx
@@ -1,12 +1,22 @@
-import type { Contract, PlayerState } from "@deadhead/schema";
+import type { Contract, PlayerState, Ship } from "@deadhead/schema";
import { useQuery } from "@tanstack/react-query";
+import { useState } from "react";
import { useSearchParams } from "react-router";
import { useApi } from "../api/context";
-import { useClock } from "../clock/clock";
+import { serverNow, useClock } from "../clock/clock";
import { Countdown } from "../components/Countdown";
-import { cargoName, formatCredits, formatMillionKm, locationName, skillName } from "../format";
+import {
+ cargoName,
+ formatClock,
+ formatCredits,
+ formatMillionKm,
+ locationName,
+ skillName,
+} from "../format";
import { distanceAt } from "../game";
-import { useLoadedPlayer } from "../state/actions";
+import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions";
+import { pairOptions } from "./DispatchSheet";
+import { DeadheadPicker } from "./Fleet";
export const locationBoardKey = (locationId: string) => ["board", "location", locationId] as const;
@@ -91,6 +101,11 @@ function LocationBoard({
const board = useQuery({
queryKey: locationBoardKey(locationId),
queryFn: () => api.call("getContractBoard", { locationId }),
+ // Fetch the new board as soon as the period rolls over (CR-004).
+ refetchInterval: (query) => {
+ const endsAt = query.state.data?.periodEndsAt;
+ return endsAt === undefined ? false : Math.max(1000, endsAt - serverNow() + 500);
+ },
});
if (board.isPending) return