diff --git a/DECISIONS.md b/DECISIONS.md
index 221bc33..69bfb4b 100644
--- a/DECISIONS.md
+++ b/DECISIONS.md
@@ -813,3 +813,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
- **"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.
+
+## Step 10D.9
+
+- **The summary gives the wake time as the device's local clock time** (like the dispatch sheet's real ETA), since the player reads it on returning; HQ gives a live countdown instead.
+- **The HQ rest card is always shown after the tutorial**, with "Everyone is ready to fly." when nobody rests, so the player learns the mechanic exists before it first bites.
diff --git a/apps/web/src/components/AwaySummary.tsx b/apps/web/src/components/AwaySummary.tsx
index e10eb4e..108c044 100644
--- a/apps/web/src/components/AwaySummary.tsx
+++ b/apps/web/src/components/AwaySummary.tsx
@@ -2,6 +2,7 @@ import { content } from "@deadhead/content";
import { useState } from "react";
import { useNavigate } from "react-router";
import { formatCredits, locationName } from "../format";
+import { formatLocalTime } from "../screens/DispatchSheet";
import { useGameAction, useLoadedPlayer } from "../state/actions";
/** "While you were away": deliveries resolved since the player last looked (Step 9.2). */
@@ -72,6 +73,12 @@ export function AwaySummary() {
Discovered {locationName(s.discoveredLocationId)}!
)}
+ {s.restEndsAt !== null && (
+
+ {state.pilots.find((p) => p.id === s.pilotId)?.name ?? "The pilot"} is resting
+ until {formatLocalTime(s.restEndsAt)}
+
+ )}
))}
diff --git a/apps/web/src/screens/HQ.tsx b/apps/web/src/screens/HQ.tsx
index 9bbaaab..8510337 100644
--- a/apps/web/src/screens/HQ.tsx
+++ b/apps/web/src/screens/HQ.tsx
@@ -28,6 +28,10 @@ export function HQ() {
const flying = state.ships.filter((s) => s.status === "in-flight").length;
const runsBeforeDeal = content.config.tutorialRunsBeforeDeal;
const next = [...state.activeDispatches].sort((a, b) => a.arrivalAt - b.arrivalAt)[0];
+ // Resting pilots and who wakes first (CR-004).
+ const resting = state.pilots
+ .filter((p) => p.resting && p.restEndsAt !== null)
+ .sort((a, b) => a.restEndsAt! - b.restEndsAt!);
return (
@@ -52,6 +56,19 @@ export function HQ() {
)}
+ {!state.tutorial && (
+
+
Pilots resting: {resting.length}
+ {resting[0] ? (
+
+ {resting[0].name} wakes in
+
+ ) : (
+
Everyone is ready to fly.
+ )}
+
+ )}
+
{!state.tutorial && state.ships.length === 0 && (
No ships yet — buy one in the Market.
)}
diff --git a/apps/web/test/rest-summary-hq.test.tsx b/apps/web/test/rest-summary-hq.test.tsx
new file mode 100644
index 0000000..c6c1f70
--- /dev/null
+++ b/apps/web/test/rest-summary-hq.test.tsx
@@ -0,0 +1,90 @@
+import type { Pilot, ResolutionSummary } from "@deadhead/schema";
+import { screen, within } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { useClock } from "../src/clock/clock";
+import { formatLocalTime } from "../src/screens/DispatchSheet";
+import { fakeApi, renderApp, samplePlayerState } from "./render";
+
+const NOW = Date.parse("2100-01-01T00:00:00Z");
+
+const pilot = (id: string, name: string, extra: Partial = {}): Pilot => ({
+ id,
+ name,
+ isAvatar: false,
+ xp: 0,
+ level: 0,
+ skills: {},
+ skillPointsAvailable: 0,
+ assignedShipId: null,
+ status: "idle",
+ locationId: "earth",
+ dutyHours: 0,
+ dutyLimit: 12,
+ resting: false,
+ restEndsAt: null,
+ ...extra,
+});
+
+const summary = (extra: Partial): ResolutionSummary => ({
+ dispatchId: "d1",
+ shipId: "s1",
+ pilotId: "p-rae",
+ originId: "earth",
+ destinationId: "moon",
+ tutorial: false,
+ credits: 500,
+ loanRepayment: 0,
+ pilotXp: 10,
+ discoveredLocationId: null,
+ scriptedId: null,
+ restEndsAt: null,
+ resolvedAt: NOW,
+ ...extra,
+});
+
+const pilots = [
+ pilot("p-rae", "Rae Okafor", { resting: true, restEndsAt: NOW + 40 * 60_000 }),
+ pilot("p-jun", "Jun Sato", { resting: true, restEndsAt: NOW + 9 * 60_000 }),
+ pilot("p-cy", "Cy Fresh"),
+];
+
+describe("rest in summaries and on HQ (Step 10D.9)", () => {
+ beforeEach(() => {
+ vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] });
+ vi.setSystemTime(NOW);
+ useClock.setState({ offsetMs: 0, synced: true, now: NOW });
+ });
+ afterEach(() => vi.useRealTimers());
+
+ it("says in the away summary that a pilot is resting, only for trips that ended in rest", async () => {
+ const restEndsAt = NOW + 40 * 60_000;
+ renderApp(
+ fakeApi({
+ getPlayerState: () =>
+ samplePlayerState({
+ tutorial: null,
+ pilots,
+ summaries: [
+ summary({ dispatchId: "d1", restEndsAt }),
+ summary({ dispatchId: "d2", pilotId: "p-cy" }),
+ ],
+ }),
+ }),
+ "/",
+ );
+ const dialog = await screen.findByRole("dialog", { name: "While you were away" });
+ const [rested, plain] = within(dialog).getAllByRole("listitem");
+ expect(rested).toHaveTextContent(`Rae Okafor is resting until ${formatLocalTime(restEndsAt)}`);
+ expect(plain).not.toHaveTextContent("resting");
+ });
+
+ it("shows how many pilots are resting and who wakes first", async () => {
+ renderApp(
+ fakeApi({ getPlayerState: () => samplePlayerState({ tutorial: null, pilots }) }),
+ "/",
+ );
+ const card = await screen.findByLabelText("Pilot rest");
+ expect(card).toHaveTextContent("Pilots resting: 2");
+ expect(card).toHaveTextContent("Jun Sato wakes in 9m 00s");
+ });
+});
diff --git a/memory-bank/progress.md b/memory-bank/progress.md
index 9b16715..36d5caa 100644
--- a/memory-bank/progress.md
+++ b/memory-bank/progress.md
@@ -172,5 +172,5 @@ Spec: `memory-bank/change-requests/CR-004-pilot-duty-rest.md`. New decisions rec
- [x] **Step 10D.6 — Client: pilot duty on the Pilots screen.** Each pilot card shows a duty bar ("9.0 h of 14 h", `progressbar` with the hours and limit); resting pilots show "Resting · wakes in …" instead. Idle, non-resting pilots (after the tutorial) have "Rest now" with an inline confirmation stating the rest length and that it covers at least one board refresh, then call `restPilot`. Test: `pilots-duty.test.tsx`.
- [x] **Step 10D.7 — Client: duty in the dispatch flow.** The dispatch sheet disables a resting pilot's pair with "Resting — mm:ss" (new `formatClock`). Eligible pairs show "Duty after" (sim `tripGameHours` + `dutyAfterTrip`) and "Will need rest on arrival" when `needsRest`. Client validation now passes the pilot's real `restEndsAt` (dispatch sheet and deadhead picker, which shows "Pilot is resting"). Test: `dispatch-duty.test.tsx`.
- [x] **Step 10D.8 — Client: board refresh and the "nothing here" case.** Every location board shows "New contracts in mm:ss" to `periodEndsAt` and refetches itself just after it. For each idle, crewed, non-resting ship at the location with no contract it can take (by the sim, via `pairOptions`), a prompt offers "Rest [pilot] — wakes to a new board" (`restPilot`) and "Deadhead (fly empty)" (opens the Fleet's deadhead picker, now exported). Test: `board-refresh.test.tsx`.
-- [ ] Step 10D.9 — Client: rest in summaries and on HQ
+- [x] **Step 10D.9 — Client: rest in summaries and on HQ.** The "While you were away" entry for a trip that ended in mandatory rest says "[Pilot] is resting until [local time]". HQ (after the tutorial) has a "Pilot rest" card: "Pilots resting: N" and the soonest to wake with a countdown, or "Everyone is ready to fly." Test: `rest-summary-hq.test.tsx`.
- [ ] Step 10D.10 — Plan amendments and regression