Audit SPA data planes for page-load-only staleness #201
Labels
No labels
burndown-2026-06
headless
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
c#
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-gaming/eco-app#201
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Issue 184 exposed a broader freshness gap. Eco App pages combine source data with different observation cadences and backend caches, but many frontend clients fetch only when a page mounts. A page can therefore remain open after its source advances without polling, an explicit refresh action, or a truthful stale-state transition.
Outcome
Audit every SPA data plane and make its browser refresh contract explicit. A reader should be able to tell whether an open page continues updating, requires a reload, or presents data that is static for the session.
Required audit
Acceptance criteria
Safety boundary
Read-only browser and backend data flows only. Do not add gameplay mutations, server control actions, or background load without a source-specific cadence justification.
Non-goals
Designer inventory and data-freshness check-in
Current main has a broad page-load-only freshness gap. The issue is justified and its non-goal against blanket polling is the right constraint.
Inventory
/preview.jsonpolls every 60 seconds over a 30-second backend cache.Backend timing families
Truthfulness gaps
This audit confirms and generalizes the three-clock model established in #183. It also preserves #184's finding that source cadence, backend cache behavior, browser age, and presentation can fail independently.
Interaction contract
Experience hierarchy
Freshness belongs beside each coherent data section. A page-level Refresh all is the dominant action. A single footer timestamp cannot carry the contract for a multi-plane page.
The lowest-cost prototype is the World or Trade page using one shared freshness line, a page-level refresh action, and per-section retry. That prototype should prove compact desktop and mobile hierarchy before the pattern rolls across the remaining planes.
The audit
Enumerated every frontend fetch client, hook and page-level data plane. 21 planes. Twenty of them fetched once on mount and never again.
One correction to the obvious first reading, including my own: it is not all of them.
hooks/useEcoStatus.tsalready polled at 60s. It lives inhooks/, notpages/, which is exactly how it got missed — a grep overpages/says "everything is mount-only" and that is wrong.Classification
Cadence is the source's, not the backend's. Where they differ the backend cache is the binding constraint on how fresh a poll can possibly be, so nothing polls faster than its TTL.
live60slive2mlive2mlive2mlive2mmanualmanualmanualmanualmanualmanualmanualmanualmanualstaticstaticstaticstaticstaticstaticUnknown boundaries, recorded honestly
fetchedAtISO, which is backend fetch age. The three are kept separate and never inferred from each other — conflating the last two is what #184 warns against, andFreshnessNoterenders them as distinct facts.manualclassification is a judgement about how fast the underlying activity moves, not a measured interval, and the rationale on each contract says so.What landed
frontend/src/lib/freshness.ts— the contract table. Cadences live here, not in components, so they are tunable in one place and tests assert behaviour rather than restating constants.frontend/src/lib/useFreshData.ts— the single fetch path. Mount load, contract-driven poll, manual refresh, and a failed refresh kept distinct from a failed first load: a refresh failure keeps the last good data on screen and says so, rather than blanking a page that was working a second ago. Polling pauses on a hidden tab.frontend/src/components/FreshnessNote.tsx— the user-visible contract, ageing on its own timer so an idle page keeps telling the truth.docs/spa-freshness.md— the boundary, and the three ages.Tests cover the four cases this issue names — open-page ageing, successful refetch, failed refetch, unknown cadence — plus the invariants that keep the table honest: every plane needs a rationale, a
liveplane's stale window must outlast its poll period, astaticplane must never poll. 16 tests, and the SPA suite is at 178 passed.What is left
Wired:
statussite-wide,/map(climate, region, map),/civics,/crafting,/social.Not wired:
/items,/item,/species,/replay,/recipes,/recipe,/trade,/user,/jobs, and the seven/uses/*pages. Their contracts are declared and honest, but the page still fetches on mount only and shows no caption or Refresh control.Two reasons for stopping where I did rather than sweeping the rest:
/item,/species,/user,/uses/*) need the hook'sdepsargument so a changed query re-fetches. That is a different edit from the simple single-plane swap, and doing it blind would risk a page that silently stops responding to its own URL./trade,/jobsand/uses/pricecompose 3–6 planes each, so they need per-plane captions rather than one.Every
liveplane — the ones where a stale page actively misleads, and the category #184 was about — is wired. The unwired remainder ismanualandstatic, where mount-only is much closer to correct.Leaving this open with that scope rather than closing it on partial coverage. The acceptance criterion is "every independently fetched SPA data plane", and 15 pages are not there yet.
Cross-references: #183 (freshness presentation) and #184 (the stale-pollution incident that motivated this).