feat: the first pages, and what a browser walk found behind them #4

Merged
whitlocktech merged 1 commits from feat/phase-4-first-pages into main 2026-09-17 02:43:29 +00:00
Member

Phase 4 of docs/modules/rust/PLAN.md — recorded there as §17 (docs#256). One repo, and the first phase whose deliverable is something a visitor looks at.

What it adds

  • /rust is the server list and the module's landing page (D12). Registered with an empty path; core renders that as the namespace root. /rust/servers/:id hangs beneath it.
  • One server, four tabs (D13) — feed, leaderboard, who is on, wipes. Tab, feed filter, wipe and leaderboard sort all live in the URL, so "last wipe's leaderboard on Main" is a link, back undoes a click, and a refresh lands where the reader was.
  • Polling while the tab is visible (D14): the feed and the presence list every 20s, paused by the Page Visibility API, refreshed the instant a viewer returns. The leaderboard and the wipe list load once.
  • site.footer.status filled with a live count (D15), linking to /rust.
  • GET …/servers/:id, the one route under that path that can say a server does not exist — every other one answers an empty list for an id nobody configured, which is a perfectly good answer to its own question.
  • capabilities grows to what the pages serve, for phase 5's Android leg to feature-detect against.

Nothing on these pages calls a game server. Every field comes from this module's own tables, which is exactly what the criterion is about: the site renders the last thing each server said while every server is off.

Four defects the walk found — two of them already shipped

Walked in a browser against the live rig: a real Rust server behind the real sidecar, plus a second, deliberately unreachable server carrying a fixture shaped as the plugin emits.

  1. An unreachable refresh erased what the server last said. Phase 3's loop called putState — the whole-row write — with two fields when nothing answered, so hostname, level, seed, world_size and wipe_id went NULL the first time a host rebooted. The page then read "Offline" with nothing beside it: not "here is what we know" but "we have never heard of it", which defeats this phase's criterion exactly. markUnreachable moves three columns and mentions no others.
  2. "Last reported" read the wrong timestamp. updated_at is when we wrote the row, and a failed poll writes it too — so an offline server claimed it had reported just now, every 30 seconds, for as long as it stayed down. last_seen_at is new and only a frame moves it.
  3. Feed rows carried a bare time of day, so six-week-old events read as this afternoon once the feed was filtered to a past wipe.
  4. A mistyped address was dressed as a fault — "No such server / Something went wrong".

Plus a fifth, smaller: the Online tab listed players under a header reading Offline with nothing saying which it was.

Notes for the reviewer

  • useAsync cannot poll — it blanks its data on every dependency change, so a 20s refresh built on it would clear the killfeed and re-fill it four times a minute. hooks/usePolled.js is the module's own: invisible when it succeeds, keeps the rows and reports the error when it fails.
  • The footer slot's cost is real and is stated in the code: core renders SiteFooter inside PublicLayout, and every public page renders PublicLayout itself, so that component mounts once per public page view. It renders nothing until it has an answer, nothing at all if the request fails, and never polls.
  • The client test fake was nearly core — it prefixed routes without stripping the trailing separator, so the first module to register an index route failed the nav check for a link that works perfectly in a browser.
  • server/test/refresh.test.js asserts against the SQL rather than a round trip, because the defect is about which columns a statement names.

Checks

npm test (server 101 / client 39), check:imports, check:bundle, check:swagger, check:externals and a locally regenerated routes.manifest.json against the pinned core all green. The chunk's identity check passed in the browser with a clean console.

  • AI-assisted: written with Claude Code.

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

Phase 4 of [`docs/modules/rust/PLAN.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/modules/rust/PLAN.md) — recorded there as §17 (docs#256). One repo, and the first phase whose deliverable is something a visitor looks at. ## What it adds - **`/rust` is the server list** and the module's landing page (D12). Registered with an empty path; core renders that as the namespace root. `/rust/servers/:id` hangs beneath it. - **One server, four tabs** (D13) — feed, leaderboard, who is on, wipes. Tab, feed filter, wipe and leaderboard sort all live in the URL, so *"last wipe's leaderboard on Main"* is a link, back undoes a click, and a refresh lands where the reader was. - **Polling while the tab is visible** (D14): the feed and the presence list every 20s, paused by the Page Visibility API, refreshed the instant a viewer returns. The leaderboard and the wipe list load once. - **`site.footer.status` filled with a live count** (D15), linking to `/rust`. - **`GET …/servers/:id`**, the one route under that path that can say a server does not exist — every other one answers an empty list for an id nobody configured, which is a perfectly good answer to its own question. - `capabilities` grows to what the pages serve, for phase 5's Android leg to feature-detect against. Nothing on these pages calls a game server. Every field comes from this module's own tables, which is exactly what the criterion is about: **the site renders the last thing each server said while every server is off.** ## Four defects the walk found — two of them already shipped Walked in a browser against the live rig: a real Rust server behind the real sidecar, plus a second, deliberately unreachable server carrying a fixture shaped as the plugin emits. 1. **An unreachable refresh erased what the server last said.** Phase 3's loop called `putState` — the whole-row write — with two fields when nothing answered, so `hostname`, `level`, `seed`, `world_size` and `wipe_id` went NULL the first time a host rebooted. The page then read "Offline" with nothing beside it: not *"here is what we know"* but *"we have never heard of it"*, which defeats this phase's criterion exactly. `markUnreachable` moves three columns and mentions no others. 2. **"Last reported" read the wrong timestamp.** `updated_at` is when *we* wrote the row, and a failed poll writes it too — so an offline server claimed it had reported just now, every 30 seconds, for as long as it stayed down. `last_seen_at` is new and only a frame moves it. 3. **Feed rows carried a bare time of day**, so six-week-old events read as this afternoon once the feed was filtered to a past wipe. 4. **A mistyped address was dressed as a fault** — "No such server / Something went wrong". Plus a fifth, smaller: the Online tab listed players under a header reading `Offline` with nothing saying which it was. ## Notes for the reviewer - **`useAsync` cannot poll** — it blanks its data on every dependency change, so a 20s refresh built on it would clear the killfeed and re-fill it four times a minute. `hooks/usePolled.js` is the module's own: invisible when it succeeds, keeps the rows and reports the error when it fails. - **The footer slot's cost is real and is stated in the code**: core renders `SiteFooter` inside `PublicLayout`, and every public page renders `PublicLayout` itself, so that component mounts once per public page view. It renders nothing until it has an answer, nothing at all if the request fails, and never polls. - **The client test fake was *nearly* core** — it prefixed routes without stripping the trailing separator, so the first module to register an index route failed the nav check for a link that works perfectly in a browser. - `server/test/refresh.test.js` asserts against the SQL rather than a round trip, because the defect is about which columns a statement names. ## Checks `npm test` (server 101 / client 39), `check:imports`, `check:bundle`, `check:swagger`, `check:externals` and a locally regenerated `routes.manifest.json` against the pinned core all green. The chunk's identity check passed in the browser with a clean console. - [x] AI-assisted: written with Claude Code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-17 02:41:39 +00:00
feat: the first pages, and what a browser walk found behind them
All checks were successful
PR Checks / client-build (pull_request) Successful in 15s
PR Checks / frozen-manifest (pull_request) Successful in 36s
PR Checks / server-tests (pull_request) Successful in 7m58s
22fd8c5da7
Phase 4. `/rust` is the server list and the module's landing page (D12);
`/rust/servers/:id` is one server with four tabs — feed, leaderboard, who is
on, wipes (D13). Everything selectable lives in the URL, so any view of the
page is a link. The feed and the presence list poll every twenty seconds while
the tab is visible and not at all when it is not (D14); the leaderboard and the
wipe list load once. `site.footer.status` is filled with a live server and
player count (D15).

Nothing on these pages calls a game server. Every field comes from this
module's own tables, which is what the phase criterion is about: the site
renders the last thing each server said while every server is off.

Walking that criterion in a browser against a live rig found four defects, two
of them already shipped in phase 3:

  * An unreachable refresh called `putState` — the whole-row write — with two
    fields, so a host that rebooted lost its hostname, map, size, seed and wipe
    id. The list then read "Offline" with nothing beside it, which is not "here
    is what we know" but "we have never heard of it". `markUnreachable` now
    moves three columns and mentions no others.
  * "Last reported" read `updated_at`, which a FAILED poll writes too — so an
    offline server claimed it had reported just now, every thirty seconds, for
    as long as it stayed down. `last_seen_at` is the new column, moved only by a
    frame that arrived.
  * Feed rows showed a bare time of day, so three events from six weeks ago all
    read as this afternoon once the feed was filtered to a past wipe.
  * `/rust/servers/typo` rendered core's ErrorState under its own heading and
    read "No such server / Something went wrong", sending a reader who mistyped
    a URL looking for an outage.

Also: a detail route (`GET …/servers/:id`), because it is the only route under
that path that can say a server does not exist — the other four answer an empty
list for an id nobody configured, and each of those is a good answer to its own
question.

`useAsync` cannot poll: it blanks its data on every dependency change, so a
twenty-second refresh built on it would clear the killfeed and re-fill it four
times a minute. `hooks/usePolled.js` is the module's own, invisible when it
succeeds and keeping the rows when it fails.

The client test fake was *nearly* core — it prefixed routes without stripping
the trailing separator, so the first module to register an index route failed
the nav check for a link that works in a browser. It now copies core's line
character for character.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
whitlocktech merged commit 7e1f037aad into main 2026-09-17 02:43:29 +00:00
whitlocktech deleted branch feat/phase-4-first-pages 2026-09-17 02:43:30 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/Module-Rust#4
No description provided.