The 35 files behind twelve public pages, seven admin views, two player views and three core-page extensions, ported onto `window.__rg`. Every one of them imports exactly the seven kit members plus `lib/format.js`, which is the finding §2.7.1 predicted and this confirms. `client/src/core.js` is the port mechanism, and unlike the server's it is a plain read: `window.__rg` is published before any module chunk evaluates, so there is no gap to defer around and a ported component keeps its ordinary import shape. `client/src/api.js` rebuilds the UO namespaces over the request primitive — same URLs, because §1.2 freezes the API surface. SPA paths changed and API paths did not. `/site/shard` is `/uo/shard`, and the admin paths lost their now-redundant `shard-` prefixes (`/admin/uo/ops`), a clean break being the only moment that is free. `shim/rg.js` becomes the single reader of the global, so the "core did not publish its dependencies" message is reachable from whichever module the bundler happens to touch first rather than from whichever one is imported first — a guarantee that used to last until someone sorted the imports. Co-Authored-By: Claude <noreply@anthropic.com>
79 lines
3.4 KiB
JavaScript
79 lines
3.4 KiB
JavaScript
import { useMemo, useState } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import { useShardFeed } from '../../lib/useShardFeed.js'
|
|
import { describe, categoryOf, kindLabel, CATEGORIES } from '../../lib/shardEvents.js'
|
|
import { ago } from '../../lib/format.js'
|
|
import api from '../../api.js'
|
|
import { ErrorState, Loading, PageHeader, PublicLayout, useAsync } from '../../core.js'
|
|
|
|
// Public activity feed: the full shard event log, filterable by category, with a
|
|
// live tail that prepends new events as they happen.
|
|
export default function ShardActivity() {
|
|
const { loading, error, data } = useAsync(() => api.shard.feed({ limit: 150 }))
|
|
const { events: live } = useShardFeed({ max: 60 })
|
|
const [cat, setCat] = useState('all')
|
|
|
|
// Merge the live tail with the loaded history, de-duped by kind+t, newest first.
|
|
const merged = useMemo(() => {
|
|
const seen = new Set()
|
|
const out = []
|
|
for (const e of [...live, ...(data || [])]) {
|
|
const key = `${e.kind}-${e.t}`
|
|
if (seen.has(key)) continue
|
|
seen.add(key)
|
|
out.push(e)
|
|
}
|
|
return out.sort((a, b) => (b.t || 0) - (a.t || 0))
|
|
}, [live, data])
|
|
|
|
const filtered = cat === 'all' ? merged : merged.filter((e) => categoryOf(e.kind) === cat)
|
|
|
|
return (
|
|
<PublicLayout section="website">
|
|
<div className="shell-narrow page-body">
|
|
<PageHeader eyebrow="Live" title="Shard Activity" />
|
|
<p style={{ marginTop: -8, marginBottom: 18 }}>
|
|
<Link to="/uo/shard" className="sans" style={{ color: 'var(--accent)', textDecoration: 'none', fontSize: '0.86rem' }}>← Back to shard</Link>
|
|
</p>
|
|
|
|
{/* Category tabs */}
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 18 }}>
|
|
{CATEGORIES.map((c) => (
|
|
<button
|
|
key={c.id}
|
|
onClick={() => setCat(c.id)}
|
|
className="pill"
|
|
style={cat === c.id ? { background: 'var(--accent)', color: 'var(--bg-deep)', borderColor: 'var(--accent)' } : undefined}
|
|
>
|
|
{c.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{loading && <Loading />}
|
|
{error && <ErrorState message="Could not load the activity feed right now." />}
|
|
|
|
{!loading && !error && (
|
|
filtered.length === 0 ? (
|
|
<div className="panel" style={{ padding: 22 }}>
|
|
<p className="sans dim" style={{ margin: 0, fontSize: '0.9rem' }}>Nothing here yet — events will appear as they happen in the world.</p>
|
|
</div>
|
|
) : (
|
|
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
{filtered.map((e) => (
|
|
<li key={e._id || `${e.kind}-${e.t}`} className="panel" style={{ padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
<span className="sans" style={{ flex: 'none', fontSize: '0.62rem', letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--accent)', minWidth: 92 }}>
|
|
{kindLabel(e.kind)}
|
|
</span>
|
|
<span className="sans" style={{ flex: 1, minWidth: 0, color: 'var(--ink)', fontSize: '0.92rem' }}>{describe(e)}</span>
|
|
<span className="sans dim" style={{ flex: 'none', fontSize: '0.76rem' }}>{ago(e.t)}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)
|
|
)}
|
|
</div>
|
|
</PublicLayout>
|
|
)
|
|
}
|