// ── This module's fill for core's `site.footer.status` slot ─────────────── // // R13, and the contract is MODULE_API.md §3.7. Core owns the position in the // footer's info row and the separator around it, and passes `linkStyle` so the // row stays visually one row. **The label, the destination, the data and whether // anything renders at all are this component's** — that is the whole division, // and it is why the slot is named for a place rather than for a meaning. // // ── The live count, and what it costs ───────────────────────────────────── // // The org lead chose a live count ("3 servers · 42 online") over a static link, // so this fetches. Be clear-eyed about where it fetches from: core renders // `SiteFooter` inside `PublicLayout`, and every public page renders // `PublicLayout` ITSELF (§3.3) — so this component mounts once per public page // view, not once per session. Every public page on the site therefore carries one // `/public/rust/servers` request, including pages that have nothing to do with // Rust. // // Two things keep that honest rather than merely cheap: // // • **It renders NOTHING until it has an answer, and nothing again if the // request fails.** An unfilled slot renders nothing and core's `wrap` takes // the separator with it, so a failed fetch degrades to exactly the footer an // instance with no module installed has. A spinner in a footer would be worse // than silence on every page of the site. // • **It never polls.** One request per page view is a cost; a timer in the // footer of every page would be a different kind of thing entirely. // // If that per-page request ever shows up in an operator's logs as a problem, the // fix is a short-lived module-scope cache here — the decision to keep the number // live stays intact, and nothing else on the site has to change. import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' import api from '../api.js' export default function FooterStatus({ linkStyle }) { const [summary, setSummary] = useState(null) useEffect(() => { let live = true api.servers .list() .then(({ servers }) => { if (!live) return // `online` already accounts for staleness — the model refuses to let a // row that has not been written in five minutes claim a server is up — // so this is a sum, not a judgement. setSummary({ servers: servers.length, players: servers.reduce((total, server) => total + (server.online ? server.players : 0), 0), }) }) // Silence, deliberately. This is the footer of every page on the site; a // module that cannot reach its own API has nothing to say there. .catch(() => {}) return () => { live = false } }, []) if (!summary || summary.servers === 0) return null return ( {summary.servers === 1 ? '1 server' : `${summary.servers} servers`} {' · '} {summary.players === 1 ? '1 online' : `${summary.players} online`} ) }