// ── Admin · Cliloc table ─────────────────────────────────────────────────── // // Operating the cliloc import: which source the table comes from, whether it has // drifted from what is loaded, and a reimport after a client patch // (docs/link/v8.md §9, 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 source. That contract is preserved here, and protocol 8 // widened the set of things it covers: a shard that is down, an asset plane the // operator has switched off, a client with no cliloc file, a client patched // halfway through the import — plus everything the file pipeline could already // report. Each is a 200 carrying `status: 'unavailable'` and a reason naming what // to fix, not a 500 that says only "something broke". // // **Import matters more than it used to.** On the bridge, boot deliberately does // not call the shard, so this endpoint is the only thing that refreshes the // table — the operator presses it after patching their client. const clilocs = require('../../model/shardClilocs/shardClilocs.model') const market = require('../../model/shardMarket/shardMarket.model') const { activity } = require('../../core') const log = require('../../core').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, // Which pipeline actually ran. Worth having in the audit log for the // same reason it is in the status: an operator debugging a stale table // needs to know whether the site asked the shard or read a file. source: result.source ?? null, 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 path. // // On an install with uo-link configured this selects where `custom/` OVERLAYS are // read from; the base table comes from the shard either way. Without a shard link // it is also where the converted base file is looked for. // // 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 // overlay resolution off (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 }