feat(rust): zone presets, and the zone step's options, messages and dome (PLAN_REDESIGNS §3, D210)
Admin → Rust zone presets: named sets of ZoneManager flags and settings for one server, several, or every server (D195), ticked in groups read from each server's own ZoneManager list in its last hello (D211). A flag not on a covered server is refused on save with the server named; two presets of one name may not share a server. rust.zone.open gains options (one line, NoBuild, radiation=10), enterMessage, leaveMessage, delivery, dome and domeStack. The options field's dropdown is rust.options.zone_presets, whose row VALUE is the preset's line, so picking one copies it into the step (D210) and no published event changes when a preset does. The line and the dome are checked against the server's hello on save and in a dry run; bad-option and dome-unavailable are permanent. Schema: rust_zone_presets, rust_zone_preset_servers. Swagger fragment and routes.manifest regenerated against the pinned core f0e7d2a. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
137
server/model/zones/zones.model.js
Normal file
137
server/model/zones/zones.model.js
Normal file
@@ -0,0 +1,137 @@
|
||||
// ── Zone presets (PLAN_REDESIGNS §3.1, D195, D210) ──────────────────────────
|
||||
//
|
||||
// An admin's own named set of ZoneManager flags and settings, for one server,
|
||||
// several, or the fleet. A zone step offers the presets as the options field's
|
||||
// dropdown; picking one COPIES its line into the step (D210), so a preset edited
|
||||
// later changes no published event. That is also why a preset needs no history
|
||||
// and deleting one is harmless.
|
||||
|
||||
const db = require('./zones.db')
|
||||
const options = require('./zoneOptions')
|
||||
|
||||
const NAME_MAX = 64
|
||||
|
||||
class ZonePresetError extends Error {
|
||||
constructor(message, status = 400) {
|
||||
super(message)
|
||||
this.status = status
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The page's whole state: each server with what it said about its zone mods,
|
||||
* the flags the form draws (every server's, grouped), the settings, and the
|
||||
* presets.
|
||||
*/
|
||||
async function describe() {
|
||||
const [servers, presets] = await Promise.all([db.listZoneServers(), db.listPresets()])
|
||||
const union = new Map()
|
||||
for (const s of servers) {
|
||||
for (const f of (s.zoneManager && s.zoneManager.flags) || []) {
|
||||
if (!union.has(f.toLowerCase())) union.set(f.toLowerCase(), f)
|
||||
}
|
||||
}
|
||||
return {
|
||||
servers: servers.map((s) => ({
|
||||
...s,
|
||||
zoneManager: s.zoneManager ? { version: s.zoneManager.version, flagCount: (s.zoneManager.flags || []).length, flags: s.zoneManager.flags } : null,
|
||||
})),
|
||||
groups: options.grouped([...union.values()]),
|
||||
settings: Object.entries(options.SETTINGS).map(([key, spec]) => ({ key, ...spec })),
|
||||
presets: presets.map((p) => ({ ...p, line: options.format(p) })),
|
||||
}
|
||||
}
|
||||
|
||||
/** Does a preset cover this server? */
|
||||
function covers(preset, serverId) {
|
||||
return preset.allServers || preset.servers.includes(serverId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a preset against every server it covers. A flag must be on each covered
|
||||
* server that has said which flags it has; a server that has not said (never
|
||||
* reached, no ZoneManager yet) is skipped, and the plugin checks the flag when a
|
||||
* step runs there.
|
||||
*/
|
||||
async function validate(input, id = null) {
|
||||
const name = String((input && input.name) || '').trim()
|
||||
if (!name || name.length > NAME_MAX) throw new ZonePresetError(`a preset's name is 1 to ${NAME_MAX} characters`)
|
||||
|
||||
const allServers = input.allServers === true
|
||||
const requested = Array.isArray(input.servers) ? [...new Set(input.servers.map(String))] : []
|
||||
if (!allServers && requested.length === 0) throw new ZonePresetError('a preset is for at least one server, or for every server')
|
||||
|
||||
const servers = await db.listZoneServers()
|
||||
const byId = new Map(servers.map((s) => [s.id, s]))
|
||||
for (const s of requested) {
|
||||
if (!byId.has(s)) throw new ZonePresetError(`no server "${s}"`, 404)
|
||||
}
|
||||
|
||||
const checked = options.check({ flags: input.flags || [], settings: input.settings || {} })
|
||||
if (!checked.ok) throw new ZonePresetError(checked.error)
|
||||
|
||||
const covered = allServers ? servers : requested.map((s) => byId.get(s))
|
||||
let flags = checked.flags
|
||||
for (const s of covered) {
|
||||
const known = s.zoneManager && s.zoneManager.flags
|
||||
if (!known || known.length === 0) continue
|
||||
const onServer = options.check({ flags }, known)
|
||||
if (!onServer.ok) throw new ZonePresetError(`${onServer.error.replace("this server's", `${s.name || s.id}'s`)}`)
|
||||
flags = onServer.flags
|
||||
}
|
||||
|
||||
// Two presets of one name on one server would be two rows in the step's
|
||||
// dropdown nobody could tell apart. Across servers it is allowed (D189).
|
||||
const mine = { allServers, servers: requested }
|
||||
const others = (await db.listPresets()).filter((p) => p.id !== id && p.name.toLowerCase() === name.toLowerCase())
|
||||
for (const other of others) {
|
||||
if (allServers && other.allServers) throw new ZonePresetError(`a preset called "${name}" is already on every server`, 409)
|
||||
const shared = servers.find((s) => covers(mine, s.id) && covers(other, s.id))
|
||||
if (shared) throw new ZonePresetError(`a preset called "${name}" is already on ${shared.name || shared.id}`, 409)
|
||||
}
|
||||
|
||||
return { name, flags, settings: checked.settings, allServers, servers: allServers ? [] : requested }
|
||||
}
|
||||
|
||||
async function create(input, userId = null) {
|
||||
const clean = await validate(input)
|
||||
const id = await db.savePreset(clean, userId)
|
||||
return db.getPreset(id)
|
||||
}
|
||||
|
||||
async function update(id, input) {
|
||||
const existing = await db.getPreset(id)
|
||||
if (!existing) throw new ZonePresetError('no such preset', 404)
|
||||
const clean = await validate(input, existing.id)
|
||||
await db.savePreset({ ...clean, id: existing.id })
|
||||
return db.getPreset(existing.id)
|
||||
}
|
||||
|
||||
async function remove(id) {
|
||||
const existing = await db.getPreset(id)
|
||||
if (!existing) throw new ZonePresetError('no such preset', 404)
|
||||
await db.deletePreset(existing.id)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* The zone step's dropdown (D210): one row per preset, whose VALUE is the line
|
||||
* of text the step keeps. Grouped by the servers each is for, so an admin
|
||||
* authoring a step for one server can see which presets were made for it.
|
||||
*/
|
||||
async function optionRows() {
|
||||
const [presets, servers] = await Promise.all([db.listPresets(), db.listZoneServers()])
|
||||
const nameOf = new Map(servers.map((s) => [s.id, s.name || s.id]))
|
||||
return presets.map((p) => ({
|
||||
value: options.format(p),
|
||||
label: p.name,
|
||||
group: p.allServers ? 'Every server' : p.servers.map((s) => nameOf.get(s) || s).join(', '),
|
||||
}))
|
||||
}
|
||||
|
||||
/** What one server's last hello said about ZoneManager and ZoneDomes, or null. */
|
||||
async function serverZoneMods(serverId) {
|
||||
return (await db.listZoneServers()).find((s) => s.id === serverId) || null
|
||||
}
|
||||
|
||||
module.exports = { ZonePresetError, describe, create, update, remove, optionRows, serverZoneMods, covers }
|
||||
Reference in New Issue
Block a user