// ── Admin · Rust · Visibility ───────────────────────────────────────────── // // Who may see who is online. The org lead's rule (2026-09-22): nothing names who // is online by default — the narrowest audience, staff, unless an operator // deliberately widens it here. A count of players is public at every setting. // // One fleet default and an optional override per server, because a creative or // PvE server may reasonably publish a roll call a PvP server must not — and a // server that has not chosen follows the fleet, so narrowing the fleet narrows // every server that never said otherwise. // // The page says what "who is online" covers, because it is wider than the tab // of the same name: the killfeed, chat and joins in the feed, and the // leaderboard's "last seen" all name a player who was on at a given moment. // // Phase 9 adds a second setting beside it: who may see a CLAN ROSTER (D48). It // defaults to the clan's own members and staff, and widening it widens online // status too, because a roster row carries it — the page says so. The same card // lists each server's clan board: a server whose clans cannot be read, one at // the game's 100-clan ceiling (D55), and one running the uMod Clans plugin, // whose clans are a separate system and never Teams (D47). import { useCallback, useEffect, useState } from 'react' import { ErrorState, Loading, useAsync } from '../../core.js' import api from '../../api.js' const INHERIT = '' const LABEL = { staff: 'Staff only', signed_in: 'Signed-in members', public: 'Everyone', } const CLAN_LABEL = { members: 'The clan’s members and staff', signed_in: 'Signed-in members', public: 'Everyone', } const CLAN_DESCRIBE = { members: 'Players whose linked Rust account is in the clan, plus admins and moderators. The default.', signed_in: 'Anybody with an account on this site.', public: 'Anybody at all, signed in or not.', } const DESCRIBE = { staff: 'Admins and moderators. The default.', signed_in: 'Anybody with an account on this site.', public: 'Anybody at all, signed in or not.', } function Card({ title, subtitle, children }) { return (

{title}

{subtitle && ( {subtitle} )}
{children}
) } function AudienceSelect({ value, onChange, audiences, inherit = null, label }) { return ( ) } export default function Visibility() { const [reloads, setReloads] = useState(0) const { data, error: loadError } = useAsync(() => api.adminVisibility.read(), [reloads]) const [fleet, setFleet] = useState('staff') const [clanRoster, setClanRoster] = useState('members') const [servers, setServers] = useState({}) const [busy, setBusy] = useState(false) const [error, setError] = useState('') const [saved, setSaved] = useState(false) // The form starts from what the server said and is reset from it after every // save — the answer to a PUT is the new state, so what is on screen is always // the site's word rather than what this page sent. const load = useCallback((state) => { setFleet(state.presence.fleet) setClanRoster((state.clans && state.clans.roster) || 'members') setServers(Object.fromEntries(state.presence.servers.map((s) => [s.id, s.override || INHERIT]))) }, []) useEffect(() => { if (data) load(data) }, [data, load]) if (loadError) return if (!data) return const audiences = data.audiences const rows = data.presence.servers const dirtyFleet = fleet !== data.presence.fleet const dirtyServers = rows.filter((s) => (servers[s.id] ?? INHERIT) !== (s.override || INHERIT)) const clans = data.clans || { audiences: [], roster: 'members', servers: [] } const dirtyClans = clanRoster !== clans.roster const dirty = dirtyFleet || dirtyServers.length > 0 || dirtyClans const effective = (id) => servers[id] || fleet const widened = fleet !== 'staff' || rows.some((s) => effective(s.id) !== 'staff') const save = async (e) => { e.preventDefault() setBusy(true) setError('') setSaved(false) try { const body = {} if (dirtyFleet) body.fleet = fleet if (dirtyClans) body.clanRoster = clanRoster if (dirtyServers.length) { body.servers = Object.fromEntries(dirtyServers.map((s) => [s.id, servers[s.id] || null])) } load(await api.adminVisibility.save(body)) setSaved(true) setReloads((n) => n + 1) } catch (err) { setError(err.message || 'That did not save.') } finally { setBusy(false) } } return (

Nothing on this site names who is online unless you choose to show it. That covers more than the Online tab: the joins, deaths and chat in each server’s feed, and the leaderboard’s “last seen”, all say that a named player was on at a given moment. How many players are online is always shown.

{DESCRIBE[fleet]}
{rows.length === 0 && (

No servers are configured yet.

)} {rows.map((s) => (
{s.name} {!s.enabled && · disabled} setServers((prev) => ({ ...prev, [s.id]: v }))} audiences={audiences} inherit={`Default (${LABEL[fleet] || fleet})`} label={`Who is online on ${s.name}`} /> {servers[s.id] ? 'its own setting' : 'follows the default'}
))}
{widened && (

Wider than staff: on a PvP server, knowing who is on tells a raiding party whose base is undefended.

)}
{CLAN_DESCRIBE[clanRoster]}

Each clan’s name, colour, score and member count are always public.

{clanRoster !== 'members' && (

A roster also shows which members are online right now, so this shows who is on to{' '} {clanRoster === 'public' ? 'everyone' : 'every signed-in member'} as well.

)}
{saved && !dirty && Saved.} {error && {error}}
) } /** * What each server's clan board says about itself. Only the servers with * something to report are listed: a board that is current, complete and read * normally is the case that needs no sentence. */ function ClanBoards({ servers }) { const notes = [] for (const s of servers) { if (s.umodClans) { notes.push([s, 'is running the uMod Clans plugin. Its clans are a separate system from the game’s own, and only the game’s clans appear on this site.']) } if (!s.supported) { notes.push([s, s.reason ? `cannot report its clans: ${s.reason}.` : 'has not reported its clans yet.']) } else if (s.truncated) { notes.push([s, 'is at the game’s limit of 100 listed clans, so clans beyond the top 100 by score are not shown, and a disbanded clan is not removed until it drops below.']) } else if (!s.fresh) { notes.push([s, 'has not reported its clans recently, so they are shown as last reported.']) } } if (!notes.length) return null return ( ) } const selectStyle = { background: 'var(--panel-flat, transparent)', color: 'var(--text)', border: '1px solid var(--line)', borderRadius: 'var(--radius-input, 6px)', padding: '4px 8px', fontSize: '0.84rem', }