Merge pull request 'fix: hydration-safe banner clock (#44, v0.10.1)' (#47) from fix/hydration-jump-dispatch into master
CI / build + unit + e2e (push) Successful in 3m32s
CI / build + unit + e2e (push) Successful in 3m32s
This commit was merged in pull request #47.
This commit is contained in:
@@ -19,3 +19,13 @@
|
|||||||
|
|
||||||
## Other
|
## Other
|
||||||
1. For longer terminal tasks you should use pty_* tools.
|
1. For longer terminal tasks you should use pty_* tools.
|
||||||
|
|
||||||
|
<!-- BEGIN:nextjs-agent-rules -->
|
||||||
|
|
||||||
|
# This is NOT the Next.js you know
|
||||||
|
|
||||||
|
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
||||||
|
|
||||||
|
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
||||||
|
|
||||||
|
<!-- END:nextjs-agent-rules -->
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v0.10.1 — 2026-10-03 18:36
|
||||||
|
- Fix React 19 hydration mismatch on every production load — the banner's live clock was prerendered at build time in the static page and desynced at hydration (CI-verified: no hydration errors in the job logs). A real fix, but NOT the cause of the red e2e jump specs.
|
||||||
|
- Fix the world-map jump dispatch — the actual cause of the three deterministically red e2e jump specs: TeamBuilder's `blocked` term was `undefined`, not `null`, whenever no jump was queued (`Array.find` returns `undefined`), so the roster rendered (truthiness check) while the DISPATCH button's `blocked !== null` guard read it as blocked and stayed disabled — the JUMP panel's dispatch was permanently dead on an empty queue (the 300 s `waitForResponse` timeout; present since the builder shipped in v0.10.0, so the UI-jump specs were red on every run since, on a fresh DB; non-JUMP panels were unaffected, their branch yielding `null`).
|
||||||
|
|
||||||
## v0.10.0 — 2026-10-03 00:57
|
## v0.10.0 — 2026-10-03 00:57
|
||||||
- Queue-based actions (#44): every game action (jump, explore, ship upgrade/expand/build, research) is now a queued task with an assigned team — a lead + optional helpers. Validation and cost deduction happen at queue time (unknown crew → 404, busy/committed crew → 409, in-transit or second jump → 409); effects apply when the task completes, mission accept is instant and free, and mission-completion checks run when the satisfying task resolves.
|
- Queue-based actions (#44): every game action (jump, explore, ship upgrade/expand/build, research) is now a queued task with an assigned team — a lead + optional helpers. Validation and cost deduction happen at queue time (unknown crew → 404, busy/committed crew → 409, in-transit or second jump → 409); effects apply when the task completes, mission accept is instant and free, and mission-completion checks run when the satisfying task resolves.
|
||||||
- Manual time: new `POST /api/turn` advances exactly one turn — each active task ticks down, completions resolve via per-type resolvers (reusing the existing jump/explore/ship/research logic), FIFO successors auto-start; an empty queue is a free skip. `save.turn` now only increments here, and the UI gets the NEXT TURN button (bottom-right) with the turn counter.
|
- Manual time: new `POST /api/turn` advances exactly one turn — each active task ticks down, completions resolve via per-type resolvers (reusing the existing jump/explore/ship/research logic), FIFO successors auto-start; an empty queue is a free skip. `save.turn` now only increments here, and the UI gets the NEXT TURN button (bottom-right) with the turn counter.
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ export function MissionControl() {
|
|||||||
const [activeTab, setActiveTab] = useState<string | null>(null)
|
const [activeTab, setActiveTab] = useState<string | null>(null)
|
||||||
// #38 — admin DB CRUD overlay, opened from the header.
|
// #38 — admin DB CRUD overlay, opened from the header.
|
||||||
const [adminOpen, setAdminOpen] = useState(false)
|
const [adminOpen, setAdminOpen] = useState(false)
|
||||||
|
// #44 (v0.10.1) — true only after hydration: the banner clock must render '' in both SSR and the
|
||||||
|
// first client render, otherwise the prerendered (build-time) string desyncs at hydration.
|
||||||
|
const [hydrated, setHydrated] = useState(false)
|
||||||
|
|
||||||
// #44 — the four mount fetches, extracted into a callback so the NEXT TURN button (5b) can
|
// #44 — the four mount fetches, extracted into a callback so the NEXT TURN button (5b) can
|
||||||
// re-run them after /api/turn: a tick can change all four snapshots (the turn always; inventory
|
// re-run them after /api/turn: a tick can change all four snapshots (the turn always; inventory
|
||||||
@@ -120,6 +123,9 @@ export function MissionControl() {
|
|||||||
return () => clearInterval(id)
|
return () => clearInterval(id)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// #44 (v0.10.1) — flip after hydration so the banner clock can start (SSR/first render both '').
|
||||||
|
useEffect(() => setHydrated(true), [])
|
||||||
|
|
||||||
// #27 — restore the persisted tab once on mount (validated against TAB_IDS).
|
// #27 — restore the persisted tab once on mount (validated against TAB_IDS).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const stored = window.localStorage.getItem(ACTIVE_TAB_KEY)
|
const stored = window.localStorage.getItem(ACTIVE_TAB_KEY)
|
||||||
@@ -139,7 +145,9 @@ export function MissionControl() {
|
|||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const timeStr = new Date().toLocaleTimeString('en-GB', { hour12: false })
|
// #44 (v0.10.1) — hydration-safe: '' until hydrated (SSR and first client render agree), then the
|
||||||
|
// live time on the 1s tick — no more build-time clock baked into the prerendered HTML.
|
||||||
|
const timeStr = hydrated ? new Date().toLocaleTimeString('en-GB', { hour12: false }) : ''
|
||||||
|
|
||||||
// Slugs of the researched techs — drives the effective caps + build gates in the SHIP SYSTEMS
|
// Slugs of the researched techs — drives the effective caps + build gates in the SHIP SYSTEMS
|
||||||
// tab via the same pure helpers the API enforces (lib/game/tech.ts).
|
// tab via the same pure helpers the API enforces (lib/game/tech.ts).
|
||||||
|
|||||||
@@ -23,9 +23,13 @@ export function TeamBuilder({ type, hopCount, roster, tasks, onQueue }: {
|
|||||||
const [helperIds, setHelperIds] = useState<number[]>([])
|
const [helperIds, setHelperIds] = useState<number[]>([])
|
||||||
|
|
||||||
// Whole-builder blocks: an ACTIVE jump is in transit (all types); in the jump panel any
|
// Whole-builder blocks: an ACTIVE jump is in transit (all types); in the jump panel any
|
||||||
// queued jump is — the second-jump 409 (JC-12).
|
// queued jump is — the second-jump 409 (JC-12). The JUMP branch coalesces find()'s
|
||||||
|
// undefined to null so blocked is strictly QueueDto | null: the roster ternary below tests
|
||||||
|
// truthiness, but the button guard compares to null — undefined slipped past it and
|
||||||
|
// disabled every empty-queue JUMP dispatch (v0.10.1 e2e jump regression).
|
||||||
const inTransit = tasks.find((t) => t.type === 'JUMP' && t.status === 'ACTIVE')
|
const inTransit = tasks.find((t) => t.type === 'JUMP' && t.status === 'ACTIVE')
|
||||||
const blocked = inTransit ?? (type === 'JUMP' ? tasks.find((t) => t.type === 'JUMP') : null)
|
const queuedJump = type === 'JUMP' ? (tasks.find((t) => t.type === 'JUMP') ?? null) : null
|
||||||
|
const blocked = inTransit ?? queuedJump
|
||||||
|
|
||||||
const lead = roster.find((c) => c.id === leadId) ?? null
|
const lead = roster.find((c) => c.id === leadId) ?? null
|
||||||
const helpers = roster.filter((c) => helperIds.includes(c.id))
|
const helpers = roster.filter((c) => helperIds.includes(c.id))
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "space-wanderer",
|
"name": "space-wanderer",
|
||||||
"version": "0.10.0",
|
"version": "0.10.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3000",
|
"dev": "next dev -p 3000",
|
||||||
|
|||||||
Reference in New Issue
Block a user