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 === 0 ? (
No houses are being tracked right now.
) : (
{filtered.map((h) => )}
)}
{board.length > 0 && filtered.length === 0 && (
No houses match “{q}”.
)}
)
}