// ── Admin · Rust · Visibility — the handlers ────────────────────────────── const core = require('../../core') const clans = require('../../model/clans/clans.model') const map = require('../../model/map/map.model') const mapDb = require('../../model/map/map.db') const mapImages = require('../../mapImages') const visibility = require('../../model/visibility/visibility.model') const visibilityDb = require('../../model/visibility/visibility.db') const log = core.logger('visibility') /** * The page's whole state: both settings, and each server's clan board beside * the roster setting. The board is where "this server runs the uMod Clans * plugin, whose clans are not Teams" (D47) and "this server is at the game's * 100-clan ceiling" (D55) come from. */ async function describe() { const [settings, boards, mapCard] = await Promise.all([visibility.describe(), clans.boardsForAdmin(), describeMap()]) return { ...settings, clans: { ...settings.clans, servers: boards }, map: mapCard } } /** * The Map card (phase 14, D114): the four layers' fleet defaults and each * server's overrides, and beside them what each server's picture is — where it * came from, which map it is of, when it was fetched — and what a render would * cost that server (D109). On this page for D106's reason: it is the one page * with a row per server, and a layer switch answers the same question the * presence switch does. */ async function describeMap() { const [servers, rows] = await Promise.all([visibilityDb.listServerPresence(), mapDb.listMeta()]) const switches = await map.describeSwitches(servers) const byId = new Map(rows.map((r) => [r.serverId, r])) return { ...switches, servers: switches.servers.map((sw, i) => { const s = servers[i] const row = byId.get(s.id) return { ...sw, name: s.name, enabled: Boolean(s.enabled), picture: row ? { source: row.source, mapKey: row.mapKey, hasPicture: Boolean(row.sha256 && Number(row.byteCount) > 0), bytes: Number(row.byteCount) || 0, width: Number(row.width), height: Number(row.height), worldSize: Number(row.worldSize), fetchedAt: row.fetchedAt ? new Date(row.fetchedAt).toISOString() : null, } : null, renderStallSeconds: map.renderStallSeconds(row ? row.worldSize : null), ...mapImages.statusOf(s.id), } }), } } async function read(req, res) { try { res.json(await describe()) } catch (err) { log.error('failed to read visibility settings', { error: err.message }) res.status(500).json({ message: 'Failed to read the visibility settings' }) } } async function update(req, res) { try { const { fleet, servers, clanRoster, news, map: mapSwitches } = req.body || {} // The map's switches are validated whole FIRST, before the rest is written: // the page saves everything with one PUT, and a refused map switch must not // leave the presence half already applied. let mapResult = null if (mapSwitches !== undefined) { mapResult = await map.update(mapSwitches, req.user, async (id) => (await visibilityDb.getServerPresence(id)) !== undefined, { dryRun: true }) if (!mapResult.ok) { res.status(mapResult.status || 400).json({ message: mapResult.message }) return } } const result = await visibility.update({ fleet, servers, clanRoster, news }, req.user) if (!result.ok) { res.status(result.status || 400).json({ message: result.message }) return } if (mapSwitches !== undefined) { mapResult = await map.update(mapSwitches, req.user, async (id) => (await visibilityDb.getServerPresence(id)) !== undefined) if (mapResult.ok && Object.keys(mapResult.changed).length) result.changed.map = mapResult.changed } // One row per save, naming everything it changed. Widening who may see the // roll call is exactly the kind of change somebody later needs to trace to a // person and a time. await core.activity.log({ req, action: 'rust.visibility.save', detail: result.changed }) res.json(await describe()) } catch (err) { log.error('failed to save visibility settings', { error: err.message }) res.status(500).json({ message: 'Failed to save the visibility settings' }) } } module.exports = { read, update }