// ── One server ──────────────────────────────────────────────────────────── // // R8's page beneath the landing page, and the phase-4 criterion lives here: it // renders the last thing this server said while every server is off. Nothing on // it is a live call to a game host — every panel reads this module's own tables, // filled by the ingest cursor — so a shard that has been down for a week renders // a week-old killfeed and a leaderboard that is still correct, rather than an // error page. // // ── Everything selectable is in the URL ─────────────────────────────────── // // Tab, feed filter, wipe and leaderboard sort all live in search parameters. // That costs a little ceremony here and buys the thing a community site is for: // "look at last wipe's leaderboard on Main" is a LINK. State held in `useState` // would make every one of those sentences unlinkable, lose the reader's place on // a refresh, and make the browser's back button leave the page instead of // undoing what they just clicked. // // `useSearchParams` comes from CORE's router (the shim in `src/shim/`), so it is // the same live navigation context core's own pages use. A module with its own // copy of react-router would get a `useParams` that returns nothing on a page // that otherwise renders perfectly — see `core.js`'s identity check. import { useSearchParams, useParams, Link } from 'react-router-dom' import { ErrorState, Loading, PageHeader, PublicLayout, useAsync } from '../../core.js' import Feed from '../../components/Feed.jsx' import Leaderboard from '../../components/Leaderboard.jsx' import Online from '../../components/Online.jsx' import Tabs from '../../components/Tabs.jsx' import WipeSelect, { ALL_TIME } from '../../components/WipeSelect.jsx' import Wipes from '../../components/Wipes.jsx' import { ago, count, day } from '../../lib/format.js' import api from '../../api.js' const TABS = [ { id: 'feed', label: 'Feed' }, { id: 'leaderboard', label: 'Leaderboard' }, { id: 'online', label: 'Online' }, { id: 'wipes', label: 'Wipes' }, ] export default function ServerDetail() { const { id } = useParams() const [params, setParams] = useSearchParams() const { data, loading, error } = useAsync(() => api.servers.get(id), [id]) const server = data ? data.server : null const tab = TABS.some((t) => t.id === params.get('tab')) ? params.get('tab') : 'feed' const filter = params.get('show') || 'all' const sort = params.get('sort') || 'kills' // `wipe` absent means all time; `wipe=current` means whatever wipe the server // is on now, which is a moving target and therefore a word rather than an id — // a link somebody shares stays about "now" rather than about the map that was // current when they sent it. const wipeParam = params.get('wipe') const wipeId = !wipeParam || wipeParam === ALL_TIME ? null : wipeParam === 'current' ? (server && server.wipeId) || null : wipeParam const set = (key, value) => { const next = new URLSearchParams(params) if (!value || value === 'all' || (key === 'tab' && value === 'feed')) next.delete(key) else next.set(key, value) // `replace` so that flipping between tabs does not fill the reader's history // with one entry per click — back should leave the page they arrived on. setParams(next, { replace: true }) } if (loading) { return ( ) } // A 404 from the detail route is the one answer the other four cannot give: // an unknown id has no events, no leaderboard and nobody online, and each of // those empty lists is a perfectly good answer to its own question. So this is // where "there is no such server" is said. // // **A mistyped address is not a fault, and must not be dressed as one.** The // first version of this page rendered core's `ErrorState` under the heading and // the result read "No such server / Something went wrong" — which sends a // reader who fat-fingered a URL looking for an outage. `ErrorState` is kept for // the case it is for: a request that failed for a reason nobody can see. if (error || !server) { const missing = !error || error.status === 404 return ( {!missing && }

Back to the server list

) } return (
{server.online ? `${count(server.players)}${server.maxPlayers ? ` / ${count(server.maxPlayers)}` : ''} online` : 'Offline'} {/* `lastSeenAt` is when a frame arrived; `updatedAt` is when this site last wrote the row, which a FAILED poll does too. Reading the second as the first is what made an offline server claim it had reported just now, every thirty seconds, for as long as it stayed down. */} {server.lastSeenAt ? `last reported ${ago(server.lastSeenAt)}` : 'has never reported'} {server.stale && server.lastSeenAt ? ' — out of date, so it is shown as offline' : ''} set('wipe', value === ALL_TIME ? null : value)} />
set('tab', next)} label={`${server.name} sections`} /> {tab === 'feed' && ( set('show', value)} /> )} {tab === 'leaderboard' && ( set('sort', value)} /> )} {tab === 'online' && } {tab === 'wipes' && ( { const next = new URLSearchParams(params) next.set('wipe', value) next.delete('tab') setParams(next, { replace: true }) }} /> )}
) } /** The world line under the heading — the things a Rust player asks first. */ function describeWorld(server) { const parts = [ server.level || null, server.worldSize ? `size ${count(server.worldSize)}` : null, server.seed ? `seed ${server.seed}` : null, server.wipedAt ? `wiped ${day(server.wipedAt)}` : null, ].filter(Boolean) return parts.length > 0 ? parts.join(' · ') : 'This server has not described itself yet.' }