import { useCallback, useEffect, useState } from 'react'
import api from '../../api.js'
import { ErrorState, Loading } from '../../core.js'
import Row from '../../components/DetailRow.jsx'
// ── Admin · Spawn atlas ─────────────────────────────────────────────────────
//
// The atlas re-derives itself from the shard's ServUO tree on every boot, so
// this panel exists for the three things a restart cannot do:
//
// • point it at a different tree,
// • apply a map change without restarting, and
// • answer a refresh that was parsed but deliberately NOT applied because it
// would remove a facet.
//
// That last one is the reason the panel is worth building. Losing a facet looks
// exactly like a half-copied or mid-update tree, and boot cannot tell them
// apart — so it stages the decision for a human instead of guessing. Until
// someone decides here, the site keeps serving the atlas it already had.
// A refresh reports its outcome rather than throwing (the boot path must never
// be stopped by a bad tree), so these are answers, not errors — the panel says
// what happened in the shard's terms instead of showing a failure box.
const OUTCOME = {
imported: (r) =>
`Imported — ${r.counts?.points?.toLocaleString() ?? '?'} spawners, ${r.counts?.creatures?.toLocaleString() ?? '?'} creatures.`,
unchanged: (r) =>
r.reason === 'refresh previously rejected'
? 'Unchanged — this exact tree was already reviewed and declined.'
: 'Unchanged — the tree matches what is already loaded.',
needsReview: () => 'Staged for review: this refresh would remove a facet, so it was not applied.',
unavailable: (r) => `The tree could not be read: ${r.reason || 'unknown reason'}`,
skipped: () => 'No ServUO path is configured, so there is nothing to import.',
failed: (r) => `Refresh failed: ${r.reason || 'unknown reason'}`,
rejected: () => 'Declined. It will not be offered again until the tree changes.',
}
const describe = (result) => (OUTCOME[result?.status] || (() => `Result: ${result?.status}`))(result)
function PendingReview({ pending, busy, onApprove, onReject }) {
const declined = pending.status === 'rejected'
return (
{declined ? (
<>
This tree was reviewed and declined, so it is not offered again until the files change.
Approving now applies it anyway.
>
) : (
<>
The tree parses cleanly but would remove {pending.removedFacets?.length || 0} facet
{(pending.removedFacets?.length || 0) === 1 ? '' : 's'} the site is currently serving. That
is what a half-copied or mid-update tree looks like as well as a real map change, so it was
not applied. Approving re-parses the tree as it is right now — if you have since fixed the
mount, what lands is the corrected import.
>
)}
{declined ? 'A refresh was declined' : 'A refresh is waiting for you'}
The bestiary and spawn map on the public site, parsed from the shard’s own ServUO files. Where those files come from depends on whether a shard is linked: with uo-link configured the shard serves them over the bridge and importing is something you do here, when a map changes. Without one, the site reads a local tree and re-imports itself on every server start. Either way the atlas is shard content rather than shard state, so what is loaded keeps serving in full while the shard is down.
A local ServUO tree the website can read directly — the same host, a bind mount or a
shared volume. This setting wins over the SERVUO_PATH deploy default, so the
mount can move without a redeploy.
{status?.source === 'bridge'
? ' It is not in use right now: this site has a shard linked, and the shard serves its' +
' own files over the bridge. Unlink or disable uo-link to fall back to a path.'
: ' Leave it blank to turn the atlas off.'}
Applies a map change without restarting — and on a linked shard it is the only thing that does, because boot deliberately never calls the shard for this. An unchanged source costs almost nothing: the file list and its hashes are read first (about 32 KB over the bridge) and no file is transferred when they match. A refresh that would remove a facet still comes back here for approval rather than being applied.