// ── 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 && ( )}
    ) } 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 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. if (!data || data.links.length === 0) return null return (
    Rust
    {data.links.map((link) => ( ))}

    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.

    ) }