// ── This module's fill for `admin.users.detail` ─────────────────────────── // // R13's first slot, and the phase criterion as an operator meets it: the Steam // id inside core's own user page, under core's own security panel. // // **The slot hands over `userId` and nothing else** — not a client. So this file // builds its own bindings for the routes the server half registered // (`api.adminUserLinks`), which is §3.5's rule applied to a slot: the two ends of // a call belong to the same module even when the URL between them is core's. // // **Most users have no Rust account, so most of the time this renders nothing.** // A panel that announced "no linked Steam accounts" on every user page in a // community that also runs a UO shard would be noise on the overwhelming // majority of them. Silence is the honest answer to "what does the Rust module // know about this person" when it is nothing. import { useCallback, useState } from 'react' import { ago, count, duration } from '../../lib/format.js' import { useAsync } from '../../core.js' import api from '../../api.js' /** Six lines of furniture the §3.4 kit does not carry, so it is vendored. */ function SectionTitle({ children }) { return (
{children}
) } /** One server's all-time totals for this player. */ function ServerRow({ server }) { return (
  • {server.serverName} {count(server.kills)} kills · {count(server.deaths)} deaths · {duration(server.playtimeSec)} {server.wipes > 1 ? ` · ${server.wipes} wipes` : ''}
  • ) } /** One linked Steam account: who it is, when it was linked, and the way out. */ function LinkPanel({ userId, link, onRemoved }) { const [busy, setBusy] = useState(false) const [error, setError] = useState('') async function unlink() { setBusy(true) setError('') try { await api.adminUserLinks.remove(userId, link.steamId) await onRemoved() } catch (err) { setError(err.message || 'Could not unlink that account.') setBusy(false) } } return (
    {link.name || link.steamId}
    {link.steamId} · linked {ago(link.linkedAt)} {link.serverId ? ` on ${link.serverId}` : ''} {link.lastSeen ? ` · last played ${ago(link.lastSeen)}` : ' · never played'}
    {/* Worth showing only when they differ: the name on the link is what they were called when they linked, the other is what the game last saw. A rename is the ordinary reason, and an operator reading a support ticket wants both names. */} {link.linkedName && link.name && link.linkedName !== link.name && (
    Linked as “{link.linkedName}”.
    )}
    {error && (

    {error}

    )} {link.servers.length > 0 && ( )}
    ) } /** * Phase 7's half of the panel: what this person may do in game. * * It renders whenever they hold anything, INCLUDING when they have linked no * Steam account — which is the one case worth going out of the way for. A grant * against an unlinked person is authored, stored, pushed nowhere, and identical * to a working one everywhere except here. */ function PermissionsPanel({ userId, data, onChanged }) { const [busy, setBusy] = useState(false) const [error, setError] = useState('') const [permission, setPermission] = useState('') const act = async (fn) => { setBusy(true) setError('') try { await fn() await onChanged() } catch (err) { setError(err.message || 'That did not work.') } finally { setBusy(false) } } if (!data) return null const nothing = data.groups.length === 0 && data.grants.length === 0 return (
    Permissions
    {nothing && (

    Nothing granted.

    )} {data.groups.map((group) => (
    {group.title || group.name}{' '} group · {group.scope === '*' ? 'every server' : group.scope} {group.permissions.length ? ` · ${group.permissions.join(', ')}` : ' · carries nothing'}
    ))} {data.grants.map((row) => (
    {row.permission}{' '} {row.scope === '*' ? 'every server' : row.scope} {row.source !== 'admin' ? ` · ${row.source}` : ''}
    ))} {!nothing && data.reaches.length === 0 && (

    This account has linked no Steam id, so none of it reaches a game yet. It will apply by itself when they link.

    )}
    { event.preventDefault() if (!permission.trim()) return act(() => api.adminUserPermissions.grant(userId, { permission: permission.trim().toLowerCase() }), ) setPermission('') }} > setPermission(event.target.value)} style={{ flex: 1 }} />
    {error && (

    {error}

    )}
    ) } export default function UserRustSections({ userId }) { // Core's `useAsync` has no refresh, so a counter in the deps is how this // re-reads after its own write (the same shape the player page uses). const [reloads, setReloads] = useState(0) const { data } = useAsync(() => api.adminUserLinks.list(userId), [userId, reloads]) const { data: permissions } = useAsync( () => api.adminUserPermissions.list(userId), [userId, reloads], ) const reload = useCallback(() => setReloads((n) => n + 1), []) // No `Loading` and no `ErrorState`, deliberately. This is a section inside // somebody else's page: a spinner on every user page for a module most users // have nothing to do with is worse than a section that appears when it has // something, and a failure here must not replace core's own user detail with an // error card. // **Both reads decide whether this section exists**, and the second one is the // reason. A browser walk found it: a person can hold permissions and have // linked no Steam account — which is exactly the state an operator most needs // to see, because it is the one that reaches nobody — and a section gated on // links alone hides it completely. const holdsSomething = permissions && (permissions.groups.length > 0 || permissions.grants.length > 0) if (!data || (data.links.length === 0 && !holdsSomething)) return null return (
    Rust
    {data.links.map((link) => ( ))} {data.links.length > 0 && (

    A link is fleet-wide and totals are all-time, summed across every wipe. Unlinking here is recorded in the activity log — it is the way back for a player who linked the wrong account and cannot reach it in game.

    )} {/* Inside the same section rather than beside it: "who is this in game" and "what may they do there" are one question asked twice, and an operator reading a support ticket has both in front of them. The note above belongs to the links, so it sits with them rather than under the panel it would otherwise appear to describe. */}
    ) }