Add shard activity feed + admin live feed; fix public-feed leak
Front ends for the rest of the sidecar data, plus a security fix the live data surfaced. - lib/shardEvents.js: shared describe()/category/label for every event kind (sales, deaths & PvP, skills, fame/karma, quests, world, and staff kinds). - Public /site/shard/activity (ShardActivity): the full event log with category filter tabs and a live tail (history + SSE merged, de-duped). Linked from the Shard page. Shard page now reuses the shared describe(). - Admin: a "Live feed (all events)" panel on the Shard admin page subscribing to the admin SSE channel — shows every kind incl. audit/cheat/login attempts. useShardFeed generalized to take a stream url; api.adminShardStreamUrl added. Security fix: GET /public/shard/feed now restricts to the public-safe kind allowlist (shardEvents.list gains a `kinds` IN-filter). Previously it returned whatever was logged — including audit.* / cheat.* / link.request. Those are still stored for the admin channel but never served publicly (verified: a public request for audit.command returns 0 rows). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011qPmpmVH1xGCiZoz9m9vW3
This commit is contained in:
@@ -4,6 +4,7 @@ import PageHeader from '../../components/PageHeader.jsx'
|
||||
import { Loading, ErrorState } from '../../components/PageState.jsx'
|
||||
import { useAsync } from '../../lib/useAsync.js'
|
||||
import { useShardFeed } from '../../lib/useShardFeed.js'
|
||||
import { describe } from '../../lib/shardEvents.js'
|
||||
import { ago } from '../../lib/format.js'
|
||||
import { api } from '../../api/client.js'
|
||||
|
||||
@@ -42,38 +43,6 @@ function Stat({ value, label }) {
|
||||
)
|
||||
}
|
||||
|
||||
// A one-line human description of a feed event.
|
||||
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 ${Number(p.price || 0).toLocaleString()}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 'house.decay':
|
||||
return `${p.name || 'A house'} is now ${p.to || p.stage}`
|
||||
case 'quest.complete':
|
||||
return `${nameOf(p.who)} completed “${p.quest}”`
|
||||
case 'skill.gain':
|
||||
return `${nameOf(p.who)} gained ${p.skill}`
|
||||
case 'mob.login':
|
||||
return `${nameOf(p.who)} entered the world`
|
||||
case 'mob.logout':
|
||||
return `${nameOf(p.who)} left the world`
|
||||
default:
|
||||
return ev.kind
|
||||
}
|
||||
}
|
||||
function nameOf(who) {
|
||||
if (!who) return 'Someone'
|
||||
if (typeof who === 'string') return who
|
||||
return who.name || who.acct || 'Someone'
|
||||
}
|
||||
|
||||
export default function Shard() {
|
||||
const { loading, error, data } = useAsync(() =>
|
||||
Promise.all([
|
||||
@@ -206,10 +175,15 @@ export default function Shard() {
|
||||
<div className="sans" style={{ color: 'var(--accent)', fontSize: '0.7rem', letterSpacing: '0.12em', textTransform: 'uppercase' }}>
|
||||
Live feed
|
||||
</div>
|
||||
<span className="sans" style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: '0.74rem', color: connected ? '#7fd0a4' : 'var(--muted)' }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: connected ? '#7fd0a4' : 'var(--dim)' }} />
|
||||
{connected ? 'Live' : 'Offline'}
|
||||
</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<Link to="/site/shard/activity" className="sans" style={{ color: 'var(--accent)', textDecoration: 'none', fontSize: '0.78rem' }}>
|
||||
View all activity →
|
||||
</Link>
|
||||
<span className="sans" style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: '0.74rem', color: connected ? '#7fd0a4' : 'var(--muted)' }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: connected ? '#7fd0a4' : 'var(--dim)' }} />
|
||||
{connected ? 'Live' : 'Offline'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{events.length === 0 ? (
|
||||
<p className="sans dim" style={{ margin: 0, fontSize: '0.88rem' }}>
|
||||
|
||||
Reference in New Issue
Block a user