{"service":"servermander-api","namespaces":["dayz","homelab"],"endpoints":[{"method":"GET","path":"/"},{"method":"POST","path":"/dayz/:action"},{"method":"POST","path":"/dayz/:group/:action"},{"method":"GET","path":"/dayz/actions"},{"method":"GET","path":"/dayz/server-info"},{"method":"POST","path":"/dayz/sources/vpp/:token"},{"method":"GET","path":"/healthz"},{"method":"POST","path":"/homelab"},{"method":"POST","path":"/homelab/latest"},{"method":"POST","path":"/keys/create"},{"method":"POST","path":"/keys/list"},{"method":"POST","path":"/keys/revoke"},{"method":"POST","path":"/keys/update"},{"method":"GET","path":"/metrics"},{"method":"GET","path":"/openapi.json"},{"method":"POST","path":"/sysload"},{"method":"POST","path":"/whoami"}],"actions":{"dayz":{"restart":{"destructive":true,"readOnly":false,"describe":"restart the DayZ server"},"stop":{"destructive":true,"readOnly":false,"describe":"stop the DayZ server"},"start":{"destructive":false,"readOnly":false,"describe":"start the DayZ server"},"status":{"destructive":false,"readOnly":true,"describe":"server info: state, uptime, players (count + roster), map, mod list, next scheduled restart, and the unit footprint (memory, threads, log + persistence sizes, restarts). ONE call covers the whole server picture — it makes exactly one unit snapshot and one RCon query, so a dashboard needs neither a second players read nor the dayz half of /sysload."},"timeseries":{"destructive":false,"readOnly":true,"describe":"historical series for the charts: { \"metrics\": [\"players_online\", ...], \"hours\": 6|12|24 }. Keys come from the server-owned allowlist (server_fps, players_online, host_load, dayz_memory, host_mem_avail, dayz_threads, persistence_size, log_dir_size, unique_players_24h, server_up) — PromQL is never accepted. Each metric comes back with its points AND its latest value, so a caller needs no second request. Read-only, loopback Prometheus, briefly cached."},"players":{"destructive":false,"readOnly":true,"describe":"current online players: count + roster (num, name, guid, ping, ip, lobby) via RCon"},"mapchange":{"destructive":true,"readOnly":false,"describe":"switch the active mission and restart (body: { \"mission\": \"dayzOffline.enoch\" })"},"missions":{"destructive":false,"readOnly":true,"describe":"installed missions (folder names under mpmissions/) — the candidates a \"mapchange\" can switch to"},"broadcast":{"destructive":false,"readOnly":false,"describe":"send an in-game message to all players (body: { \"message\": \"...\" })"},"update":{"destructive":false,"readOnly":false,"describe":"queue a server update for the next restart — arms it; the next start (scheduled, manual, or forced) pulls the latest server build + mods. Does NOT restart now. Body: { reason? }"},"update/status":{"destructive":false,"readOnly":true,"describe":"update status: installed vs latest build, whether an update is available or already queued, and the last applied update outcome (with its log tail)"},"update/cancel":{"destructive":false,"readOnly":false,"describe":"cancel a queued update (clears the pending flag) — no-op if none is queued"},"positions":{"destructive":false,"readOnly":true,"describe":"live player map positions, ANONYMIZED to [{x,z}] only — from the CustomServerMods LiveTracker serverMod (profiles/LiveTracker/players.json, rewritten every 20s from the live player roster, replacing the old minutes-lagged .ADM scrape). stale (>60s = server/mod down) or missing returns none rather than a frozen snapshot."},"bandits":{"destructive":false,"readOnly":true,"describe":"live AI positions [{x,z,type,age}] from the CustomServerMods LiveTracker serverMod (profiles/LiveTracker/ai.json, rewritten every 20s). type = \"eai\" (ExpansionAI) | \"bandit\" (AI Bandits, only when @aibandits is loaded — retired); age = seconds that NPC has been alive this session (game clock, resets on restart). ageSec = seconds since the last FILE write; stale = older than the 60s freshness window (3 missed writes = server/mod down). stale and missing both return NO positions, so the map never plots a frozen snapshot."},"ship":{"destructive":false,"readOnly":true,"describe":"the patrol ship position [{x,z,state,target}] from the FlyingDutchman serverMod (profiles/FlyingDutchman/ship.json, rewritten every ~20s; [] while no ship is alive). state = \"patrol\" | \"docked\" (town stop) | \"halted\" (holding for nearby players); target = the waypoint label it is heading to. Same freshness contract as the other live layers. missing = the mod is not enabled — it ships DORMANT, so missing is the normal state until the ship goes live."},"world-time":{"destructive":false,"readOnly":true,"describe":"the in-game world clock {year,month,day,hour,minute} from the CustomServerMods LiveTracker serverMod (profiles/LiveTracker/time.json, rewritten every 20s). ageSec = seconds since the last write; stale (>60s) flags a dead server/mod and nulls the fields; missing = mod not loaded yet (fresh boot). hour is the number that drives the day/night cycle (serverTimeAcceleration)."},"logs/sources":{"destructive":false,"readOnly":true,"describe":"list the browsable log sources (server rpt/adm + each mod that keeps its own logs) — ids feed the \"source\" param of the \"logs/files\" and \"logs/read\" actions. Add a source in deploy.config.json Dayz.LogSources."},"logs/files":{"destructive":false,"readOnly":true,"describe":"list one log source's files, newest first — names feed the \"logs/read\" action. Params: source (an id from \"logs/sources\"; default rpt)"},"logs/read":{"destructive":false,"readOnly":true,"describe":"read a slice of one log: range, filter, scroll. Params: source (an id from \"logs/sources\"; default rpt) picks which log family; file (a name from \"logs/files\") or omit to read that source's newest file; offset = 1-based line to start at (omit for the tail); limit 1-500 (default 100); filter = grep -E regex matched per line; ignoreCase; raw = include the known-noise lines the box-side pre-filter hides by default (engine spam like Sakhal's \"Unknown object class\" — pattern set in deploy.config.json Dayz.LogNoiseFilter; noiseHidden reports how many were dropped). With a filter, offset/limit page through the MATCHED lines; every returned line keeps its original line number (n), and nextOffset/prevOffset are ready-made cursors for scrolling."},"configs/list":{"destructive":false,"readOnly":true,"describe":"list the config files available to retrieve (names for the \"configs/get\" action)"},"configs/get":{"destructive":false,"readOnly":true,"describe":"retrieve one allowlisted config file (params: { \"name\": \"serverSettings\" }; see the \"configs/list\" action for names)"},"docs/list":{"destructive":false,"readOnly":true,"describe":"list documentation files bundled in the @mod folders (paths for the \"docs/get\" action)"},"docs/get":{"destructive":false,"readOnly":true,"describe":"retrieve one mod-doc file by relpath (params: { \"name\": \"@aibunleashed/readme.txt\" }; see the \"docs/list\" action)"},"configs/writable":{"destructive":false,"readOnly":true,"describe":"list the box-owned files an admin may replace whole via configs/set-own (ban/allow lists)"},"configs/readonly":{"destructive":false,"readOnly":true,"describe":"list the generated (compiler-output) config globs the web editor must render read-only; own-write refuses to target any of them"},"configs/disabled":{"destructive":false,"readOnly":true,"describe":"list config surface relpaths whose owning mod is disabled in mods.conf; the web editor drops these rows so a turned-off mod stops surfacing its config files (the box-side files stay intact)"},"configs/owned":{"destructive":false,"readOnly":true,"describe":"the owned-surface masks (registry category:'owned'): files = exact relpaths, dirs = folders whose json/xml files are owned. The web editor routes matching rows to the whole-file two-copy editor (configs/own + configs/set-own) instead of a raw text edit. edited = owned files that have a captured .defaults baseline beside them, i.e. saved through the editor at least once — the tree marks those; it does NOT mean the content differs from the baseline today."},"configs/own":{"destructive":false,"readOnly":true,"describe":"one category-'owned' config file raw, plus its version hash — the generic whole-file read of the two-copy model (Phase 1). path = a ServerDir-relative file under an owned registry surface (json/xml only). Pass the version back to configs/set-own as baseVersion so a concurrent admin edit is rejected (409)."},"configs/set-own":{"destructive":false,"readOnly":false,"describe":"replace one category-'owned' config file whole — the generic whole-file write of the two-copy model; the bespoke types/settings writers migrate onto it. The box validates by extension (JSON parse / well-formed XML), refuses generated + disabled-mod + non-owned paths, snapshots the outgoing version (.own-versions/, keep 30), and writes atomically. Pass baseVersion (from configs/own) for optimistic concurrency (409 on conflict)."},"terrain/heightmaps":{"destructive":false,"readOnly":true,"describe":"list the maps with baked terrain heightmaps (inputs for the terrain/surface-y action)"},"terrain/surface-y":{"destructive":false,"readOnly":true,"describe":"resolve terrain height Y at world X/Z from the baked heightmap (single: map, x, z — or bulk: body { map, points: [{x,z},…] }; see \"terrain/heightmaps\" for maps)"},"quests/types":{"destructive":false,"readOnly":true,"describe":"one row per KIND of quest - counts, how many are switched off, chain count, reward band and chain-rule findings - for the quest catalogue landing view. Reads Quests/ ONLY (about 124 files rather than 282): a quest file declares its own objectives' types, so grouping and chaining need nothing else. The rules that compare a quest against an objective or an NPC cannot run here and are not reported; ask quests/list for those. mission defaults to the running one."},"quests/list":{"destructive":false,"readOnly":true,"describe":"the quests of ONE type, as toggle units. A chain is one unit holding every quest in it, because Expansion reads a follow-up's givers before null-checking it and half a chain throws on the parent's turn-in. Each unit carries its quests, its live state, its findings, and what switching it off would strand. Reads the whole staging tree, which is what the cross-file rules need."},"quests/template":{"destructive":false,"readOnly":true,"describe":"a ready-to-edit document for a NEW quest, objective or NPC, with the next free ID and the path it belongs at. The field set comes from the working files - the mod drops a document whose shape it does not recognise and says one line about it, so nothing composes one from scratch. Save it with baseVersion \"new\" to create it."},"quests/npcs":{"destructive":false,"readOnly":true,"describe":"every quest NPC as id/name/file/active, so a quest can be pointed at a giver or a turn-in by NAME rather than by remembering a number. QuestGiverIDs and QuestTurnInIDs hold IDs; this is what turns them back into people."},"quests/classnames":{"destructive":false,"readOnly":true,"describe":"every item classname the server knows, merged from its CE types documents. Feeds the quest editor's suggestion list and the unknown-classname warning. A quest naming an item that does not exist fails silently in game - the objective never completes and nothing in the log says why."},"quests/resource":{"destructive":false,"readOnly":true,"describe":"one quest, objective or NPC file raw, plus its version hash. file = the relpath the graph recorded (e.g. Quests/Quest_141.json); pass version back to quests/save as baseVersion so a concurrent edit is rejected."},"quests/save":{"destructive":false,"readOnly":false,"describe":"write one quest, objective or NPC file whole. The document is judged by the same rules a read reports BEFORE it is written, so a change that could never work is refused rather than found at the next restart. baseVersion makes a concurrent edit a 409; the literal \"new\" CREATES the file instead and fails with 409 if it already exists."},"quests/set-active":{"destructive":false,"readOnly":false,"describe":"switch quests on or off by setting Active in their own files - the field the mod itself reads, so enablement has one owner and no second list to disagree with. Takes every ID in a toggle unit: a chain is indivisible, because Expansion reads a follow-up quest's givers before null-checking it and half a chain throws on the parent turn-in. Every file is read and checked before any is written. Deliberately NOT the ordinary save, which refuses a document carrying an error finding - a broken quest is the one you most need to be able to switch off. Takes effect at the next restart."},"quests/delete":{"destructive":false,"readOnly":false,"describe":"remove one quest, objective or NPC file. REFUSED while anything still points at it: deleting an objective a quest uses turns that quest into one the mod skips at load, and deleting a quest another chains into makes the parent's turn-in throw. The removed bytes are snapshotted on the box, so a wrong delete is recoverable."},"quests/progress":{"destructive":false,"readOnly":true,"describe":"who has quest progress saved on the box: PLAYER NAME, size, and when it was last written. Progress is per-player state the game writes, NOT config - this names the files and never reads what is inside them. Files are stored under a 44-character player UID, so the name is resolved from the ADM log; a player the log has not seen recently comes back with name null and only the UID, never a guessed name. The most recently written row is whoever played last."},"quests/progress-detail":{"destructive":false,"readOnly":true,"describe":"one player's saved quests, so a single questline can be reset instead of their whole history. Reads the progress file's CONTENTS - quest ids, state and completion counts - and joins each id to its title from the catalogue. A quest the catalogue no longer carries still lists, with title null: progress outlives the config that created it."},"quests/reset-quest":{"destructive":false,"readOnly":false,"describe":"reset ONE questline for one player, leaving the rest of their progress intact. Removing a quest's saved entry is what makes the mod offer it again from the start. All-or-nothing: if any named id has no saved progress the whole call is refused, because a partial match usually means the list was stale. The outgoing bytes are snapshotted on the box first. Refused only while THAT player is connected - other players online do not block it."},"quests/reset-progress":{"destructive":false,"readOnly":false,"describe":"erase saved quest progress so a quest can be taken again from the start. Pass file to reset one player, or all:true for everyone. The removed bytes are snapshotted on the box first. A single-player reset is refused only while THAT player is connected; all:true still needs an empty server. The mod keeps quest state in memory and writes it back on save, so a reset under the connected owner would do nothing."},"economy/export":{"destructive":false,"readOnly":true,"describe":"the live quest economy as a spreadsheet - every quest with its reward, AI count and effort estimate, plus the market price list for reference. Returns the .xlsx base64 encoded so it rides the ordinary signed JSON path. Edit the yellow cells and hand it back to economy/import."},"economy/import":{"destructive":false,"readOnly":false,"describe":"apply an edited economy workbook. REPORT ONLY unless apply:true - it returns every reward it would change so the caller can see the damage before agreeing to it. Writes exactly one field per quest (the ExpansionBanknoteHryvnia Amount) through own-write with base= concurrency. Refuses any quest the box changed after the workbook was exported, and any quest whose Rewards hold gear but no currency. Takes effect at the next restart."}}}}