Wire up the three uo-link sidecar surfaces that weren't integrated yet. Champion spawns - Ingest champ.update/champ.remove into a new shard_champs table (served from our own store, like online/houses); public /site/champs board with a nav link, live via the existing SSE feed (champ.* added to the public allowlist). Staff write plane (admin + moderator) - kick / ban / unban / broadcast via /admin/shard/*; actor is stamped server-side from the session, never the browser. Sidecar status codes mapped (403 disabled/ protected, 404 unknown, 503/504 transient). admin.audit events are logged and surfaced at /admin/shard/audit. - New admin "In-Game Ops" view (/admin/shard-ops), plus per-account Kick/Ban/Unban on the user-detail and character views (ShardAccountActions, self-gated to staff). Help-page (support) queue - Ingest page.new/updated/closed into a new shard_pages table; respond/close via /admin/shard/pages/*. Champ board and page queue are snapshotted from the sidecar's /champs and /pages on every WS (re)connect (guarded so a failed call never wipes local state). Verified live end-to-end against MariaDB + the Rust sidecar + ServUO; unit tests cover ingest routing (shardIngest.champsPages.test.js). Swagger regenerated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
107 lines
4.7 KiB
JavaScript
107 lines
4.7 KiB
JavaScript
// Shared formatting for shard events — used by the public Shard page, the
|
||
// Activity feed, and the admin live feed. One place decides how each kind reads
|
||
// and which category/badge it belongs to.
|
||
|
||
function nameOf(who) {
|
||
if (!who) return 'Someone'
|
||
if (typeof who === 'string') return who
|
||
return who.name || who.acct || 'Someone'
|
||
}
|
||
|
||
const n = (v) => Number(v || 0).toLocaleString()
|
||
|
||
// A one-line human description of an event. Accepts either a stored event
|
||
// (with .payload) or a raw live frame (fields at top level).
|
||
export function describe(ev) {
|
||
const p = ev.payload || ev
|
||
switch (ev.kind) {
|
||
case 'vendor.sale':
|
||
return `${p.itemType || 'An item'}${p.amount > 1 ? ` ×${p.amount}` : ''} sold for ${n(p.price)}gp`
|
||
case 'player.death':
|
||
return `${nameOf(p.who)} was slain${p.killer ? ` by ${nameOf(p.killer)}` : ''}`
|
||
case 'player.murdered':
|
||
return `${nameOf(p.victim)} was murdered${p.murderer ? ` by ${nameOf(p.murderer)}` : ''}`
|
||
case 'mob.killed':
|
||
return `${nameOf(p.killer)} killed ${nameOf(p.killed)}`
|
||
case 'skill.gain':
|
||
return `${nameOf(p.who)} gained ${p.skill}${p.base != null ? ` (${p.base})` : ''}`
|
||
case 'fame.change':
|
||
return `${nameOf(p.who)}’s fame changed to ${n(p.new)}`
|
||
case 'karma.change':
|
||
return `${nameOf(p.who)}’s karma changed to ${n(p.new)}`
|
||
case 'quest.complete':
|
||
return `${nameOf(p.who)} completed “${p.quest}”`
|
||
case 'house.decay':
|
||
return `${p.name || 'A house'} is now ${p.to || p.stage}${p.region ? ` — ${p.region}` : ''}`
|
||
case 'mob.login':
|
||
return `${nameOf(p.who)} entered the world`
|
||
case 'mob.logout':
|
||
return `${nameOf(p.who)} left the world`
|
||
case 'economy.supply':
|
||
return `Gold supply: ${n(p.gold)} across ${n(p.accounts)} accounts`
|
||
case 'server.hello':
|
||
return `Shard online — ${n(p.accounts)} accounts, ${n(p.mobiles)} mobiles`
|
||
case 'server.shutdown':
|
||
return 'Shard shut down'
|
||
case 'server.crashed':
|
||
return `Shard crashed${p.error ? `: ${p.error}` : ''}`
|
||
case 'champ.update': {
|
||
const where = p.name || p.type || 'A champion spawn'
|
||
if (p.status === 'active' && p.bossUp) return `${where}: boss is up${p.boss ? ` (${p.boss})` : ''}`
|
||
if (p.status === 'active') return `${where} is active${p.level != null ? ` — level ${p.level}` : ''}`
|
||
if (p.status === 'cooldown') return `${where} is on cooldown`
|
||
return `${where} is ${p.status || 'idle'}`
|
||
}
|
||
case 'champ.remove':
|
||
return `A champion spawn ended`
|
||
// Support (help-page) queue + in-game moderation (admin channel only)
|
||
case 'page.new':
|
||
return `New ${p.type || 'help'} page from ${nameOf(p.sender)}`
|
||
case 'page.updated':
|
||
return `Help page from ${nameOf(p.sender)} updated${p.handled ? ' (claimed)' : ''}`
|
||
case 'page.closed':
|
||
return `Help page ${p.pageId || ''} closed`
|
||
case 'admin.audit':
|
||
return `${p.actor || 'Staff'} ${p.action || 'acted'}${p.target ? ` on ${p.target}` : ''}${p.origin ? ` [${p.origin}]` : ''}`
|
||
// Staff / sensitive (admin channel only)
|
||
case 'audit.set':
|
||
return `${nameOf(p.staff) || 'Staff'} set ${p.prop} on ${p.target || p.targetSerial} (${p.old} → ${p.new})`
|
||
case 'audit.command':
|
||
return `${nameOf(p.staff) || 'Staff'} ran ${p.command}${p.args ? ` ${p.args}` : ''}`
|
||
case 'cheat.fastwalk':
|
||
return `Fast-walk flagged: ${nameOf(p.who)}${p.ip ? ` (${p.ip})` : ''}`
|
||
case 'account.login.attempt':
|
||
return `Login attempt: ${p.acct}${p.ip ? ` from ${p.ip}` : ''}`
|
||
case 'gold.change':
|
||
return `${p.acct}: gold ${p.delta >= 0 ? '+' : ''}${n(p.delta)} → ${n(p.new)}`
|
||
default:
|
||
return ev.kind
|
||
}
|
||
}
|
||
|
||
// Category grouping for the filter tabs.
|
||
// Vendor sales are intentionally NOT a public category — they are owner-private
|
||
// (a linked player sees their own under the portal). The admin live feed still
|
||
// describes vendor.sale via describe() below.
|
||
export const CATEGORIES = [
|
||
{ id: 'all', label: 'All', kinds: null },
|
||
{ id: 'pvp', label: 'Deaths & PvP', kinds: ['player.death', 'player.murdered', 'mob.killed'] },
|
||
{ id: 'progress', label: 'Progression', kinds: ['skill.gain', 'fame.change', 'karma.change', 'quest.complete'] },
|
||
{ id: 'world', label: 'World', kinds: ['house.decay', 'mob.login', 'mob.logout', 'server.hello', 'server.shutdown', 'server.crashed', 'economy.supply'] },
|
||
]
|
||
|
||
const CATEGORY_OF = (() => {
|
||
const m = {}
|
||
for (const c of CATEGORIES) if (c.kinds) for (const k of c.kinds) m[k] = c.id
|
||
return m
|
||
})()
|
||
|
||
export function categoryOf(kind) {
|
||
return CATEGORY_OF[kind] || 'other'
|
||
}
|
||
|
||
// Short badge label for a kind (the part after the dot, title-cased-ish).
|
||
export function kindLabel(kind) {
|
||
return String(kind || '').replace(/[._]/g, ' ')
|
||
}
|