Compare commits

...

2 Commits

Author SHA1 Message Date
fireshaper ba03d78051 Step 14.0: inventory of what the port preserves
CI / check (push) Has been cancelled Details
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 19:09:44 -04:00
fireshaper 8ffd408290 CR-014: record the local-first change request
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 19:09:22 -04:00
4 changed files with 212 additions and 5 deletions

View File

@ -1333,3 +1333,10 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
- **"Max upgrades" installs every upgrade at its cap for that model** (per-model caps from CR-009/CR-012), free and without shop time; "Finish shop work" ends a job now, so the usual "out of the shop" note still appears. - **"Max upgrades" installs every upgrade at its cap for that model** (per-model caps from CR-009/CR-012), free and without shop time; "Finish shop work" ends a job now, so the usual "out of the shop" note still appears.
- **"Move here" keeps the rules that matter**: only an idle ship, only to a discovered place. Its pilot moves with it, as assigned pilots follow their ship. - **"Move here" keeps the rules that matter**: only an idle ship, only to a discovered place. Its pilot moves with it, as assigned pilots follow their ship.
- **"Discover everywhere"** marks every location discovered (no first-contact pay), for testing range, reach and boards. - **"Discover everywhere"** marks every location discovered (no first-contact pay), for testing range, reach and boards.
## CR-014 — Local-First (Phase 14)
- **Renumbered on arrival:** titled "CR-011 … Phase 10K"; CR-011 to CR-013 and Phases 10K–13 already exist, so it is **CR-014**, done as **Phase 14** (its 10K.n are 14.n).
- **Decisions:** its 0.5.66–0.5.71 are **0.5.87–0.5.92**. It replaces 0.5.18 and 0.5.20 (same numbers here) and its "0.5.53–0.5.55" are this plan's **0.5.71–0.5.73** (CR-011's accounts, backup code, several devices). Its "CR-008" is this repo's **CR-011**; its "CR-001 to CR-010" means **CR-001 to CR-013**.
- **Order:** the "Fixed Map and Fuel Tanks" request arrived first (also titled CR-014) but is written for this local-first engine; the user chose to build local-first first. It will be recorded as **CR-015** (Phase 15).
- **`tech-stack.md`** wasn't yet marked for this change in this repo (the CR says the designer had marked it); it is updated during the phase.

View File

@ -0,0 +1,165 @@
# Change Request CR-014 — Local-First: Run the Whole Game on the Device
> **Numbering note (added when recorded):** this request arrived titled "CR-011 — Local-First" for "Phase 10K". CR-011 to CR-013 and Phases 10K–13 already existed, so it is stored as **CR-014** and done as **Phase 14**: every "10K.n" below means **14.n**. Its decisions 0.5.66–0.5.71 are recorded as **0.5.87 Local-first, 0.5.88 Device clock, 0.5.89 No cross-device play, 0.5.90 Single writer, 0.5.91 Local notifications, 0.5.92 Save export/import**. The decisions it replaces map to this plan as: 0.5.18 (testable clock) and 0.5.20 (same domain) are the same numbers; its "0.5.53 guest accounts / 0.5.54 backup code / 0.5.55 several devices" are this plan's **0.5.71 No accounts yet, 0.5.72 Backup code, 0.5.73 Several devices** (from CR-011). Its "CR-008" (settings, steps 10H.n) is this repo's **CR-011** (Phase 10K, steps 10K.n); its "CR-001 to CR-010" means this repo's **CR-001 to CR-013**. Its §6 row for Plan 13 applies to the deployment built in Phase 13. `tech-stack.md` had not been marked for this change in this repo; it is updated as part of the phase. The "Fixed Map and Fuel Tanks" request that arrived first (also titled CR-014) waits until this one is done and will be recorded as **CR-015** (Phase 15), using the §7 translation table.
**Project:** Deadhead: Space Freight Tycoon
**Applies to:** the current build, whatever change requests are complete
**Do this:** **next**, as **Phase 10K**, before starting any change request that isn't finished yet
**Related docs:** `implementation-plan.md`, `tech-stack.md`, all earlier change requests, `DECISIONS.md`
---
## 1. Why
The game is single-player and every outcome is derived from timestamps, so it doesn't need a server. The user has decided:
- Playing the same company on web and phone **doesn't matter**.
- Players changing the device clock to skip waits **doesn't matter**.
So the backend goes away. The game runs entirely on the device, saves locally, works fully offline, and uses **local notifications** for arrivals (arrival times are known at dispatch, so no push service is needed). This removes hosting costs and outage risk, makes App Store review simpler (offline play, no accounts, no data collected), and removes a whole layer of code.
The `packages/sim` package already holds the game rules with no I/O, which is what makes this a port rather than a rewrite.
## 2. Target architecture
| Today | After |
|---|---|
| `apps/server` (Fastify) handles every action | A new **`packages/engine`**: the same game logic as commands, running in the app |
| Postgres + Drizzle | One **save document** (validated with Zod), stored by a small storage layer |
| Row locks and transactions | A **single mutation queue**: commands run one at a time; each one loads, changes, and saves the document |
| pg-boss arrival jobs | A **resolution loop** in the app (on launch, on resume, and every few seconds while open) plus **local notifications** |
| Server clock | A **device clock** that never runs backward, with a dev-only offset |
| Better Auth guest accounts, backup code | No accounts. The company is created on first launch. **Export/import save** replaces the backup code |
| API client over HTTP | The same typed client, calling engine commands in-process |
### Storage
- **Web:** IndexedDB, one record for the save plus rotating backups.
- **iOS/Android (Capacitor):** a JSON file in the app's data directory via the Capacitor Filesystem plugin, written atomically (write to a temp file, then rename). This location is included in iCloud and Android device backups.
- Both sit behind one storage interface with an in-memory version for tests.
- Keep the **last three saves** as rotating backups. If the main save fails to load or validate, load the newest valid backup and tell the player.
### Save document
- Holds everything the database holds today: company, credits, loan, ships, upgrades, pilots, bays, discovered locations, stored contract boards, accepted contracts, dispatches, goals, tutorial state, world seed, and a `saveVersion`.
- Resolved dispatches older than a configured retention window are pruned on save so the document stays small; keep enough history for "while you were away" summaries.
- Each `saveVersion` change ships with a migration function from the previous version.
### Clock
- `now = max(device time + dev offset, last time seen)`. Time never runs backward, so setting the clock back can't undo progress or produce negative timers. Setting it forward is allowed (decision §4).
- `last time seen` is stored in the save.
## 3. Rules for this change
- All rules in `implementation-plan.md` §0.1 apply: one commit per step, each step's test must pass, `packages/sim` stays pure, all numbers live in `packages/content`.
- **Port, don't redesign.** Every server endpoint becomes an engine command with the **same request and response schemas** (reuse the route catalog as a command catalog). Game behavior must not change.
- **Port the tests.** Every server integration test from Phases 5–7 and from completed change requests is moved to run against the engine with in-memory storage. A test is only deleted if what it tests no longer exists (for example HTTP status codes, auth, Postgres migrations).
- `packages/engine` may use storage, the clock, and the notification scheduler **only through injected interfaces**, so it stays testable.
- Generate the world seed and ids with the platform's cryptographic random source, outside `packages/sim`.
- Resetting existing dev data is fine; no migration from Postgres to local saves is needed.
- Update `architecture.md`, `tech-stack.md`, and `progress.md` as you go.
- Stop for the user's review at the end of Phase 10K.
## 4. Decisions
Record these in `DECISIONS.md` and `implementation-plan.md` §0.5.
- **0.5.66 Local-first.** The game runs entirely on the device. There is no game server.
- **0.5.67 Device clock.** Time comes from the device clock, clamped so it never runs backward. Moving the clock forward to skip waits is accepted.
- **0.5.68 No cross-device play.** Each device (and each browser) has its own company. Moving a company between devices is done by exporting and importing a save.
- **0.5.69 Single writer.** All state changes go through one mutation queue. In the browser, only one tab may run the game at a time (use the Web Locks API; other tabs show "Deadhead is open in another tab" with a "Use here" button that takes over).
- **0.5.70 Local notifications.** On iOS and Android, the app schedules a local notification for each arrival, each pilot's rest ending, and each upgrade finishing, and cancels them if the event changes (for example a new company). If several land within the same minute, schedule one combined notification. The permission prompt appears right after the player accepts the deal, not at first launch. The web build has no notifications.
- **0.5.71 Save export/import.** Export produces a single text code (the save document, compressed and encoded) that can be copied or shared as a file. Import validates it, migrates it if it's an older `saveVersion`, and replaces the current company after confirmation.
**Replaced or removed:** 0.5.18 (testable server clock → local clock with dev offset), 0.5.20 (same-domain API → no API), 0.5.53 (guest accounts → no accounts), 0.5.54 (backup code → save export/import), 0.5.55 (several devices → not applicable). Step 6.14's row locks are replaced by the mutation queue.
## 5. Steps — Phase 10K
### Step 10K.0 — Inventory
- List in `progress.md` which change requests (CR-001 to CR-010) are complete, in progress, or not started. Everything complete must be preserved by this port.
- **Test:** `progress.md` contains the list. (Manual check by the user.)
### Step 10K.1 — Engine package and clock
- Create `packages/engine` with the Clock interface (§2) and a device clock implementation with dev offset.
- **Test:** The clock returns device time plus offset. If device time moves backward, the clock returns the last time seen. Moving forward is reflected immediately.
### Step 10K.2 — Save document schema and migrations
- Define the save document with Zod in `packages/schema`, covering all state from every completed change request, plus `saveVersion` and `lastTimeSeen`.
- Add the migration framework (an ordered list of version-to-version functions) with an identity migration for version 1.
- **Test:** A fully populated example save validates. An invalid one is rejected with a readable error. Migrating a version-1 fixture produces a valid current save.
### Step 10K.3 — Storage layer
- Storage interface: load, save, list backups, load backup. Implementations: in-memory, IndexedDB (web), Capacitor Filesystem (native, atomic write). Rotate the last three backups on every save.
- On load: if the main save is missing, return nothing; if it fails to parse or validate, fall back to the newest valid backup and report that a backup was used.
- **Test:** Each implementation passes the same shared test suite (IndexedDB via a fake IndexedDB library, native via a mocked Filesystem plugin): round-trip save and load, backup rotation keeps exactly three, a corrupted main save falls back to a backup, an interrupted native write never leaves a half-written main file.
### Step 10K.4 — Mutation queue and tab lock
- All commands run through one queue: load (or use the cached document), apply, validate, save, then release. A failed command leaves the saved state unchanged.
- In the browser, acquire a Web Lock for the game; a second tab shows the "open in another tab" screen with "Use here".
- **Test:** Two rapid "buy ship" commands where only one is affordable produce exactly one purchase and no negative balance (replaces the Step 6.14 test). A command that throws leaves the save unchanged. A second tab can't run commands until it takes over the lock.
### Step 10K.5 — Port the game logic
- Move every server endpoint's logic into an engine command with the same input and output schemas. This includes all completed change requests (tutorial, deal, loan, goals, open markets, rest, goods and Priority, upgrades, veteran, settings actions, new ships).
- Move the matching integration tests to run against the engine with in-memory storage and a fixed clock.
- **Test:** The ported test suite passes. A test listing every command in the catalog confirms each has at least one engine test.
### Step 10K.6 — Resolution loop
- Resolve due dispatches, rest endings, and shop completions on load, when the app resumes (Capacitor `resume`, browser `visibilitychange`), and every 5 seconds while open. Uses the same idempotent resolution as Step 6.1.
- **Test:** A dispatch whose arrival passed while the app was closed is resolved exactly once on load. Running the loop twice in a row changes nothing the second time. "While you were away" summaries appear as before.
### Step 10K.7 — Switch the client to the engine
- The typed API client calls engine commands instead of HTTP. TanStack Query keeps working with engine calls as its query and mutation functions. Remove the server-time sync (Step 8.3); use the engine clock.
- **Test:** Existing component tests pass with the engine mocked through the same client interface. A Playwright test with all network requests blocked from the first page load can start a company, finish the tutorial, accept the deal, and dispatch a contract.
### Step 10K.8 — Company creation on the device
- First launch with no save shows the start screen; "Start your company" creates the starting state locally (same code path as the old guest creation, CR-001 state) with a fresh world seed.
- **Test:** With empty storage, starting a company produces the exact guest starting state from CR-001. Reloading loads the same company.
### Step 10K.9 — Local notifications
- Add a notification scheduler interface with a Capacitor Local Notifications implementation (native) and a no-op implementation (web). Schedule per decision 0.5.70 whenever a dispatch, rest, or upgrade is created; cancel and reschedule when state changes; cancel all on new company or import.
- Ask for permission right after the deal is accepted. Add a **Notifications** on/off toggle to Settings (CR-008 left this out because push didn't exist).
- **Test:** With a mocked plugin: dispatching schedules one notification at the arrival time; two arrivals in the same minute produce one combined notification; starting a new company cancels all scheduled notifications; with the toggle off, nothing is scheduled. On web, the no-op scheduler is used and nothing errors.
### Step 10K.10 — Settings changes
- Replace the backup code (CR-008 Steps 10H.8–10H.9) with **Export save** (shows the code with Copy and Share as file) and **Import save** (paste or open a file, then confirm). Keep the import warning about replacing the current company.
- "Start a new company", rename, and the Developer tools (clock offset, add credits, finish all flights) become local commands available only in development builds where noted.
- "Copy debug info" drops the player id and adds the save id, `saveVersion`, and storage type.
- **Test:** Exporting then importing on a fresh device storage reproduces the same state. Importing an older-version save migrates it. Importing garbage shows an error and changes nothing. Developer actions work in development builds and are absent in production builds.
### Step 10K.11 — Retire the server
- Remove `apps/server`, Postgres and the Docker Compose file, Drizzle, pg-boss, Better Auth, and their environment variables. Update CI to drop database services.
- **Test:** CI is green. The production web build makes no network requests other than loading its own static files (verified with Playwright). No server dependencies remain in the lockfile.
### Step 10K.12 — Plan amendments and regression
- Apply the amendments in §6 and the translation table in §7.
- Run the full suite.
- **Test:** CI is green. Manual check: on a phone (installed PWA or Capacitor dev build), dispatch a contract, close the app, turn on airplane mode, wait past arrival, reopen, and confirm the delivery resolved and (native build) a notification arrived.
## 6. Amendments
| Where | Change |
|---|---|
| Plan 0.5 | Add 0.5.66–0.5.71; remove or replace 0.5.18, 0.5.20, 0.5.53–0.5.55 as listed in §4. |
| Plan Phases 5–7 | Superseded by `packages/engine` (10K.1–10K.6). |
| Plan 8.2–8.5 | Client calls the engine; no server-time sync; no guest account (10K.7, 10K.8). |
| Plan 11.2 | No "offline" mode: the game always works offline. Remove the offline banner and disabled actions. |
| Plan 12.3–12.6 | End-to-end tests run against the web build alone, using the developer clock offset to fast-forward. |
| Plan 13 | Replaced: host the static web build. Remove server containers and database backups (players' saves live on their devices). |
| `tech-stack.md` | Update per §2 (the designer has already marked this). |
| CR-008 | Backup code replaced by save export/import; Notifications toggle added (10K.9, 10K.10). |
## 7. Translating change requests that aren't done yet
When a later or unfinished change request says:
| It says | Do this instead |
|---|---|
| Server endpoint | Engine command with the same schemas |
| Database table or column | Field in the save document, with a save migration |
| Database migration | Save migration (bump `saveVersion`) |
| Transaction with row lock | Mutation queue (already serial) |
| pg-boss job or scheduled job | Resolution loop, plus a local notification if the player should hear about it |
| Test-only server endpoint | Developer-only engine command |
| Server integration test | Engine test with in-memory storage |
## 8. Out of scope
Cloud saves, cross-device sync, leaderboards, clock-cheat detection, web notifications, in-app purchases, and the App Store readiness work (Capacitor release builds, store assets, privacy policy). Those come in a later change request.

View File

@ -73,9 +73,9 @@ These answer the pre-build review. Log each one in `DECISIONS.md` as it is imple
15. **Bays** have no ship limit. 15. **Bays** have no ship limit.
16. **Tutorial purchases.** *(Replaced by CR-001.)* During the tutorial, buying ships, leasing bays and hiring pilots are blocked with a `tutorial-active` error. 16. **Tutorial purchases.** *(Replaced by CR-001.)* During the tutorial, buying ships, leasing bays and hiring pilots are blocked with a `tutorial-active` error.
17. **Cargo fallback.** If no cargo type is both produced at the origin and demanded at the destination, use Standard. *Amended by 0.5.54 (CR-007): the class fallback uses each location's `cargoClassMix`; the good then falls back to a Standard good on the route, else any Standard good made at the origin.* 17. **Cargo fallback.** If no cargo type is both produced at the origin and demanded at the destination, use Standard. *Amended by 0.5.54 (CR-007): the class fallback uses each location's `cargoClassMix`; the good then falls back to a Standard good on the route, else any Standard good made at the origin.*
18. **Testable clock.** The server reads time through an injectable clock. Tests call job handlers directly; E2E tests use a test-only clock-offset endpoint that does not exist in production. 18. **Testable clock.** The server reads time through an injectable clock. Tests call job handlers directly; E2E tests use a test-only clock-offset endpoint that does not exist in production. *(Replaced by CR-014, decision 0.5.88: a device clock with a dev-only offset.)*
19. **Summaries.** Dispatches have an acknowledged-at column; resolved dispatches without one form the "while you were away" summary. 19. **Summaries.** Dispatches have an acknowledged-at column; resolved dispatches without one form the "while you were away" summary.
20. **Same domain.** In production, the client and API share one domain, with the API under `/api`, so the session cookie is first-party (iOS Safari blocks cross-site cookies). 20. **Same domain.** In production, the client and API share one domain, with the API under `/api`, so the session cookie is first-party (iOS Safari blocks cross-site cookies). *(Removed by CR-014, decision 0.5.87: there is no API.)*
21. **Tutorial boards per location.** The tutorial board is stored per period *and* per loaner location (one Earth→Moon board, one Moon→Earth board). *(Amended by CR-001: boards are also keyed by the number of tutorial runs completed, so each run gets a fresh three-contract board; the "large enough" size rule no longer applies.)* 21. **Tutorial boards per location.** The tutorial board is stored per period *and* per loaner location (one Earth→Moon board, one Moon→Earth board). *(Amended by CR-001: boards are also keyed by the number of tutorial runs completed, so each run gets a fresh three-contract board; the "large enough" size rule no longer applies.)*
22. **Pilot endpoints.** The route catalog includes "unassign pilot", and "hire pilot" takes the bay location to hire at. 22. **Pilot endpoints.** The route catalog includes "unassign pilot", and "hire pilot" takes the bay location to hire at.
23. **Avatar during tutorial.** The avatar is locked to the loaner and cannot be assigned to an owned ship until the tutorial ends. 23. **Avatar during tutorial.** The avatar is locked to the loaner and cannot be assigned to an owned ship until the tutorial ends.
@ -126,9 +126,9 @@ These answer the pre-build review. Log each one in `DECISIONS.md` as it is imple
68. **Courier to Mars** (CR-009). A Courier with maxed fuel tanks can reach Mars at and around closest approach, but not at Mars's farthest distance. Base and lower-level Couriers still can't reach Mars at all. *(Amended by CR-012:)* measured from Earth; from the Moon it reaches Mars more often, which is fine. 68. **Courier to Mars** (CR-009). A Courier with maxed fuel tanks can reach Mars at and around closest approach, but not at Mars's farthest distance. Base and lower-level Couriers still can't reach Mars at all. *(Amended by CR-012:)* measured from Earth; from the Moon it reaches Mars more often, which is fine.
69. **Deduction order** (CR-010). On resolution: pilot pay is taken from the payout first; loan repayment (0.5.32) is calculated on what remains. 69. **Deduction order** (CR-010). On resolution: pilot pay is taken from the payout first; loan repayment (0.5.32) is calculated on what remains.
70. **Veteran pilot** (CR-010). Accepting the deal also creates the veteran (CR-010 §2): at Earth, unassigned, level 2 with Heavy Cargo 1. The veteran is a normal hired pilot in every respect except how they were acquired. *(Amended by CR-013:)* the veteran starts with **Expensive Cargo 1** (was Heavy Cargo 1) and crewing the deal's Clipper (0.5.82), not unassigned. 70. **Veteran pilot** (CR-010). Accepting the deal also creates the veteran (CR-010 §2): at Earth, unassigned, level 2 with Heavy Cargo 1. The veteran is a normal hired pilot in every respect except how they were acquired. *(Amended by CR-013:)* the veteran starts with **Expensive Cargo 1** (was Heavy Cargo 1) and crewing the deal's Clipper (0.5.82), not unassigned.
71. **No accounts yet** (CR-011). Players remain guests. Account linking (email, Apple, Google) stays deferred (§0.3). 71. **No accounts yet** (CR-011). Players remain guests. Account linking (email, Apple, Google) stays deferred (§0.3). *(Replaced by CR-014: no accounts at all; the company is created on the device.)*
72. **Backup code** (CR-011). Each player can have one backup code: random, at least 80 bits of entropy, shown as groups of letters and digits that avoid look-alike characters (no 0/O, 1/I/L). The server stores only a hash. Generating a new code invalidates the old one. Entering a valid code on any device signs that device into the same player. Restore attempts are rate-limited per IP and per device. 72. **Backup code** (CR-011). Each player can have one backup code: random, at least 80 bits of entropy, shown as groups of letters and digits that avoid look-alike characters (no 0/O, 1/I/L). The server stores only a hash. Generating a new code invalidates the old one. Entering a valid code on any device signs that device into the same player. Restore attempts are rate-limited per IP and per device. *(Replaced by CR-014, decision 0.5.92: save export/import.)*
73. **Several devices** (CR-011). More than one device may be signed into the same player at once; the server is authoritative, so this is safe. 73. **Several devices** (CR-011). More than one device may be signed into the same player at once; the server is authoritative, so this is safe. *(Not applicable after CR-014, decision 0.5.89.)*
74. **New company** (CR-011). Starting a new company deletes all of the player's company data and recreates the starting state exactly as guest creation does (CR-001 tutorial state, new world seed). The player record and backup code stay the same. 74. **New company** (CR-011). Starting a new company deletes all of the player's company data and recreates the starting state exactly as guest creation does (CR-001 tutorial state, new world seed). The player record and backup code stay the same.
75. **Company name** (CR-011). Players can rename their company: 3–32 characters, letters, digits, spaces, and basic punctuation, trimmed. The default name is generated from the world seed. 75. **Company name** (CR-011). Players can rename their company: 3–32 characters, letters, digits, spaces, and basic punctuation, trimmed. The default name is generated from the world seed.
76. **Display preferences** (CR-011). Clock defaults to the device's locale format. Countdowns default to "real time". Reduced motion defaults to the device's reduced-motion setting and can be overridden. 76. **Display preferences** (CR-011). Clock defaults to the device's locale format. Countdowns default to "real time". Reduced motion defaults to the device's reduced-motion setting and can be overridden.
@ -142,6 +142,12 @@ These answer the pre-build review. Log each one in `DECISIONS.md` as it is imple
84. **Economic distance** (Step 12.1, user decision). Contract pay and fuel use `reference × (distance / reference)^exponent` instead of the plain distance (reference 0.384M km, Earth–Moon; exponent 0.65), so a Moon run is unchanged and long legs pay and cost on a gentler curve. Range, travel time, Long Haul and XP keep the plain distance. 84. **Economic distance** (Step 12.1, user decision). Contract pay and fuel use `reference × (distance / reference)^exponent` instead of the plain distance (reference 0.384M km, Earth–Moon; exponent 0.65), so a Moon run is unchanged and long legs pay and cost on a gentler curve. Range, travel time, Long Haul and XP keep the plain distance.
85. **A first contact the fleet can fly** (Step 12.1). While some undiscovered place is reachable, one fit slot on each board is a first-contact job sized and skilled for a ship that can fly it from there (decision 0.5.52's fit rules), so exploring never waits on board luck. 85. **A first contact the fleet can fly** (Step 12.1). While some undiscovered place is reachable, one fit slot on each board is a first-contact job sized and skilled for a ship that can fly it from there (decision 0.5.52's fit rules), so exploring never waits on board luck.
86. **Hosting** (Phase 13, user decision). The game runs on a home server with Docker Compose at deadhead.belowland.com; a Caddy container in the stack terminates HTTPS (Let's Encrypt), serves the built web app and proxies `/api`; nightly Postgres dumps go to a mounted folder or NAS share. 86. **Hosting** (Phase 13, user decision). The game runs on a home server with Docker Compose at deadhead.belowland.com; a Caddy container in the stack terminates HTTPS (Let's Encrypt), serves the built web app and proxies `/api`; nightly Postgres dumps go to a mounted folder or NAS share.
87. **Local-first** (CR-014). The game runs entirely on the device. There is no game server.
88. **Device clock** (CR-014). Time comes from the device clock, clamped so it never runs backward (`max(device time + dev offset, last time seen)`). Moving the clock forward to skip waits is accepted.
89. **No cross-device play** (CR-014). Each device (and each browser) has its own company. Moving a company between devices is done by exporting and importing a save.
90. **Single writer** (CR-014). All state changes go through one mutation queue. In the browser, only one tab may run the game at a time (Web Locks; other tabs show "Deadhead is open in another tab" with "Use here").
91. **Local notifications** (CR-014). On iOS and Android the app schedules a local notification for each arrival, each pilot's rest ending and each upgrade finishing, cancelled if the event changes; several in the same minute become one. Permission is asked right after the deal. The web build has none.
92. **Save export/import** (CR-014). Export gives one text code (the save, compressed and encoded) to copy or share as a file; import validates and migrates it and replaces the current company after confirmation.
--- ---
@ -721,6 +727,12 @@ Steps 10M.1–10M.5 are specified in `memory-bank/change-requests/CR-013-veteran
--- ---
## Phase 14 — Local-First: Run the Whole Game on the Device (CR-014)
Steps 14.0–14.12 are specified in `memory-bank/change-requests/CR-014-local-first.md` §5 (written there as 10K.0–10K.12), with its §6 amendments and §7 translation table for later change requests. Supersedes the server phases (5–7) and replaces Phase 13's server deployment with static hosting. Stop for review at the end.
---
## Definition of Done (Base Game) ## Definition of Done (Base Game)
- All step tests pass in CI. - All step tests pass in CI.

View File

@ -330,3 +330,26 @@ Choices (asked at the start of the phase, decision 0.5.86): a home server runnin
## Follow-ups ## Follow-ups
- [x] **More developer tools (user request).** Settings → Developer gains a pilot picker (Level up: XP to the next level, so skill points appear; Clear duty and rest), a ship picker (Max upgrades: every upgrade at its cap, installed at once and free; Reset upgrades; Finish shop work; Move an idle ship to a discovered place) and Discover everywhere. Server routes `devPilot`, `devShip`, `devDiscoverAll` (`/api/test/...`, non-production only, answer with the player state). Tests: server `dev-tools.test.ts` (each action, move refused to an undiscovered place, all routes 404 in production), web `dev-tools.test.tsx`. - [x] **More developer tools (user request).** Settings → Developer gains a pilot picker (Level up: XP to the next level, so skill points appear; Clear duty and rest), a ship picker (Max upgrades: every upgrade at its cap, installed at once and free; Reset upgrades; Finish shop work; Move an idle ship to a discovered place) and Discover everywhere. Server routes `devPilot`, `devShip`, `devDiscoverAll` (`/api/test/...`, non-production only, answer with the player state). Tests: server `dev-tools.test.ts` (each action, move refused to an undiscovered place, all routes 404 in production), web `dev-tools.test.tsx`.
## Phase 14 — Local-First (CR-014)
Spec: `memory-bank/change-requests/CR-014-local-first.md` (received as "CR-011 … Phase 10K"). New decisions recorded as plan §0.5.87–0.5.92; 0.5.18, 0.5.20 and 0.5.71–0.5.73 replaced or removed. The "Fixed Map and Fuel Tanks" request waits and becomes CR-015 (Phase 15).
- [x] **Step 14.0 — Inventory.** What the port must preserve (all complete, none in progress or not started):
- **Base plan:** Phases 1–12 complete (Phase 13's server deployment is replaced by this CR: static hosting only).
- **CR-001** Faster Start (Phase 10A) · **CR-002** Open Markets, Bays as HQs (10B) · **CR-003** Map Art (10C) · **CR-004** Pilot Duty and Rest (10D) · **CR-005** UI Overhaul (10E) · **CR-006** Boards That Fit Your Fleet (10F) · **CR-007** Cargo Goods, Priority Contracts and Shippers (10G) · **CR-008** Boards That Feel Fresh (10H) · **CR-009** Ship Upgrades (10I) · **CR-010** Inherited Veteran Pilot and Profit-Share Pay (10J) · **CR-011** Settings Screen and Company Backup (10K; its backup code and several-devices parts are replaced by this CR) · **CR-012** Lighter and Clipper, Per-Leg Range (10L) · **CR-013** The Veteran's Clipper (10M) — all complete.
- **Phase 11** (installable PWA, offline shell: the offline banner and disabled actions are removed by this CR) and **Phase 12** (balance simulation with economic distance and fleet-fit first contacts; end-to-end tests, which move to the web build alone) complete.
- **Follow-ups to preserve:** Earth launch cost 2.1; base pay +50 %; developer tools for pilots, ships and discovery (become developer-only engine commands); as an allowed Vite host.
- **Not started:** the Fixed Map and Fuel Tanks request (recorded after this phase as CR-015).
- [ ] Step 14.1 — Engine package and clock
- [ ] Step 14.2 — Save document schema and migrations
- [ ] Step 14.3 — Storage layer
- [ ] Step 14.4 — Mutation queue and tab lock
- [ ] Step 14.5 — Port the game logic
- [ ] Step 14.6 — Resolution loop
- [ ] Step 14.7 — Switch the client to the engine
- [ ] Step 14.8 — Company creation on the device
- [ ] Step 14.9 — Local notifications
- [ ] Step 14.10 — Settings changes
- [ ] Step 14.11 — Retire the server
- [ ] Step 14.12 — Plan amendments and regression