PLAN.md §33, D134-D143. Protocol 12. - Chat titles (D135-D137): per-server rules (stat, top N, text, colour) that rank the current wipe, and a mode (first | all | up to N). Worked out once in model/titles and read three ways: pushed whole to the game by a new titleSync loop (on change, restart or wipe), and on every leaderboard row as `titles`. Admin: PUT /servers/:id/titles. - Group styles (D138, D139): a site group may carry all twelve BetterChat fields (rust_perm_group_chat). They ride perm.sync with `expect` from the pushed ledger, which gains a value column; a field changed in game is a `chat-field` drift row with the game's value, adopted into the style or put back. A withdrawn style is one `chat-group` retirement, never for `default`, cleared from the ledger only once BetterChat removed it. - The voice (D140): one fleet setting naming a styled group; news and rust.announce chat lines carry its format and the plugin says them with no sender. Admin: GET/PUT /voice. - Popups (D141, D142): rust.announce gains `delivery` (still version 1, from rust.options.delivery); each server gains news_delivery beside the news switch; `popup-unavailable` is not retried. - GET /servers/:id/integrations reads, live, which optional mods a server has loaded. README lists BetterChat and PopupNotifications as optional. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
115 lines
4.5 KiB
JavaScript
115 lines
4.5 KiB
JavaScript
// ── 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, newsDelivery, 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, newsDelivery }, 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 }
|