feat: the first pages, and what a browser walk found behind them
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
This commit is contained in:
@@ -19,6 +19,8 @@
|
||||
import { registry, coreApiVersion } from './core.js'
|
||||
|
||||
import Servers from './routes/public/Servers.jsx'
|
||||
import ServerDetail from './routes/public/ServerDetail.jsx'
|
||||
import FooterStatus from './components/FooterStatus.jsx'
|
||||
|
||||
// The module id, exactly as `module.json` spells it. Core keys the registry by it
|
||||
// and prefixes every route path with it.
|
||||
@@ -33,7 +35,7 @@ const ID = 'rust'
|
||||
// installed side by side cannot collide, and an operator can see from a URL which
|
||||
// module served it.
|
||||
//
|
||||
// So this page is at `/rust/servers`.
|
||||
// So the list below is at `/rust` and the detail page at `/rust/servers/:id`.
|
||||
//
|
||||
// **Note what is NOT here: an auth wrapper.** `gate: { roles: [...] }` is
|
||||
// available and core applies it as its own `RoleGate`; supplying your own is not
|
||||
@@ -41,11 +43,22 @@ const ID = 'rust'
|
||||
// see what, and they only do if one thing decides.
|
||||
//
|
||||
// R8's landing page is the server list, and `/rust/servers/:id` hangs beneath it.
|
||||
// The detail route is a later phase's, and it is deliberately not stubbed here: a
|
||||
// registered route that renders nothing is a 200 with a blank page, which is
|
||||
// worse than the 404 an unregistered one gives.
|
||||
//
|
||||
// **The list is registered with an EMPTY path**, which core renders as the
|
||||
// module's namespace root: `/rust`. The prefixing code strips the separator it
|
||||
// would otherwise leave behind (`registry.js`: `${id}/${path}` with trailing
|
||||
// slashes trimmed), so a module can own its own root without being able to spell
|
||||
// its way out of it. Phase 1 served this page at `/rust/servers` and left `/rust`
|
||||
// to core's CMS catch-all; the org lead settled it at `/rust` in phase 4, so the
|
||||
// address an operator links to is the module's name.
|
||||
//
|
||||
// React Router ranks a static segment above a dynamic one, so `/rust` wins
|
||||
// against core's `/:slug` CMS route without depending on registration order.
|
||||
registry.registerRoutes(ID, {
|
||||
public: [{ path: 'servers', element: <Servers /> }],
|
||||
public: [
|
||||
{ path: '', element: <Servers /> },
|
||||
{ path: 'servers/:id', element: <ServerDetail /> },
|
||||
],
|
||||
})
|
||||
|
||||
// ── Nav ───────────────────────────────────────────────────────────────────
|
||||
@@ -67,9 +80,23 @@ registry.registerRoutes(ID, {
|
||||
// one is the only row in its sidebar with no glyph, which reads as breakage.
|
||||
registry.registerNav(ID, {
|
||||
area: 'public',
|
||||
items: [{ label: 'Servers', to: '/rust/servers' }],
|
||||
items: [{ label: 'Servers', to: '/rust' }],
|
||||
})
|
||||
|
||||
// ── Extension slots ───────────────────────────────────────────────────────
|
||||
//
|
||||
// Core declares a slot, only core may declare one, and at most one module may
|
||||
// fill it (§3.7). `site.footer.status` is the status-ish spot in core's footer
|
||||
// info row: core owns the position and passes `linkStyle`; the label, the
|
||||
// destination, the data and whether anything renders at all are the module's.
|
||||
//
|
||||
// It is a CLIENT slot and cannot be named in `module.json`'s `extensions` —
|
||||
// that array is validated against the SERVER registry, and naming a client slot
|
||||
// there fails the load outright with `unknown extension slot`. Phase 1 found
|
||||
// that the hard way; the two halves of R13 are declared in different places on
|
||||
// purpose.
|
||||
registry.registerExtension(ID, 'site.footer.status', FooterStatus)
|
||||
|
||||
// `module.json`'s `coreApi` range was checked by the loader before this file was
|
||||
// ever served, so there is nothing to re-check here. Log it anyway: a mismatch
|
||||
// between the core that validated the manifest and the core that published this
|
||||
|
||||
Reference in New Issue
Block a user