// ── The feed: what happened on one server ───────────────────────────────── // // Rows come from `/public/rust/servers/:id/events`, which serves a default-deny // ALLOWLIST (`server/catalogue.js`). Everything carrying an IP address, a // player's report about another player, or the grid square somebody's base is in // is stored and never answered here — so this component cannot leak one by // forgetting to filter, which is the point of the boundary living on the server. // // It polls (org lead, phase 4): every twenty seconds while the tab is visible, // paused when it is not. `usePolled` keeps the rows on screen across a refresh — // see the comment at the top of that file for why core's `useAsync` cannot do // this job. import { EmptyState, ErrorState, Loading } from '../core.js' import { describe, FILTERS, kindsFor } from '../lib/feed.js' import { ago, clock } from '../lib/format.js' import usePolled from '../hooks/usePolled.js' import api from '../api.js' const TONE = { kill: 'var(--accent-bright)', death: 'var(--muted)', join: 'var(--mode-live, #5fb98a)', leave: 'var(--dim)', chat: 'var(--text)', server: 'var(--mode-maint, #e6c26a)', other: 'var(--muted)', } export default function Feed({ serverId, wipeId, filter, onFilter }) { const kinds = kindsFor(filter) const { data, error, loading, at } = usePolled( () => api.servers.events(serverId, { kinds, wipe: wipeId, limit: 100 }), // The key is the QUESTION. Changing server, wipe or filter blanks the rows, // because what is on screen is an answer to a different one; a poll tick // does not, because it is the same question asked again. { key: `${serverId}|${wipeId || ''}|${filter}`, intervalMs: 20_000 }, ) const events = data ? data.events : [] return (
{/* What a refresh is FOR: saying when the page last managed one. Without it a feed that stopped updating looks exactly like a quiet server. */} {at && ( updated {ago(at)} )} {error && ( the last refresh failed — showing what we had )}
{loading && } {/* An error with nothing to fall back on is the only case that takes over the panel. A failed REFRESH keeps the rows and says so in the line above, because a site whose premise is "it renders while the game is off" must not blank itself the first time a request does. */} {error && !data && } {data && events.length === 0 && ( )} {events.length > 0 && (
    {events.map((event) => { const line = describe(event) return (
  1. {line.actor && {line.actor}} {line.actor && (line.join || ' ')} {line.verb} {line.subject && ' '} {line.subject && {line.subject}} {line.detail && ( {' · '} {line.detail} )}
  2. ) })}
)}
) } const selectStyle = { background: 'var(--panel-flat, transparent)', color: 'var(--text)', border: '1px solid var(--line)', borderRadius: 'var(--radius-input, 6px)', padding: '3px 8px', fontSize: '0.78rem', }