// ── Admin · Cliloc table ─────────────────────────────────────────────────── // // Operating the cliloc import: where the converted cliloc file is, whether it // has drifted from what is loaded, and a forced reimport after a client patch // (docs/website/CLILOCS.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.** `shardClilocs.refresh()` reports // `unavailable` / `failed` rather than throwing, because the boot path must never // be stopped by a bad file. That contract is preserved here: a missing file, or // the single most likely operator mistake — pointing at the client's own // COMPRESSED `Cliloc.enu` — is a 200 carrying `status: 'unavailable'` and the // reason, not a 500. A 500 would say only "something broke"; the operator needs // to be told which file to convert. const clilocs = require('../../../model/shardClilocs/shardClilocs.model') const market = require('../../../model/shardMarket/shardMarket.model') const activity = require('../../../model/activity/activity.model') const log = require('../../../utils/logger')('admin-shard-clilocs') // GET /admin/shard/clilocs — what is loaded, what the file looks like, whether // they disagree. There is no public counterpart: the cliloc table is never // served as a table, only applied to names the site already returns. async function getStatus(req, res) { try { return res.json(await clilocs.status()) } catch (err) { log.error('getStatus', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // POST /admin/shard/clilocs/import — reload after a client patch or a change to // the shard's own overlay files, without a restart. // // `force` reimports even when the source hashes match what is loaded (the escape // hatch for "the database is wrong but the files are not"). // // `approve` accepts a refresh in which a previously-loaded source has VANISHED. // That is refused by default because an unmounted volume and a deliberate // deletion look identical from the server — the lighter cousin of the atlas's // approve/reject flow, and the reason it can be a flag here rather than a // pending table is that nothing is stored to approve: the import re-reads the // files at approval time by construction. async function importClilocs(req, res) { try { const force = !!req.body?.force const approve = !!req.body?.approve const result = await clilocs.refresh({ force, approve }) // The marketplace denormalizes resolved item names into // shard_vendor_items.display_name, and the shard's market sweep will NOT // re-send an unchanged shop just because the site learned what its items are // called — so without this pass, an operator who imports clilocs after the // first sweep keeps seeing item ids until every shop happens to change. // Awaited (rather than fired and forgotten) so the panel's "imported" is // honest about the names being live; the pass is a bounded walk of one table // and never throws. if (result.status === 'imported') await market.refreshDisplayNames() await activity.log({ req, action: 'shard.clilocs.import', detail: { force, approve, status: result.status, count: result.count ?? null, missingSources: result.missingSources ?? result.acceptedMissing ?? null, }, }) return res.json(result) } catch (err) { log.error('importClilocs', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // PUT /admin/shard/clilocs/path — point the site at a different cliloc file. // // Persisted as a setting, which wins over the UO_CLIENT_PATH env default so an // operator can move the mount without a redeploy. Blank clears it, which turns // resolution off (boot skips, the loaded table keeps serving) — a legitimate // thing to want, so it is allowed rather than validated away. // // Deliberately does NOT import as a side effect, for the same reason the atlas // path does not: changing where the table reads from and reloading it are // separate decisions. 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 clilocs.setClientPath(value, req.user?.id ?? null) await activity.log({ req, action: 'shard.clilocs.path', detail: { path: value } }) return res.json(await clilocs.status()) } catch (err) { log.error('setClilocPath', err) return res.status(500).json({ message: 'Internal Server Error' }) } } module.exports = { getStatus, importClilocs, setPath }