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
55 lines
2.1 KiB
JavaScript
55 lines
2.1 KiB
JavaScript
import { useEffect, useRef, useState } from 'react'
|
|
import { api } from '../api/client.js'
|
|
|
|
// Subscribe to the public shard live-event SSE stream and keep a rolling buffer
|
|
// of the most recent events. The browser talks to our own /public/shard/stream
|
|
// route (plain HTTP EventSource) — never the sidecar's WebSocket — so the token
|
|
// stays server-side and it works through any reverse proxy.
|
|
//
|
|
// EventSource auto-reconnects on drop, so there is no manual retry loop here; a
|
|
// `connected` flag is exposed for a small live/offline indicator. `filter` (a
|
|
// Set of kinds, optional) limits which events are buffered. `max` caps the
|
|
// buffer length.
|
|
export function useShardFeed({ url, filter, max = 40 } = {}) {
|
|
const [events, setEvents] = useState([])
|
|
const [connected, setConnected] = useState(false)
|
|
// Keep the latest filter in a ref so re-renders don't tear down the stream.
|
|
const filterRef = useRef(filter)
|
|
filterRef.current = filter
|
|
const streamUrl = url || api.shardStreamUrl
|
|
|
|
useEffect(() => {
|
|
// EventSource isn't available during SSR / very old browsers — degrade to
|
|
// "no live feed" rather than throwing.
|
|
if (typeof window === 'undefined' || typeof window.EventSource === 'undefined') return undefined
|
|
|
|
const es = new EventSource(streamUrl, { withCredentials: true })
|
|
|
|
es.onopen = () => setConnected(true)
|
|
es.onerror = () => setConnected(false) // EventSource will retry on its own
|
|
|
|
es.onmessage = (msg) => {
|
|
let event
|
|
try {
|
|
event = JSON.parse(msg.data)
|
|
} catch {
|
|
return
|
|
}
|
|
if (!event || !event.kind) return
|
|
const f = filterRef.current
|
|
if (f && !f.has(event.kind)) return
|
|
setEvents((prev) => {
|
|
// Tag with a stable-ish local id for React keys (events carry t but can
|
|
// collide within a ms) and cap the buffer.
|
|
const next = [{ ...event, _id: `${event.kind}-${event.t}-${prev.length}` }, ...prev]
|
|
return next.slice(0, max)
|
|
})
|
|
}
|
|
|
|
return () => es.close()
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [max, streamUrl])
|
|
|
|
return { events, connected }
|
|
}
|