// ── Admin · Spawn atlas ──────────────────────────────────────────────────── // // Operating the atlas import: where the ServUO tree is, whether it has drifted // from what is loaded, and the approve/reject decision for a refresh that would // remove a facet (docs/website/SPAWN_ATLAS.md). // // The policy lives in the model. This controller does three things and no more: // it validates input, it maps a refresh RESULT onto an HTTP status, and it // records the action in the admin activity log. // // **A refresh result is not an exception.** `shardAtlas.refresh()` reports // `unavailable` / `failed` / `needsReview` rather than throwing, because the boot // path must never be stopped by a bad tree. That contract is preserved here: an // unreadable mount is a 200 carrying `status: 'unavailable'`, not a 500. The // admin needs to be told what is wrong with their path, and a 500 says only // "something broke". const atlas = require('../../model/shardAtlas/shardAtlas.model') const { activity } = require('../../core') const log = require('../../core').logger('admin-shard-atlas') // GET /admin/shard/atlas — what is loaded, what the tree looks like, what is // staged. Unlike the public /atlas/meta route this DOES carry the filesystem // path and the drift flag: that is the whole point of the panel. async function getStatus(req, res) { try { return res.json(await atlas.status()) } catch (err) { log.error('getStatus', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // POST /admin/shard/atlas/import — apply a map change without a restart. // // `force` reimports even when the source hashes match what is loaded (the escape // hatch for "the database is wrong but the tree is not"). Facet loss is still // staged rather than applied — approving is a separate, explicit act. async function importAtlas(req, res) { try { const force = !!req.body?.force const result = await atlas.refresh({ force }) await activity.log({ req, action: 'shard.atlas.import', detail: { force, status: result.status, counts: result.counts ?? null }, }) return res.json(result) } catch (err) { log.error('importAtlas', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // POST /admin/shard/atlas/approve — apply a staged refresh, facet loss and all. // // Re-parses the tree rather than applying something captured at boot: only the // DECISION was stored, so what lands matches the tree as it is now. If the // operator has since fixed a half-copied mount, the approved import is simply // the corrected one — which is the desired outcome, not a surprise. async function approve(req, res) { try { const result = await atlas.approvePending() await activity.log({ req, action: 'shard.atlas.approve', detail: { status: result.status, removed: result.removedFacets ?? null }, }) return res.json(result) } catch (err) { log.error('approveAtlas', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // POST /admin/shard/atlas/reject — keep the current atlas and remember the // decision against those exact source hashes, so a declined refresh does not // re-prompt on every restart. Changing the tree asks again. async function reject(req, res) { try { const result = await atlas.rejectPending() if (result.status === 'none') { return res.status(404).json({ message: 'No refresh is awaiting review.' }) } await activity.log({ req, action: 'shard.atlas.reject', detail: {} }) return res.json(result) } catch (err) { log.error('rejectAtlas', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // PUT /admin/shard/atlas/path — point the atlas at a different ServUO tree. // // Persisted as a setting, which wins over the SERVUO_PATH env default so an // operator can move the mount without a redeploy. Blank clears it, which turns // the atlas off (boot skips, the loaded atlas keeps serving) — that is a // legitimate thing to want, so it is allowed rather than validated away. // // Deliberately does NOT import as a side effect: changing where the atlas reads // from and reloading it are separate decisions, and an operator fixing a typo // should not have a multi-thousand-row replace happen under them. The response // carries the refreshed status so the panel can offer the import immediately. async function setPath(req, res) { try { const value = String(req.body?.path ?? '').trim() await atlas.setServuoPath(value, req.user?.id ?? null) await activity.log({ req, action: 'shard.atlas.path', detail: { path: value } }) return res.json(await atlas.status()) } catch (err) { log.error('setAtlasPath', err) return res.status(500).json({ message: 'Internal Server Error' }) } } module.exports = { getStatus, importAtlas, approve, reject, setPath }