import { useMemo, useState } from 'react' import { Loading, ErrorState } from '../../../components/PageState.jsx' import { useAsync } from '../../../lib/useAsync.js' import { useShardFeed } from '../../../lib/useShardFeed.js' import { api } from '../../../api/client.js' // Staff-only FULL house registry (admin + moderator). Owner, price, co-owners and // decay — everything the public board hides. Loaded from /admin/shard/houses, kept // live from the admin SSE channel (house.update / house.remove). const HOUSE_KINDS = new Set(['house.update', 'house.remove', 'house.decay']) const DECAY_TONE = { LikeNew: '#7fd0a4', Ageless: '#7fd0a4', Slightly: '#a9cf8a', Somewhat: '#d7c56a', Fairly: '#e0a95f', Greatly: '#d9736f', IDOC: '#e05a5a', Collapsed: '#8c96a5', } function DecayBadge({ decay, isIdoc }) { const label = isIdoc ? 'IDOC' : decay if (!label) return null const tone = DECAY_TONE[label] || 'var(--muted)' return ( {label} ) } function ownerLabel(h) { return h.ownerName || h.ownerAcct || null } function HouseRow({ h }) { const owner = ownerLabel(h) return (
{h.name || 'An unnamed house'}
{owner ? <>Owned by {owner} : 'No owner'} {(h.coOwners || h.friends) ? ` · ${h.coOwners || 0} co-owners, ${h.friends || 0} friends` : ''}
{h.region || h.map || '—'}{h.x != null ? ` (${h.x}, ${h.y})` : ''}
{h.price != null && (
{Number(h.price).toLocaleString()}
placement value
)}
) } export default function HousesAdmin() { const { loading, error, data } = useAsync(() => api.admin.shard.houses()) // Full registry deltas ride the admin SSE channel (never the public one). const { events, connected } = useShardFeed({ url: api.adminShardStreamUrl, filter: HOUSE_KINDS, max: 80 }) const [q, setQ] = useState('') const board = useMemo(() => { const map = new Map() for (const h of data || []) if (h && h.serial) map.set(h.serial, h) for (let i = events.length - 1; i >= 0; i -= 1) { const ev = events[i] if (!ev.serial) continue if (ev.kind === 'house.update') { map.set(ev.serial, { ...ev, ownerName: ev.owner?.name ?? ev.ownerName, ownerAcct: ev.owner?.acct ?? ev.ownerAcct }) } else if (ev.kind === 'house.remove') { map.delete(ev.serial) } else if (ev.kind === 'house.decay') { const cur = map.get(ev.serial) || { serial: ev.serial, name: ev.name, region: ev.region, map: ev.map, x: ev.x, y: ev.y } map.set(ev.serial, { ...cur, isIdoc: String(ev.to).toUpperCase() === 'IDOC' }) } } return [...map.values()] }, [data, events]) const filtered = useMemo(() => { const needle = q.trim().toLowerCase() const rows = needle ? board.filter((h) => [h.name, h.region, h.map, ownerLabel(h)].some((v) => v && String(v).toLowerCase().includes(needle))) : board return [...rows].sort((a, b) => (a.name || '').localeCompare(b.name || '')) }, [board, q]) if (loading) return if (error) return return (

{board.length.toLocaleString()} houses {connected ? '● live' : '○ offline'}

setQ(e.target.value)} placeholder="Search by owner, region…" style={{ flex: 'none', width: 230, maxWidth: '55%', fontSize: '0.84rem' }} />
{board.length === 0 ? (

No houses are being tracked right now.

) : (
{filtered.map((h) => )}
)} {board.length > 0 && filtered.length === 0 && (

No houses match “{q}”.

)}
) }