// ── 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 }