Clears the 124 CODE_SMELL findings from the SonarQube scan (server, client, and bot). All changes are behaviour-preserving refactors — no route, protocol, schema, or config changes — verified against the full server (381) and client (43) test suites plus a clean client build. By rule: - S3776 (20, cognitive complexity): extract helpers/handlers so each function drops under the threshold — shard model upsert builders, page/wiki update, block validation, notification stream mapping (dispatch table), SSO mobile login, shard ingest deps, uo-link socket backfill/connect, the bot slash- command dispatchers + discord manager, and the Shard/UserDetail/HeroEditor/ CharacterStats React components. - S4624 (34, nested template literals): pull inner templates into locals / a withQs() helper; rewrite shardEvents.describe() as a formatter table. - S3358 (35, nested ternaries): lift to if/else vars, lookup maps, small components, or guarded JSX expressions. - S6479 (12, array-index React keys): key by stable content instead of index (two in-editor lists left as-is; index matches their by-index edit model). - S6353 (6): [0-9]/[^0-9] -> \d/\D. S125 (5): reword state-shape comments that parsed as code. S3800/S3782 (botScore): JSDoc-type PATH_WEIGHTS tuples. - S6481 (2): memoize Auth/Site context values (and SiteContext brand). - S4144: dedupe HeroEditor upload handler into useImageUpload(). - S1126 (2), S6035, S5869 (redundant A-Z under /i), S5843 (town-name regex -> prefix list): assorted one-liners. Co-Authored-By: Claude <noreply@anthropic.com>
80 lines
2.9 KiB
JavaScript
80 lines
2.9 KiB
JavaScript
import { useEffect, useState } from 'react'
|
|
|
|
// A small stat-tile row for a "My Characters" page: total characters, how many
|
|
// are online right now, and how many game accounts are linked. `scope` is the
|
|
// shard api object (admin or player self-service). Renders nothing until an
|
|
// account is linked, so the empty/link-prompt state below it stands alone.
|
|
//
|
|
// It fetches the same rosters GameAccounts loads; for a personal page that's at
|
|
// most a couple of extra live round-trips, and keeps this presentational bit
|
|
// decoupled from GameAccounts' per-account roster loading.
|
|
|
|
function Tile({ value, label }) {
|
|
return (
|
|
<div className="panel" style={{ padding: 20, textAlign: 'center' }}>
|
|
<div className="display" style={{ fontSize: '1.6rem', color: 'var(--head)' }}>{value}</div>
|
|
<div className="sans" style={{ color: 'var(--accent)', fontSize: '0.68rem', fontWeight: 700, letterSpacing: '0.15em', textTransform: 'uppercase', marginTop: 8 }}>
|
|
{label}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Fold the settled roster results into totals. `complete` is false when any
|
|
// account's roster failed (a partial result — shown as a dash rather than a
|
|
// misleadingly low count).
|
|
function summarizeRosters(rosters) {
|
|
let chars = 0
|
|
let online = 0
|
|
let complete = true
|
|
for (const r of rosters) {
|
|
if (r.status !== 'fulfilled') {
|
|
complete = false
|
|
continue
|
|
}
|
|
const cs = r.value.chars || []
|
|
chars += cs.length
|
|
online += cs.filter((c) => c.online).length
|
|
}
|
|
return { chars, online, complete }
|
|
}
|
|
|
|
export default function CharacterStats({ scope }) {
|
|
const [stats, setStats] = useState(null)
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
;(async () => {
|
|
try {
|
|
const accounts = await scope.accounts()
|
|
const linked = accounts.length
|
|
if (linked === 0) {
|
|
if (!cancelled) setStats({ linked: 0 })
|
|
return
|
|
}
|
|
// Roster is a live round-trip and can be unavailable (503); tolerate a
|
|
// partial result so a restarting shard doesn't blank the whole row.
|
|
const rosters = await Promise.allSettled(accounts.map((a) => scope.roster(a.account)))
|
|
if (!cancelled) setStats({ linked, ...summarizeRosters(rosters) })
|
|
} catch {
|
|
if (!cancelled) setStats({ error: true })
|
|
}
|
|
})()
|
|
return () => { cancelled = true }
|
|
}, [scope])
|
|
|
|
// Hidden until we know an account is linked (or while first loading).
|
|
if (!stats || stats.error || stats.linked === 0) return null
|
|
|
|
// Counts depend on live rosters; show a dash if none came back.
|
|
const count = (n) => (stats.complete || stats.chars > 0 ? n : '—')
|
|
|
|
return (
|
|
<section className="grid-3" style={{ gap: 14, marginBottom: 26 }}>
|
|
<Tile value={count(stats.chars)} label="Characters" />
|
|
<Tile value={count(stats.online)} label="Online now" />
|
|
<Tile value={stats.linked} label={stats.linked === 1 ? 'Linked account' : 'Linked accounts'} />
|
|
</section>
|
|
)
|
|
}
|