import { useCallback, useEffect, useRef, useState } from 'react' import { useShardFeed } from '../../lib/useShardFeed.js' import { describe, kindLabel } from '../../lib/shardEvents.js' import { ago } from '../../lib/format.js' import api from '../../api.js' import { ErrorState, Loading } from '../../core.js' // Full live feed from the admin SSE channel — every kind, incl. staff audit, // cheat detection and login attempts that the public channel never carries. function AdminLiveFeed() { const { events, connected } = useShardFeed({ url: api.adminShardStreamUrl, max: 60 }) return (

Live feed (all events)

{connected ? 'Live' : 'Offline'}
{events.length === 0 ? (

Waiting for shard events…

) : ( )}
) } // uo-link sidecar control panel. The auth token is write-only over this API — // stored encrypted, never returned — same convention as the Discord bot token. // Saving (re)starts the WS ingest client, so Enabled/URL/token changes take // effect immediately with no redeploy. function Toggle({ checked, onChange, label }) { return ( ) } const STATUS_COLOR = { connected: '#7fd0a4', reconnecting: '#e0b070', error: '#d98b84', disconnected: 'var(--muted)', } function StatusPanel({ config }) { const color = STATUS_COLOR[config.status] || 'var(--muted)' const ingest = config.ingest || {} const health = config.health || {} return (
{config.status || 'disconnected'}
{config.statusDetail && (

{config.statusDetail}

)}
Shard link: {config.pluginConnected ? 'up' : 'down'} WS ingest: {ingest.connected ? 'connected' : 'offline'} Reconnects: {ingest.reconnects ?? 0} SSE clients: {(config.sse?.publicClients ?? 0) + (config.sse?.adminClients ?? 0)} {config.lastEventAt && Last event: {new Date(config.lastEventAt).toLocaleString()}} {health.uptime && Sidecar uptime: {health.uptime}}
) } // ── Game-account signup ───────────────────────────────────────────────────── // // This field lived in core's Site Settings until slice 3 of the extraction. It // moved here rather than being deleted or left behind, because its help text has // always described an agreement between this site and a ServUO shard — and half // of that agreement is configured in Bridge.cfg, which core has never heard of. // // The setting key and value are unchanged (`game_account_signup`), so an // instance that had this configured finds it here, set to what it was. const SIGNUP_MODES = [ { value: 'disabled', label: 'Disabled — link an existing account only' }, { value: 'website', label: 'Website — the site creates game accounts' }, { value: 'hybrid', label: 'Hybrid — site or in-game (recommended)' }, { value: 'game', label: 'Game only — created in the game client, not the site' }, ] function GameSignup() { const [mode, setMode] = useState(null) const [busy, setBusy] = useState(false) const [msg, setMsg] = useState('') const [error, setError] = useState('') useEffect(() => { let active = true api.admin.getSignupMode() .then((r) => active && setMode(r.mode)) .catch(() => active && setError('Could not load the signup mode.')) return () => { active = false } }, []) async function save(next) { const previous = mode setMode(next); setBusy(true); setMsg(''); setError('') try { await api.admin.saveSignupMode(next) setMsg('Saved.') } catch (err) { setMode(previous) // the select must not show a mode the server did not take setError(err.message || 'Could not save.') } finally { setBusy(false) } } return (

Game-account creation

Whether players can create a GAME account (for the game client) from the site. The game server’s own SignupMode (Bridge.cfg) must agree: website/hybrid accept site-created accounts, game refuses them. When enabled, a “Create a game account” form appears in the player portal and after an invite is accepted.

{msg && {msg}} {error && {error}}
) } // ── Town crier ────────────────────────────────────────────────────────────── function TownCrier() { const [id, setId] = useState('') const [text, setText] = useState('') const [durationSec, setDurationSec] = useState(3600) const [busy, setBusy] = useState(false) const [msg, setMsg] = useState('') const [error, setError] = useState('') async function post() { setBusy(true); setMsg(''); setError('') const lines = text.split('\n').map((l) => l.trim()).filter(Boolean) if (!id.trim() || lines.length === 0) { setBusy(false) return setError('An id and at least one line are required.') } try { await api.admin.postTownCrier({ id: id.trim(), lines, durationSec: Number(durationSec) || undefined }) setMsg(`Posted “${id.trim()}”.`) } catch (err) { setError(err.message || 'Could not post.') } finally { setBusy(false) } } async function remove() { if (!id.trim()) return setError('Enter the id to remove.') setBusy(true); setMsg(''); setError('') try { await api.admin.deleteTownCrier(id.trim()) setMsg(`Removed “${id.trim()}”.`) } catch (err) { setError(err.message || 'Could not remove.') } finally { setBusy(false) } } return (

Town crier

Broadcast a message that every in-game town crier announces until it expires. Re-posting the same id replaces it.